atomescrochus/laravel-string-similarities
Compare strings in Laravel with a simple facade or helper. Get similarity scores and percentages using similar_text, levenshtein, soundex, metaphone and more, making fuzzy search, deduplication and typo-tolerant matching easy to build.
The package is a thin Laravel wrapper around atomescrochus/php-string-similarities, exposing a facade and an injectable Compare class that each return a similarity percentage between two strings.
Install:
composer require atomescrochus/laravel-string-similarities
On Laravel 5.5+ the service provider and StringSimilarity facade are auto-discovered. On older apps, register Atomescrochus\StringSimilarities\StringSimilaritiesServiceProvider in config/app.php and alias 'StringSimilarity' => Atomescrochus\StringSimilarities\Facades\StringSimilarity::class.
Where to look first: the underlying repo atomescrochus/php-string-similarities — the wrapper barely changes the API, and that's where the exact return values and edge-case behaviour are documented.
First use case — fuzzy name/title matching:
use Atomescrochus\StringSimilarities\Facades\StringSimilarity;
StringSimilarity::similarText('Acme Corporation', 'Acme Corporaton'); // ~96.x
StringSimilarity::jaroWinkler('Jon', 'John'); // ~93.x
StringSimilarity::levenshtein('kitten', 'sitting'); // edit distance, normalised
Before wiring anything into a feature, drop into php artisan tinker and compare a handful of realistic strings per algorithm. The algorithms do not share a scale or a sense of "similar", and you want to see that with your own data before choosing one.
The canonical first task is a "did you mean…" suggestion or de-duplicating a CSV of contacts — both are a loop over candidates plus a threshold.
Different methods suit different inputs; treat this like picking a hashing function, not a default:
| Method | Good for | Notes |
|---|---|---|
similarText |
General prose, addresses | Byte-based, order-sensitive |
levenshtein |
Typos on short strings | O(n·m) — truncate long input |
jaroWinkler |
Short names, usernames, SKUs | Weighted toward common prefixes |
hamming |
Fixed-width codes (postal, EAN) | Requires equal length |
smithWatermanGotoh |
Longer free text | Slowest; use sparingly |
Raw comparison punishes casing, punctuation, and accents. Wrap it:
protected function normalise(string $s): string
{
$s = trim($s);
$s = mb_strtolower($s, 'UTF-8');
$s = transliterator_transliterate('Any-Latin; Latin-ASCII', $s); // drop accents
return preg_replace('/\s+/', ' ', $s);
}
The most common workflow — map to scores, sort, filter:
use Atomescrochus\StringSimilarities\Facades\StringSimilarity;
$needle = 'Acme Corporaton';
$matches = collect($candidates)
->map(fn ($c) => [
'candidate' => $c,
'score' => StringSimilarity::jaroWinkler($needle, $c),
])
->sortByDesc('score')
->filter(fn ($r) => $r['score'] >= 85)
->values();
Thresholds in the 80–90 band tend to mean "probably the same"; below ~70 you get noise. Tune per algorithm and per field.
You cannot push these algorithms into SQL. For any non-trivial table, narrow the candidate set first with cheap SQL, then score in PHP:
$candidates = Customer::query()
->where('last_name', 'like', mb_substr($needle, 0, 3).'%') // or SOUNDEX() in MySQL
->limit(500)
->pluck('last_name');
Score in a queued job if the candidate set is large — a few thousand Levenshtein calls per request will hurt.
namespace App\Support;
use Atomescrochus\StringSimilarities\Facades\StringSimilarity;
class FuzzyMatcher
{
public function score(string $a, string $b, string $algo = 'jaroWinkler'): float
{
$a = $this->normalise($a);
$b = $this->normalise($b);
return (float) StringSimilarity::{$algo}($a, $b);
}
}
Bind it in the container and inject FuzzyMatcher rather than the facade directly — you'll thank yourself when you want to swap algorithms or mock scores in tests.
A neat integration for "confirm this field matches what we have":
public function passes($attribute, $value): bool
{
return app(FuzzyMatcher::class)->score($value, $this->expected, 'similarText') >= 90;
}
Useful for lenient confirmations (e.g. company name entry) while keeping a strict fallback elsewhere.
For dedup, compare each record only against a reduced set (same first letter, same postcode district, shared rare token) rather than all pairs — O(n²) is the trap here, not the algorithm itself.
hamming throws on unequal lengths. Guard with strlen($a) === strlen($b) or skip it entirely for user input. It's the one method that will break quietly in production.levenshtein's raw distance with jaroWinkler's percentage and then comparing to a threshold will silently misbehave.similarText counts bytes, not characters — accented or non-Latin input can swing a score by 10+ points. Transliterate first; don't rely on mb_* alone.similarText('Acme ', 'Acme') is not 100. Always trim and case-fold before scoring.if ($a === '' || $b === '') return 0.0; in your wrapper is worth it.similarText($a, $b) and similarText($b, $a) can return different values because of the matching heuristic. Pick an argument order and stick to it in comparisons.StringSimilarity::shouldReceive(...) works via the Facade's swap, but partial mocks need the underlying Compare to be container-bound. Easier: inject your FuzzyMatcher wrapper and mock that.php artisan vendor:publish --provider="Atomescrochus\StringSimilarities\StringSimilaritiesServiceProvider" will surface anything publishable; mostly you'll extend your own wrapper instead.FuzzyMatcher behind the same interface rather than calling the facade inline across the codebase. That keeps the algorithm name in one place.How can I help you explore Laravel packages today?