Skip to content

Features

Multiple Source Fields

Generate slugs from multiple attributes by passing an array:

php
#[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:

php
#[Slugify(from: 'title', to: 'slug', maxLength: 15)]
InputSlug
"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:

php
#[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):

php
#[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:

php
// 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:

bash
php artisan vendor:publish --tag=slugify-migrations
php artisan migrate

Then add the HasSlugHistory trait:

php
use Oliwol\Slugify\HasSlug;
use Oliwol\Slugify\HasSlugHistory;

#[Slugify(from: 'title', to: 'slug')]
class Post extends Model
{
    use HasSlug, HasSlugHistory;
}

Lookup by old slug

php
// Checks current slug first, then history
$post = Post::findBySlugWithHistory('old-slug');

Implementing 301 redirects

php
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

php
$post->slugHistory;                    // Collection of SlugHistory entries
$post->slugHistory->pluck('slug');     // ["old-slug", "older-slug"]
$post->slugHistory->first()->created_at; // Carbon instance

Translatable Slugs

For multilingual applications, integrate with spatie/laravel-translatable:

bash
composer require spatie/laravel-translatable

Use HasTranslatableSlug instead of HasSlug:

php
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'];
}
php
$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'); // → Post

Uniqueness 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:

EventWhenProperties
SlugGeneratedSlug created for the first time$model, $slug
SlugUpdatedExisting slug changes$model, $oldSlug, $newSlug

Events are only dispatched when the slug actually changes.

php
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:

php
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:

bash
# 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-run

Records are processed in chunks of 200 with a progress bar, safe for large datasets.

Released under the MIT License.