jfcherng/php-sequence-matcher
PHP 8.4+ longest sequence matcher inspired by Python difflib. Compare strings or arrays to find matching blocks and similarities for diffing, change detection, and text analysis. Lightweight library extracted from php-diff with improvements.
Installation:
composer require jfcherng/php-sequence-matcher
Requires PHP 8.4+ (compatible with Laravel 11+).
First Use Case: Compare two strings for similarity ratio (0.0 to 1.0):
use Jfcherng\SequenceMatcher\SequenceMatcher;
$matcher = new SequenceMatcher();
$ratio = $matcher->ratio("Laravel", "Laravul"); // ~0.857
Where to Look First:
ratio(), getOpCodes()).Basic Comparisons:
$matcher = new SequenceMatcher();
$ratio = $matcher->ratio($a, $b); // Similarity score (0.0–1.0)
$isSimilar = $ratio >= 0.7; // Custom threshold
Laravel Integration:
AppServiceProvider):
$this->app->singleton(SequenceMatcher::class, fn() => new SequenceMatcher());
Macroable):
SequenceMatcher::macro('isSimilar', fn($a, $b, $threshold = 0.7) =>
app(SequenceMatcher::class)->ratio($a, $b) >= $threshold
);
Eloquent Observers:
Track field changes by comparing old() and new() attributes:
class PostObserver {
public function saved(Model $post) {
if ($post->isDirty('content')) {
$similarity = app(SequenceMatcher::class)
->ratio($post->getOriginal('content'), $post->content);
logger()->info("Content similarity: {$similarity}");
}
}
}
API Responses: Return similarity scores in JSON:
return response()->json([
'data' => $model,
'similarity' => app(SequenceMatcher::class)->ratio($model->content, $referenceContent),
]);
Queue Jobs: Offload heavy comparisons (e.g., large arrays):
class CompareLargeDataJob implements ShouldQueue {
public function handle() {
$matcher = new SequenceMatcher();
$result = $matcher->getOpCodes($largeArrayA, $largeArrayB);
// Store results in DB/cache
}
}
Data Migration: Compare legacy and new database records:
$legacy = LegacyModel::find($id)->toArray();
$new = NewModel::find($id)->toArray();
$similarity = app(SequenceMatcher::class)->ratio(json_encode($legacy), json_encode($new));
Testing: Assert API responses match expected outputs:
$response = $this->get('/api/data');
$expected = ['key' => 'value'];
$similarity = app(SequenceMatcher::class)->ratio($response->json(), $expected);
$this->assertGreaterThan(0.9, $similarity);
Content Moderation: Flag duplicate submissions:
$submissions = Submission::all()->pluck('content');
$newSubmission = request('content');
$duplicates = collect($submissions)
->filter(fn($content) => app(SequenceMatcher::class)->ratio($content, $newSubmission) > 0.9);
Normalization: Preprocess data for consistent comparisons:
$normalizedA = Str::lower(json_encode($a, JSON_UNESCAPED_UNICODE));
$normalizedB = Str::lower(json_encode($b, JSON_UNESCAPED_UNICODE));
$ratio = app(SequenceMatcher::class)->ratio($normalizedA, $normalizedB);
Caching: Cache results for repeated comparisons:
$cacheKey = "similarity:{$aHash}:{$bHash}";
return cache()->remember($cacheKey, now()->addHours(1), fn() =>
app(SequenceMatcher::class)->ratio($a, $b)
);
Batch Processing: Use Laravel Collections for bulk operations:
$results = collect($itemsA)
->crossJoin($itemsB)
->map(fn($pair) => [
'a' => $pair[0],
'b' => $pair[1],
'similarity' => app(SequenceMatcher::class)->ratio($pair[0], $pair[1]),
]);
Custom Scoring: Extend the matcher for domain-specific logic:
class CustomMatcher extends SequenceMatcher {
public function weightedRatio(array $a, array $b, array $weights) {
$total = 0;
foreach ($a as $key => $value) {
$weight = $weights[$key] ?? 1;
$total += $weight * $this->ratio($value, $b[$key] ?? '');
}
return $total / array_sum($weights);
}
}
Memory Usage:
Allowed memory exhausted.$chunkSize = 1000;
$chunksA = array_chunk($largeArrayA, $chunkSize);
$chunksB = array_chunk($largeArrayB, $chunkSize);
$results = [];
foreach (array_map(null, $chunksA, $chunksB) as $chunkAAndB) {
$results[] = app(SequenceMatcher::class)->ratio($chunkAAndB[0], $chunkAAndB[1]);
}
Unicode Handling:
"café") may not compare as expected.Str::ascii() or mb_convert_encoding():
$normalized = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
Floating-Point Precision:
0.7000000000000001 may fail strict comparisons.$rounded = round($matcher->ratio($a, $b), 2);
Object Comparison:
$ratio = app(SequenceMatcher::class)->ratio(
json_encode($modelA->toArray()),
json_encode($modelB->toArray())
);
Performance with Arrays:
getOpCodes() for granular control:
$opcodes = $matcher->getOpCodes($arrayA, $arrayB);
// Process opcodes manually for better performance
Inspect Opcodes: Debug mismatches by examining operation codes:
$opcodes = $matcher->getOpCodes($a, $b);
foreach ($opcodes as $op) {
logger()->debug("Op: {$op[0]}, A: {$op[1]}, B: {$op[2]}");
}
1 = equal ('eq').2 = delete ('del').4 = insert ('ins').8 = replace ('rep').Benchmarking:
Compare performance with Python’s difflib:
# Python benchmark
from difflib import SequenceMatcher
import time
start = time.time()
for _ in range(1000):
SequenceMatcher(None, "long_string" * 100, "long_string" * 100 + "x").ratio()
print(time.time() - start)
Edge Cases: Test these scenarios:
ratio("", "") → 1.0.ratio("abc", "abc") → `1.How can I help you explore Laravel packages today?