happyr/slugify-bundle
A Symfony bundle for generating URL-friendly slugs from titles and other text. It integrates with Doctrine entities and Twig to help you create, store, and render clean slugs in your application.
Before you composer require anything: this is a Symfony 2 bundle, not a Laravel package. There is no ServiceProvider, no Facade, no publishable config, and no Kernel registration mechanism Laravel recognizes. What actually ships is a thin DI wrapper around cocur/slugify plus a Twig filter. "Leveraging this package day-to-day in Laravel" therefore means one of two things:
cocur/slugify directly and write ~20 lines of container binding. (Recommended — this is what 95% of teams mean when they say they want this package.)happyr/slugify-bundle, which will likely fail composer install against a modern Laravel app because its symfony/framework-bundle constraint resolves to a Symfony 2.x/3.x generation. Verify with:composer require happyr/slugify-bundle --dry-run
A resolution failure here is the fast, clean reject signal. Assume incompatible until proven otherwise.
Recommended first step — cocur/slugify directly (framework-agnostic):
composer require cocur/slugify
Look first at the cocur/slugify README (not the bundle's) — that's where the actual API surface, character maps, and rule system live. The bundle adds nothing you can't bind yourself.
First use case — slug generation from a title, no bundle required:
use Cocur\Slugify\Slugify;
$slugify = new Slugify();
$slugify->slugify('Héllo Wörld'); // "hello-world"
If you're on Laravel 9+, Str::slug() already does this via voku/portable-ascii:
use Illuminate\Support\Str;
Str::slug('Héllo Wörld'); // "hello-world"
Only reach for cocur/slugify if you need custom transliteration rules, per-language rulesets, or the slugify rule chaining API. Otherwise Str::slug() is the entire story.
Bind a singleton in a service provider. The one thing the bundle actually provides is container wiring — do it yourself:
// app/Providers/SlugifyServiceProvider.php
namespace App\Providers;
use Cocur\Slugify\Slugify;
use Illuminate\Support\ServiceProvider;
class SlugifyServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(Slugify::class, function () {
return (new Slugify())
->activateRuleSet('default');
});
}
}
Register it in config/app.php (or bootstrap/providers.php on Laravel 11). This is the functional equivalent of the Symfony bundle's services.xml, and it's the only thing you needed from the bundle.
Inject the slugifier, never the container. Same discipline the Symfony bundle encourages — pass the service into domain code:
class CreatePost
{
public function __construct(private Slugify $slugify) {}
public function handle(string $title, string $body): Post
{
return Post::create([
'title' => $title,
'body' => $body,
'slug' => $this->slugify->slugify($title),
]);
}
}
Blade directive for template parity with the Symfony Twig filter. The bundle's headline feature is a {{ title|slugify }} Twig filter. Blade equivalent:
// app/Providers/SlugifyServiceProvider.php, boot()
use Illuminate\Support\Facades\Blade;
Blade::directive('slugify', function ($expression) {
return "<?php echo e(app(\Cocur\Slugify\Slugify::class)->slugify($expression)); ?>";
});
<a href="{{ route('posts.show', ['slug' => $post->slug]) }}">@slugify($post->title)</a>
Model-attribute auto-slugging (the Doctrine lifecycle callback analog). The Symfony pattern is @PrePersist/@PreUpdate. Laravel equivalents — pick one:
// Modern, attribute-friendly
protected static function booted(): void
{
static::saving(function (Post $post) {
if ($post->isDirty('title') || blank($post->slug)) {
$post->slug = app(Slugify::class)->slugify($post->title);
}
});
}
Or, more idiomatically and with uniqueness handling built in, use spatie/laravel-sluggable and drop the custom service entirely:
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Post extends Model
{
use HasSlug;
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('title')
->saveSlugsTo('slug');
}
}
Configuring separators and rules (the "centralized configuration" the bundle advertises). Do this in config/slugify.php and read it in the provider:
// config/slugify.php
return [
'separator' => '_',
'rulesets' => ['default', 'czech'],
];
// in the provider
$config = config('slugify');
$slugify = new Slugify(['separator' => $config['separator']]);
$slugify->activateRuleset('default');
Do not install the bundle "just for the filter." Installing happyr/slugify-bundle will pull transitive symfony/framework-bundle classes into your Laravel dependency graph. Even where Composer resolves it via symfony/* aliasing, you're loading Symfony kernel abstractions into an environment that has none, and any future PHP minor bump that the abandoned package can't handle becomes your emergency. Reserve this only for a repo that is a Symfony 2 app.
Silent transliteration drift is the biggest real risk. If you start on this bundle (or cocur/slugify with one ruleset) and later swap the underlying library or upgrade it, already-published slugs will diverge from newly generated ones — invisible until a user reports a 404 on a cached URL. Mitigation:
No upstream safety net — treat it as your code. 2 stars, 0 dependents, EOL host framework. Any bug you hit becomes yours. If you genuinely need the behavior this bundle provides, own it: vendor the character maps you care about into config/slugify.php and a App\Support\Sluggable trait, and cite the library in your own tests. Don't leave a runtime dependency on code nobody is patching.
Uniqueness is not solved by this package (or by Str::slug()/cocur/slugify). All three produce a slug; none of them guarantee it's unique. Do this at the database layer:
$slug = $slugify->slugify($title);
$original = $slug;
$i = 1;
while (Post::where('slug', $slug)->exists()) {
$slug = "{$original}-{$i}";
$i++;
}
Better: a unique:posts,slug column constraint plus a retry on insert, or delegate to spatie/laravel-sluggable, which handles collisions for you.
Transliteration quirks to test explicitly before deploying. Slugifiers differ on emoji, CJK, RTL scripts, and punctuation-heavy strings. Before you commit to any behavior, run a real corpus through both the bundle-era output and your Laravel-side choice and diff:
$inputs = ['Héllo Wörld', '東京', 'naïve café', 'a/b/c', '🔥 hot take'];
foreach ($inputs as $in) {
dump($in => [$slugify->slugify($in), Str::slug($in)]);
}
Any divergence on user-facing strings is a potential broken-URL incident.
Twig filter name is slugify in the bundle's docs — but verify it in Resources/config/services.xml. The README is not the source of truth for a 2-star package. The file that actually registers the Twig extension is what you'd port if you were recreating the filter parity in Blade (see the Blade::directive snippet above).
If you inherit a legacy Symfony 2 service that already uses this bundle: leave it pinned, don't extend it, and fold its removal into the framework-upgrade ticket — not a standalone task. The removal cost is a half-day spike plus a gap-analysis between the bundle's output and cocur/slugify / Str::slug(), and the adoption cost of a first-party replacement is under a day. That math should make the decision for you.
Extension point if you truly need to customize: the cocur/slugify RuleProviderInterface is where custom rule chains live (e.g., ->addRule('@', 'at'), custom character maps). Wire any custom rules through your SlugifyServiceProvider so every consumer sees consistent behavior — this is the one place mirroring the bundle's "centralized configuration" claim is genuinely worth doing.
How can I help you explore Laravel packages today?