Features
Multiple Source Fields
Generate slugs from multiple attributes by passing an array:
#[Slugify(from: ['first_name', 'last_name'], to: 'slug')]
class Author extends Model
{
use HasSlug;
}Null or empty values are skipped:
first_name: "John",last_name: null→"john"first_name: "",last_name: "Doe"→"doe"
Max Length
Truncate slugs at word boundaries to fit a maximum length:
#[Slugify(from: 'title', to: 'slug', maxLength: 15)]| Input | Slug |
|---|---|
"Hello World Foo" | "hello-world-foo" (15 chars, fits) |
"Hello World Foo Bar" | "hello-world-foo" (truncated at word boundary) |
Uniqueness suffixes are also accounted for within the max length.
Custom Separator
Use a different character to separate words:
#[Slugify(from: 'title', to: 'slug', separator: '_')]"Hello World" → "hello_world"
The separator is also used for uniqueness suffixes: "hello_world_2".
Regeneration Control
Prevent slugs from changing on update (useful for SEO):
#[Slugify(from: 'title', to: 'slug', regenerateOnUpdate: false)]- Slug is generated on creation
- Source attribute changes on update are ignored
- Manual slug changes are still respected
Finding Models by Slug
The trait provides two static lookup methods:
// Returns the model or null
$post = Post::findBySlug('hello-world');
// Returns the model or throws ModelNotFoundException
$post = Post::findBySlugOrFail('hello-world');Both methods respect the configured slug column and apply scopeSlugQuery() for scoped lookups.
Slug History
Track previous slugs for SEO-friendly 301 redirects. First, publish and run the migration:
php artisan vendor:publish --tag=slugify-migrations
php artisan migrateThen add the HasSlugHistory trait:
use Oliwol\Slugify\HasSlug;
use Oliwol\Slugify\HasSlugHistory;
#[Slugify(from: 'title', to: 'slug')]
class Post extends Model
{
use HasSlug, HasSlugHistory;
}Lookup by old slug
// Checks current slug first, then history
$post = Post::findBySlugWithHistory('old-slug');Implementing 301 redirects
public function show(string $slug)
{
$post = Post::findBySlugWithHistory($slug);
if (! $post) {
abort(404);
}
if ($post->slug !== $slug) {
return redirect()->route('posts.show', $post->slug, 301);
}
return view('posts.show', compact('post'));
}Accessing history
$post->slugHistory; // Collection of SlugHistory entries
$post->slugHistory->pluck('slug'); // ["old-slug", "older-slug"]
$post->slugHistory->first()->created_at; // Carbon instanceTranslatable Slugs
For multilingual applications, integrate with spatie/laravel-translatable:
composer require spatie/laravel-translatableUse HasTranslatableSlug instead of HasSlug:
use Oliwol\Slugify\HasTranslatableSlug;
use Oliwol\Slugify\Slugify;
use Spatie\Translatable\HasTranslations;
#[Slugify(from: 'title', to: 'slug')]
class Post extends Model
{
use HasTranslations, HasTranslatableSlug;
public array $translatable = ['title', 'slug'];
}$post = Post::create([
'title' => ['en' => 'Hello World', 'de' => 'Hallo Welt'],
]);
$post->getTranslation('slug', 'en'); // → 'hello-world'
$post->getTranslation('slug', 'de'); // → 'hallo-welt'
// Find by locale
Post::findBySlug('hallo-welt', 'de'); // → PostUniqueness is checked per locale. See the API Reference for details.
Limitation
HasTranslatableSlug only supports a single attribute name or method name as source. Array sources are not supported — use a method source to combine multiple translatable values.
Events
The package dispatches events during the slug lifecycle:
| Event | When | Properties |
|---|---|---|
SlugGenerated | Slug created for the first time | $model, $slug |
SlugUpdated | Existing slug changes | $model, $oldSlug, $newSlug |
Events are only dispatched when the slug actually changes.
use Oliwol\Slugify\Events\SlugGenerated;
use Oliwol\Slugify\Events\SlugUpdated;
Event::listen(SlugGenerated::class, function (SlugGenerated $event) {
Log::info("Slug created: {$event->slug}");
});
Event::listen(SlugUpdated::class, function (SlugUpdated $event) {
Redirect::create([
'from' => $event->oldSlug,
'to' => $event->newSlug,
]);
});Custom Scoping
Scope slug uniqueness per tenant, team, or any custom criteria:
public function scopeSlugQuery($query)
{
return $query->where('tenant_id', $this->tenant_id);
}This appends a WHERE tenant_id = ? clause when checking for existing slugs and when using findBySlug().
Artisan Command
Generate or regenerate slugs for existing database records:
# Generate slugs for records that don't have one
php artisan slugify:generate "App\Models\Post"
# Regenerate all slugs (overwrite existing)
php artisan slugify:generate "App\Models\Post" --force
# Preview changes without saving
php artisan slugify:generate "App\Models\Post" --dry-runRecords are processed in chunks of 200 with a progress bar, safe for large datasets.