integrated/slug-bundle
A Laravel bundle for generating clean, URL-friendly slugs from strings, helping you build readable routes and permalinks with consistent, SEO-friendly formatting across your application.
Calibrate first: this is a Symfony bundle, not a Laravel package. The Bundle suffix, the event-listener design, and the integratedfordevelopers org all point at Symfony + Doctrine. There is no Illuminate\Support\ServiceProvider, no package auto-discovery, and no publishable config/slug.php. Treat it as a reference implementation to read and possibly vendor, not a dependency to composer require into a Laravel app.
Minimal steps that actually work:
composer require integrated/slug-bundle may resolve in a Laravel app (the bundle class just sits inert), but it also drags in Symfony component constraints that can fight with the versions Laravel already pins. Read composer.json before anything else — if it wants a Symfony major that doesn't intersect with your Laravel version, stop there.(string, options) => string.prePersist/preUpdate hook that writes the slug back onto the configured entity field. This is the part that has no execution path in Laravel.php artisan tinker against your own product titles, not synthetic strings. Compare its output to Str::slug($title) and to Symfony\Component\String\AsciiSlugger. That comparison is the entire evaluation.First use case to test it against: "auto-generate a slug from title on create, keep it unique, never regenerate on update." If this bundle can't make that use case cheaper than a 30-line HasSlug trait over Str::slug() plus a unique index, it isn't worth the dependency — and in practice it won't be.
The realistic patterns are porting, not installing.
Spike in isolation, then decide. Bind the slugger as a singleton in AppServiceProvider and call it from an artisan command that walks a corpus of real titles. Make the acceptance gate a golden-file suite (input → expected slug) seeded from production data. If its output beats Str::slug() on your corpus, vendor the single class with MIT attribution in a NOTICE file; if it doesn't, use cocur/slugify or symfony/string's AsciiSlugger and keep going. Your integration code is identical either way.
Event listener → Eloquent observer or trait hook. The bundle's prePersist/preUpdate listeners translate to either a saving hook on the model or a HasSlug trait:
// app/Concerns/HasSlug.php
protected static function bootHasSlug(): void
{
static::saving(function ($model) {
$source = $model->slugSource ?? 'title';
if (blank($model->slug) && filled($model->{$source})) {
$model->slug = app(Slugger::class)->slug($model->{$source}, $model);
}
});
}
Guard on blank($model->slug) so a curated slug is never overwritten — this is the Laravel equivalent of the bundle's "generate on chosen fields" behavior, with immutability built in.
Config mapping → per-model properties or config/slugs.php. The bundle's YAML field mapping becomes either protected properties on the model ($slugSource, $slugField) or an entry in a config array keyed by model class. Prefer properties — it keeps the mapping next to the model and avoids a config cache dependency.
Uniqueness as a suffix loop against a DB unique index. The bundle's duplicate-detection is app-level; do not rely on it. Make slug a real unique column and retry:
$base = $slugger->slug($title);
$slug = $base; $i = 1;
while ($this->slugExists($slug, $model->getKey())) {
$slug = $base . '-' . (++$i);
}
Or catch the unique-constraint violation and retry the save. The constraint is the contract; the loop is the UX.
Backfill as its own chunked, throttled command. chunkById(500), generate, log collisions, leave the column non-unique until the table is clean, then add the unique index in a follow-up migration. Don't do this in the same deploy that adds the column.
Port the "custom generator" extension point as a strategy binding. If the bundle exposes a generator interface, mirror it as a SlugGenerator contract in app/Contracts/ with a default transliteration implementation bound in the container. That gives you the same swap-ability without the bundle's DI wiring.
The update listener is the landmine. Auto-regeneration on entity update silently rewrites published URLs and breaks inbound links. If you port the listener behavior, scope generation to new records only, or gate regeneration behind an explicit regenerateSlug() call. Any regeneration path ships with an old-slug → new-slug redirect table in the same change, not a follow-up ticket.
Transliteration quality is the whole library, and it's unverified here. Diacritics are usually fine; CJK, emoji, and non-Latin scripts are where cheap sluggers fall over. Test with your actual content. Add a fallback: if the generated slug is empty or just -, use a UUID fragment or the record ID rather than writing a broken URL, and alert on empty-slug rate.
Composer constraint conflicts are the likely first failure. Laravel pins symfony/console, symfony/http-foundation, symfony/process, and often symfony/string. A bundle requiring a different major will either fail resolution or force an upgrade you don't want. Check constraints before you spend time on output quality.
Truncate deterministically to your column length. Slug columns are typically varchar(191) or (255) for index limits. Long titles will either throw or silently truncate at the DB. Truncate in the slugger, on a word or separator boundary, so behavior is identical across MySQL and SQLite test runs.
Uniqueness lookups are one indexed read per write. Fine for interactive saves, painful in bulk imports — N queries plus N constraint checks. For imports, pre-compute an in-memory set of taken slugs for the batch and resolve collisions before insert. Also be aware the check-then-insert is racy under concurrency; rely on the unique index and retry rather than trusting the pre-check.
Watch utf8mb4 collation. Uniqueness on slug behaves differently under case-insensitive collations; that's usually what you want, but confirm it matches your suffix loop's assumptions or you'll get surprising -2 duplicates.
No upstream tests, no changelog, one star, zero dependents. There is no community to catch regressions and no security advisory channel. If you vendor anything, name an internal owner and record it in a NOTICE/service catalogue entry — an unowned slug function becomes a mystery box within two quarters.
Own the transliteration table refresh. This is the recurring cost you take on by not depending on a maintained package: budget roughly half a day a year for Unicode table updates, plus work whenever you add a locale. Multi-language slugs (per-locale uniqueness, per-locale slug columns) are not handled by this package at all.
Debugging tip: when a slug looks wrong, isolate whether it's the transform or the persistence hook. Run the slugger class directly in tinker with the same input — if it produces the right output there, the bug is in your observer, the uniqueness loop, or the column length, not the library.
How can I help you explore Laravel packages today?