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

Soundex Fr Bundle Laravel Package

keller/soundex-fr-bundle

Symfony bundle implementing the French Soundex phonetic algorithm, letting you index and compare French names and words by sound for fuzzy matching, tolerant search, and deduplication even when spellings differ.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle — there is no Laravel service provider, no config/*.php, and package discovery will not register anything. Treat it as a source of one algorithm, not a framework integration.

composer require keller/soundex-fr-bundle

If Composer complains about platform constraints (likely — bundles from this era often target PHP 5.x/7.x), that is your first signal to vendor the file rather than install the package:

composer require keller/soundex-fr-bundle --ignore-platform-reqs
# then copy the encoder class out, or:
# composer remove keller/soundex-fr-bundle

Where to look first. Open vendor/keller/soundex-fr-bundle/src/ and ignore everything except the encoder:

  • Skip: *Bundle.php, DependencyInjection/, Resources/config/, DependencyInjection/Configuration.php.
  • Keep: the class that actually implements the encoding — likely SoundexFr, FrenchSoundex, or similar. It is probably 50–150 lines of pure string transformation with no framework coupling.

First use case — a Tinker sanity check before you plan anything else.

php artisan tinker
>>> $enc = new Keller\SoundexFrBundle\SoundexFr(); // confirm actual FQCN
>>> $enc->soundex('Dupont');    // e.g. "D153"
>>> $enc->soundex('Dupon');     // should match
>>> $enc->soundex('Lefèvre');   // does it handle the accent?
>>> $enc->soundex('Lefevre');   // does it match the above?
>>> $enc->soundex('Œuvre');     // ligature — note what happens

If Dupont/Dupon collide and Lefèvre/Lefevre collide, you have something usable. If accented input returns null, throws, or produces a different key, that is the first thing you wrap before anything else. Run this against five or ten real names from your own database before committing to the two-day integration.

The first shippable slice is usually a "sounds like" filter on a name search or a duplicate-candidate list on a contacts index — not a full dedup/merge system.

Implementation Patterns

1. Vendor the encoder, bind it in a service provider

Copy the class into app/Support/Phonetics/ and change the namespace. Then:

// app/Providers/PhoneticsServiceProvider.php
namespace App\Providers;

use App\Support\Phonetics\FrenchSoundex;
use Illuminate\Support\ServiceProvider;

class PhoneticsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(FrenchSoundex::class, fn () => new FrenchSoundex());
    }
}

Program against an interface so you can swap the encoder later without touching call sites:

interface PhoneticEncoder
{
    public function encode(?string $value): ?string;
}

Bind FrenchSoundex::class to PhoneticEncoder::class. The day you decide Postgres pg_trgm or Meilisearch does it better, only the binding changes. Given the package is abandoned, this is your insurance policy.

2. Normalize in your wrapper, not in the vendored class

The vendored code is a pure function and you should leave it alone — put every domain decision in a wrapper:

final class FrenchSoundex implements PhoneticEncoder
{
    public function encode(?string $value): ?string
    {
        if (blank($value)) {
            return null;
        }

        $value = Str::ascii($value);                 // é→e, ç→c, ñ→n
        $value = strtr($value, ['œ' => 'oe', 'æ' => 'ae']);
        $value = mb_strtolower($value);
        $value = preg_replace('/[^a-z]/', '', $value);

        return $value === '' ? null : $this->soundex($value);
    }
}

The rule that matters: normalization must be identical at write time and query time. The single most common "why doesn't this match" bug is an encoder that strips accents when persisting and doesn't when searching (or vice versa).

3. Persist the key on the model, never compute it in a WHERE

// app/Models/Concerns/HasFrenchSoundex.php
trait HasFrenchSoundex
{
    public static function bootHasFrenchSoundex(): void
    {
        static::saving(function ($model) {
            foreach ($model->soundexAttributes ?? [] as $attr) {
                $model->{"{$attr}_soundex"} = app(PhoneticEncoder::class)
                    ->encode($model->{$attr});
            }
        });
    }
}
// contacts migration
$table->string('last_name_soundex', 8)->nullable();
$table->index('last_name_soundex');

Then backfill existing rows with a queued job (chunkById to keep memory flat). Remember: Model::insert() and upsert() bypass Eloquent events — if you have bulk import paths, the trait will not fire and your keys will be silently null. Either route imports through save() or set the column explicitly in the importer.

4. Two-stage query: soundex to narrow, similarity to rank

Soundex produces a key, not a score. Use it as a candidate generator, then rank the candidates with something that actually orders them:

$key = app(PhoneticEncoder::class)->encode($request->input('q'));

$candidates = Contact::query()
    ->where('last_name_soundex', $key)   // index hit — cheap
    ->limit(200)
    ->get();

$ranked = $candidates
    ->sortByDesc(fn ($c) => similar_text(
        Str::ascii(Str::lower($c->last_name)),
        Str::ascii(Str::lower($request->input('q'))),
    ))
    ->take(10);

Without the second stage, every result is "equally correct" and the UI is useless.

5. Version the column for key drift

Any change to the encoder — a bug fix, a ligature added, upgrading the vendored file — invalidates every stored key, and it fails silently (false negatives, no exception). Cheapest mitigation: name the column last_name_soundex_v1, and ship a reindex job with the change. If you can't version the column, store soundex_version alongside and reindex when it changes.

6. Combine with exact-match and LIKE in a UNION/scored query

Real search paths usually stack three passes: exact match, prefix/LIKE, then soundex fallback. Weight them so an exact hit always outranks a phonetic hit, otherwise identical-name users get buried under phonetic collisions.

7. Fixture-driven tests

Write PHPUnit tests against a fixed list of real surnames from your own data — not Dupont. This is your only defence against a silent regression when someone "tidies up" the encoder:

it('collapses spelling variants', function () {
    $enc = new FrenchSoundex();
    expect($enc->encode('Dupont'))->toBe($enc->encode('Dupon'));
    expect($enc->encode('Lefèvre'))->toBe($enc->encode('Lefevre'));
});

Gotchas and Tips

It will not install cleanly on PHP 8.2. The Symfony-era constraints often pin php: ^5.6 || ^7.0 and an ancient symfony/framework-bundle. That is not a bug in your app — it is the package telling you it was last touched a decade ago. Vendor the class; do not fight --ignore-platform-reqs in production.

Bundle wiring is dead code in Laravel. services.yml, the DI extension, and the Bundle class never execute. The encoder works when instantiated directly, but app()->make(SoundexFr::class) will fail unless you bind it yourself.

ASCII-only string functions. Old implementations typically use strtolower/strtoupper, which do nothing useful on É. Run Str::ascii() before the encoder ever sees the string, or patch the class to use mb_*.

Ligatures are the usual blind spot. œ and æ are not covered by Str::ascii() in every Laravel version. Œuvre and Oeuvre producing different keys is the classic silent failure — handle strtr explicitly.

setlocale dependence. If the class calls setlocale(LC_CTYPE, ...), output varies across servers with different locales installed. Grep for setlocale before you deploy. If it's there, remove it.

Collisions on short French surnames are brutal. A four-character key means Roy, Roux, Rou, and Roe all likely collapse to R000/R200. Treat a soundex match as "candidate worth ranking", never as "same person". This matters most for the KYC/dedup cases where a false positive is expensive.

Never compute soundex inside the query. ->whereRaw("soundex(last_name) = ?") is a full table scan. The whole point of persisting the column is to hit an index. Same applies to SOUNDEX() in SQL — which, incidentally, is not the French variant, so do not mix PHP-computed keys with DB-computed ones. Pick one and apply it everywhere.

Hyphenated and compound names. Dupont-Moreau, Jean-Pierre, de la Fontaine. Decide whether to encode the whole string, the last token, or strip particles, and apply that rule at write and read. Do not strip de/le unless you also strip it from the query input — a one-sided normalizer is worse than none.

Str::ascii() on ß, ø, ł. You will encounter these via German, Nordic, and Polish names in any French-market dataset. Check your fixture set against the transcoder you actually use.

License before you ship. The repo's license file governs whether you can vendor this into a closed-source product with attribution. Check LICENSE in the package root before the copy-paste, not after.

Assume zero upstream support. Six stars, zero dependents: no issue triage, no security response, no fix for whatever Unicode edge case you hit. Pin the vendored file's commit SHA in a comment above the class so the next engineer knows exactly where it came from and whether anyone has diverged from it.

Check whether you need it at all. If you already run Postgres, pg_trgm's similarity() plus a GIN index handles typo-tolerant name matching with infrastructure you already back up and monitor. If you use Meilisearch via Scout, its typo-tolerance setting often covers the same ground. The soundex column is worth building when you specifically need a key to join on across tables, or when you want deterministic, offline, auditable behaviour with no external service in the request path. Otherwise it is a nice-to-have enrichment — do not let it block a release.

Extension points: essentially none. The vendored class is a single pure function. Every useful extension lives in your wrapper — normalization, particle stripping, versioning, the interface. That is exactly the right place for it, and it's why forking is cheap here.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log