jfcherng/php-sequence-matcher
PHP longest sequence matcher inspired by Python difflib. Compare arrays or strings to find matching blocks and measure similarity, useful for diffing and change detection. Lightweight, modern PHP (8.4+) package.
Verify PHP 8.3+ Compatibility:
php -v # Must be ≥8.3
composer require jfcherng/php-sequence-matcher
Basic Comparison (Arrays/Strings):
use jfcherng\SequenceMatcher\SequenceMatcher;
use jfcherng\SequenceMatcher\Options;
$matcher = new SequenceMatcher(
['a', 'b', 'c'], // Sequence A
['x', 'b', 'y'], // Sequence B
new Options() // Object-based config (PHP 8.3+)
);
First Use Case: Generate Opcodes
$opCodes = $matcher->getOpCodes();
// Example output: [['replace', 0, 1, 0, 1], ['equal', 1, 2, 1, 2]]
Key Methods to Explore:
ratio() → Similarity score (0.0–1.0)getMatchingBlocks() → Identical subsequencesgetOpCodes() → Edit instructions (insert/delete/replace/equal)Laravel Service Wrapper (Recommended)
namespace App\Services;
use jfcherng\SequenceMatcher\SequenceMatcher;
use jfcherng\SequenceMatcher\Options;
class SequenceMatcherService {
public function __construct(
private SequenceMatcher $matcher
) {}
public static function create(array $a, array $b): self {
return new self(
new SequenceMatcher($a, $b, new Options())
);
}
public function getChanges(): array {
return $this->matcher->getOpCodes();
}
}
Register in AppServiceProvider:
$this->app->bind(SequenceMatcherService::class, function () {
return SequenceMatcherService::create($oldData, $newData);
});
Diff Rendering for UI
$changes = [];
foreach ($matcher->getOpCodes() as $op) {
[$tag, $i1, $i2, $j1, $j2] = $op;
switch ($tag) {
case SequenceMatcher::OP_REP:
$changes[] = "Line {$i1}: '{$old[$i1]}' → '{$new[$j1]}'";
break;
case SequenceMatcher::OP_DEL:
$changes[] = "Line {$i1}: Removed '{$old[$i1]}'";
break;
case SequenceMatcher::OP_INS:
$changes[] = "Line {$j1}: Added '{$new[$j1]}'";
break;
}
}
return view('diff', ['changes' => $changes]);
Schema Validation with Similarity Thresholds
$similarity = $matcher->ratio();
if ($similarity < 0.9) {
throw new \RuntimeException(
"Schema drift detected (similarity: {$similarity * 100}%)"
);
}
Token-Level Diffing (e.g., for Code)
$tokensOld = token_get_all(file_get_contents('old.php'));
$tokensNew = token_get_all(file_get_contents('new.php'));
$matcher = new SequenceMatcher($tokensOld, $tokensNew);
$diffs = $matcher->getOpCodes();
getMatchingBlocks() to identify identical chunks for partial processing.array_map('strtolower', $sequence)).SequenceMatcher instances for repeated comparisons:
$matcher = Cache::remember(
"diff_{$hashOld}_{$hashNew}",
now()->addHours(1),
fn() => new SequenceMatcher($old, $new, new Options())
);
PHP Version Mismatch:
Class 'jfcherng\SequenceMatcher\Options' not found → Cause: PHP <8.3.chrisboulton/php-diff for PHP 8.1).Opcode Confusion (v3.0+):
OP_EQ is now 1 (not 'eq' string). Use constants:
if ($op[0] === SequenceMatcher::OP_REP) { ... }
Options for IDE autocompletion.Object-Based Options Overhead:
serialize()-compatible wrappers or avoid caching SequenceMatcher directly.Non-Greedy Matching:
difflib's greedy algorithm.Options for custom behavior (check Options class docs).Memory Spikes:
getMatchingBlocks() for partial matches.$opCodes = $matcher->getOpCodes();
dump($opCodes); // Debug structure: [tag, i1, i2, j1, j2]
$ratio = $matcher->ratio();
dump($ratio); // Should be 0.0–1.0
$blocks = $matcher->getMatchingBlocks();
dump($blocks); // [[i1, i2, j1, j2], ...]
Custom Opcodes:
Combine base opcodes (e.g., OP_INS | OP_DEL for complex edits).
$op = SequenceMatcher::OP_INS | SequenceMatcher::OP_DEL;
Subclass Options:
Extend for project-specific defaults:
class AppOptions extends Options {
public function __construct() {
parent::__construct();
$this->setAutoJunk(false); // Disable junk filtering
}
}
Laravel Facade: Create a facade for cleaner syntax:
// app/Facades/SequenceMatcher.php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class SequenceMatcher extends Facade {
protected static function getFacadeAccessor() {
return 'sequenceMatcher';
}
}
Bind in AppServiceProvider:
$this->app->singleton('sequenceMatcher', function () {
return new SequenceMatcher([], [], new Options());
});
Usage:
$diffs = \App\Facades\SequenceMatcher::getOpCodes();
SequenceMatcher directly—wrap it in a service class to handle dynamic sequences.Options only, not the full SequenceMatcher:
$job->handle(new Options($matcher->getOptions()));
SequenceMatcher in unit tests:
$this->partialMock(SequenceMatcher::class, function ($mock) {
$mock->method('getOpCodes')->willReturn([['equal', 0, 1, 0, 1]]);
});
How can I help you explore Laravel packages today?