oliwol/laravel-slugify

A trait to easily add slug generation to your Laravel models.

Maintainers

Package info

github.com/oliwol/laravel-slugify

pkg:composer/oliwol/laravel-slugify

Statistics

Installs: 630

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 7

v1.2.0 2026-03-23 07:53 UTC

README

Latest Version on Packagist GitHub Tests Action Status License

A tiny trait that gives your Eloquent models clean, automatic slugs β€” without setup, ceremony, or extra weight.

Attach it to a model, define the source attribute, and the trait quietly handles generation, updates and uniqueness.

πŸš€ Installation

Install the package via Composer:

composer require oliwol/laravel-slugify

⚑️ Quick Start

Using the PHP Attribute (recommended)

use Oliwol\Slugify\HasSlug;
use Oliwol\Slugify\Slugify;

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

Using method overrides

use Oliwol\Slugify\HasSlug;

class Post extends Model
{
    use HasSlug;

    public function getAttributeToCreateSlugFrom(): string|array
    {
        return 'title';
    }

    public function getRouteKeyName(): string
    {
        return 'slug';
    }
}

Priority: Method overrides always take precedence over the #[Slugify] attribute.

πŸ› οΈ Usage

Add the HasSlug trait to any Eloquent model where a slug should be automatically generated.

Configuration via #[Slugify] Attribute

The #[Slugify] attribute accepts the following parameters:

  • from (required) β€” the attribute(s) used to generate the slug. Accepts a single string (e.g. 'name') or an array of strings (e.g. ['first_name', 'last_name']).
  • to (optional) β€” the column to save the slug to. Falls back to getRouteKeyName() if omitted.
  • separator (optional) β€” the character used to separate words in the slug. Defaults to '-'.
  • maxLength (optional) β€” maximum number of characters for the slug. Truncates at word boundaries. Defaults to null (no limit).
  • regenerateOnUpdate (optional) β€” whether to regenerate the slug when the source attribute changes on update. Defaults to true. Set to false to only generate slugs on creation (useful for SEO).
use Oliwol\Slugify\HasSlug;
use Oliwol\Slugify\Slugify;

// Full configuration via attribute
#[Slugify(from: 'name', to: 'slug')]
class Post extends Model
{
    use HasSlug;
}

// Only 'from' β€” slug column is determined by getRouteKeyName()
#[Slugify(from: 'name')]
class Post extends Model
{
    use HasSlug;

    public function getRouteKeyName(): string
    {
        return 'slug';
    }
}

// Multiple source attributes β€” generates slug from combined values
#[Slugify(from: ['first_name', 'last_name'], to: 'slug')]
class Author extends Model
{
    use HasSlug;
}
// first_name: "John", last_name: "Doe" β†’ "john-doe"

// Custom separator β€” uses underscores instead of hyphens
#[Slugify(from: 'title', to: 'slug', separator: '_')]
class Post extends Model
{
    use HasSlug;
}
// "Hello World" β†’ "hello_world"

// SEO-safe β€” slug is only generated on creation, never updated
#[Slugify(from: 'title', to: 'slug', regenerateOnUpdate: false)]
class Post extends Model
{
    use HasSlug;
}

Note: The to parameter only controls where the slug is saved. For route model binding, you still need to override getRouteKeyName() separately on your model.

Configuration via methods

Alternatively, you can configure slug generation by overriding methods:

  • getAttributeToCreateSlugFrom() β€” the attribute(s) used to generate the slug. Return a string or array<string>.
  • getRouteKeyName() β€” the slug column for route model binding (e.g. slug).
  • Optionally getAttributeToSaveSlugTo() β€” a different column to save the slug.
  • Optionally getSlugSeparator() β€” the separator character (default '-').
  • Optionally getMaxSlugLength() β€” maximum slug length, truncated at word boundaries (default null).
  • Optionally shouldRegenerateSlugOnUpdate() β€” return false to only generate slugs on creation (default true).
  • Optionally override scopeSlugQuery() β€” scoping for uniqueness (e.g. per team).
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Oliwol\Slugify\HasSlug;

class Post extends Model
{
    use HasSlug;

    /**
     * Attribute(s) used for generating the slug.
     * Return a string or an array of strings.
     */
    public function getAttributeToCreateSlugFrom(): string|array
    {
        return 'name';
    }

    /**
     * Use slug for route binding.
     */
    public function getRouteKeyName(): string
    {
        return 'slug';
    }

    /**
     * This package uses Laravel's getRouteKeyName to store the slug.
     * If you are using a different column for your routes,
     * use getAttributeToSaveSlugTo to store the slug.
     */
    public function getAttributeToSaveSlugTo(): string
    {
        return 'slug';
    }

    /**
     * Scope applied when checking for uniqueness.
     */
    public function scopeSlugQuery($query)
    {
        return $query->where('tenant_id', 1);
    }
}

Make sure your table contains the slug column:

$table->string('slug')->unique();

If you use scoping, you probably don’t want a global unique index. Example: slugs must be unique per tenant:

$table->unique(['tenant_id', 'slug']);

βš™οΈ How it works

The HasSlug trait hooks into the Eloquent saving event:

protected static function bootHasSlug(): void
 {
     static::saving(function (Model $model): void {
         if ($model->isSluggable()) {
             $model->createSlug();
         }
     });
 }

When triggered, it will:

  1. Resolve the source attribute(s) β€” from the #[Slugify] attribute or a getAttributeToCreateSlugFrom() override. Supports a single attribute or multiple attributes.
  2. Generate a slug by combining filled source values (null/empty values are skipped).
  3. Skip regeneration if:
    1. None of the source attributes are dirty (unchanged), or
    2. The slug has been manually set and differs from the original.
  4. Ensure uniqueness by incrementing existing slugs (my-post, my-post-2, my-post-3, …).

πŸ”Ž Finding Models by Slug

The trait provides two static methods to look up models by their slug:

// 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 (to / getAttributeToSaveSlugTo()) and apply scopeSlugQuery() for scoped lookups.

βœ… Best practices & caveats

  • Ensure the route key column (getRouteKeyName()) is present in your table and is not the primary key (unless intentionally designed).
  • If you manually set a slug, the trait will not override it. Use this to allow user-edited slugs.

πŸ” Custom Scoping Example

To ensure slugs are unique per tenant, override the scopeSlugQuery() method:

public function scopeSlugQuery($query)
{
    return $query->where('tenant_id', 1);
}

This will append a WHERE tenant_id = ? clause when checking for existing slugs.

πŸ“„ License

This package is open-sourced software licensed under the MIT license.