gibilogic/slugger-bundle
gibilogic/slugger-bundle is a PHP bundle for generating clean, URL-friendly slugs from titles and text. It provides reusable slugging utilities and configurable behavior for Symfony-style applications, helping keep routes and content identifiers consistent.
Know what you're actually getting. gibilogic/slugger-bundle is a Symfony 2 bundle, not a Laravel package. It does one thing: registers a small slugger service in the Symfony DI container so you can inject a slugify helper instead of calling a static function. There is no ServiceProvider, no facade, no config file to publish, and no Illuminate dependency anywhere in it. Last release was 2018-10-23, it targets Symfony 2 conventions (XML service definitions under Resources/config/), and it has zero stars and zero dependents. Treat it as a reference implementation to copy, not a dependency to composer require.
Where to look first. In the repo, open the service definition (Resources/config/services.xml or equivalent) to find the service id and any aliases, then open the slugger class itself — it will be one class with essentially one public method (slugify($string, $separator = '-')). The entire useful surface area is under ~50 lines. Confirm whether it delegates to iconv/intl transliteration or hand-rolled str_replace maps, because that determines output quality for non-ASCII input.
Minimal Laravel path (recommended):
app/Support/Slugger.php, swap any Symfony-specific type hints for plain PHP.// AppServiceProvider::register()
$this->app->singleton(\App\Support\Slugger::class);
app(\App\Support\Slugger::class)->slugify('Hello, World!'); // hello-world
If you genuinely want to pull the vendor package in for study, expect PHP/constraint conflicts with modern Laravel; you can composer require with --ignore-platform-reqs in a scratch project, but do not ship it.
First real use case. Generating a URL-safe slug from a title column on save, then routing to records by slug instead of id. That's the 90% case and it's what the next section covers.
Bind, don't new. The only thing the bundle buys you is injectability, so preserve that in Laravel. Depend on your own interface (App\Contracts\Slugifier) and let the copied class implement it — that way if you later swap to Str::slug() or cocur/slugify, nothing in your domain code changes.
A trait for the Eloquent side. This is the pattern that replaces what the bundle would do for you:
trait HasSlug
{
public static function bootHasSlug(): void
{
static::saving(function ($model) {
if (blank($model->slug) || $model->isDirty($model->slugSource())) {
$model->slug = $model->generateSlug();
}
});
}
protected function slugSource(): string
{
return 'title';
}
protected function generateSlug(): string
{
return app(Slugifier::class)->slugify($this->{$this->slugSource()});
}
}
Uniqueness — pick a strategy deliberately. Three viable ones:
post-2, post-3): loop while static::where('slug', $candidate)->exists(), then rely on a unique DB index plus a retry-on-QueryException wrapper, because the loop is racy under concurrency.post-a1b2c3): deterministic enough and collision-free in practice, but uglier URLs.slugs (slug, sluggable_id, sluggable_type) and 301 from old to new. Best for SEO, most work.Route model binding. Once slugs exist, getRouteKeyName(): string { return 'slug'; } gives you /posts/hello-world for free. Keep the numeric id available for admin routes so a slug rename never breaks internal links.
Batch backfill. For existing rows, write an artisan command using chunkById(500, ...) and saveQuietly() inside the loop so you don't fire observers, then run the uniqueness pass separately. Never Model::all() in a backfill.
Non-model use cases are where a standalone service still earns its keep: sitemap URL generation, importers that build filenames, CMS page paths, image/file naming. In those places call the service directly rather than reaching for Eloquent.
Multi-locale. Slugify per locale and store per-locale columns (slug_en, slug_fr) or a translations table row per locale; don't try to make one slug serve every language, and don't re-slugify on read.
SluggerBundle::class in config/bundles.php is meaningless here; the XML DI definitions won't be parsed by Laravel's container.composer.json.Crème Brûlée or 北京 depends on whether intl/iconv is compiled in, and on locale settings. Pin behavior in a unit test with a fixed set of inputs rather than trusting it.''. Always fall back to Str::random()/UUID or the record id, and reject empty slugs before insert.utf8mb4_unicode_ci column treats Hello and hello as the same slug — usually what you want, but be explicit. Also cap the slug column length (191 for older MySQL utf8mb4 indexes) and truncate generated slugs to match, or inserts will fail intermittently.saving fires on save()/create(), not on update(), insert(), or raw query-builder writes. Mass imports and admin bulk actions will silently produce null slugs; guard the column as NOT NULL so failures are loud.admin, api, login, new, edit as slugs will collide with your route table. Either prefix routes (/p/{slug}) or blocklist those values.Str::slug($input) in tinker — Str::slug is the de facto baseline in Laravel and any divergence is either a feature you want or a bug you inherited from the 2015 implementation.App\Contracts\Slugifier. If you skip this, a future migration off this code touches every model and controller that calls it.How can I help you explore Laravel packages today?