Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel String Similarities Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

Pick the algorithm deliberately

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

Normalise before comparing

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);
}

Score-and-rank over a candidate set

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.

Pre-filter in the database

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.

Wrap it in a service for testability and a single normalisation point

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.

Custom validation rule

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.

Bulk de-duplication pattern

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.

Gotchas and Tips

  • 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.
  • Scales differ. Most methods return a 0–100 percentage, but confirm each in tinker. Mixing levenshtein's raw distance with jaroWinkler's percentage and then comparing to a threshold will silently misbehave.
  • Byte vs. multibyte. 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.
  • Whitespace and case are not normalised for you. similarText('Acme ', 'Acme') is not 100. Always trim and case-fold before scoring.
  • Empty strings. Guard against null/empty inputs before calling — depending on the algorithm you'll get 0, a warning, or a thrown exception. A simple if ($a === '' || $b === '') return 0.0; in your wrapper is worth it.
  • Levenshtein performance. It's O(n·m) in both time and memory. Cap inputs at a couple hundred characters before comparing, or you'll see latency spikes on pasted blobs.
  • Symmetry is not guaranteed. 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.
  • Mocking in tests. 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.
  • No config to publish, but do read the provider. If you need to override behaviour, php artisan vendor:publish --provider="Atomescrochus\StringSimilarities\StringSimilaritiesServiceProvider" will surface anything publishable; mostly you'll extend your own wrapper instead.
  • Extension point: your own comparator. If you need a different metric (cosine on tokens, Double Metaphone, etc.), add it to FuzzyMatcher behind the same interface rather than calling the facade inline across the codebase. That keeps the algorithm name in one place.
  • Queue the bulk work. Similarity scoring inside a request handler on >~1,000 candidates will show up in your APM. Move it to a job and cache results if the candidate set is stable.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names