NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Sluggable for Laravel

Slug definitions

Override slugOptions() to declare one or more slug columns. Each SlugDefinition is a fluent builder; any option you leave unset falls back to the config defaults:

use RoundlyConsulting\Sluggable\Definitions\SlugDefinition;
use RoundlyConsulting\Sluggable\Definitions\SlugOptions;
use RoundlyConsulting\Sluggable\Enums\UpdatePolicy;

public function slugOptions(): SlugOptions
{
    return SlugOptions::make(
        SlugDefinition::for('slug')
            ->from('name')
            ->uniqueWithin('clinic_id')
            ->onUpdate(UpdatePolicy::WhenSourceChanges)
            ->keepHistory()
            ->routeKey(),
        SlugDefinition::for('handle')
            ->from(['brand.name', 'name'])       // relation dot path + attribute
            ->separator('_')
            ->maxLength(40)
            ->immutable(),
        SlugDefinition::for('code')
            ->from(fn (Product $product, ?string $locale): string => $product->sku)
            ->lowercase(false)
            ->notUnique(),
    );
}

Definitions run in declaration order, so a later column may use an earlier slug as its source. At most one definition may be the routeKey(); duplicate columns or several route keys throw InvalidSlugDefinitionException.

Sources

from() accepts an attribute name, a list of attributes, dot paths through relations (loaded with loadMissing(), so preventLazyLoading() stays happy) or a closure fn (Model $m, ?string $locale): ?string. Scalar attributes are read through getAttribute(), so accessors and casts apply and enums use their value. A missing relation contributes nothing.

Every option

MethodMeaning
for(string $column)The slug column.
from(string|list<string>|Closure)Attribute(s), dot paths through relations (loaded with loadMissing), or fn (Model $m, ?string $locale): ?string.
sourceLocale(string)Which locale of a locale-map source feeds a string slug (default: fallback → current → first).
separator(string), maxLength(int), maxWords(?int)Formatting.
language(string|Closure|null), dictionary(array), unicode(), lowercase(bool)Transliteration and characters.
using(Closure $slugger)Replace the formatting steps; the output is still validated as a safe URL segment.
reserved(list<string>)Extra reserved words.
prefix(string|Closure), suffix(string|Closure)Affixes, never cut by truncation.
unique(), uniqueWithin(string ...$columns), uniqueWhere(Closure), notUnique()Uniqueness.
perLocaleUniqueness(), uniqueAcrossLocales()Locale maps only.
includeTrashed(bool), excludeTrashed()Whether soft-deleted rows count as taken.
sequentialSuffix(int $start = 2), randomSuffix(?int $length), suffixUsing(SuffixGenerator|Closure)Collision strategy.
randomLength(int)Random slug / suffix length (4–32).
onCreate(bool), onUpdate(UpdatePolicy), immutable(), regenerateOnUpdate()Lifecycle.
manual(ManualSlugPolicy)How host-supplied values are treated.
whenEmptySource(EmptySourcePolicy)Empty source → random / skip / fail.
lockWhen(Closure), locked()Freeze a slug (e.g. once published).
skipWhen(Closure)Skip the column for a save.
localized(bool), storage(SlugStorage)Force the storage (default: auto-detected).
locales(TargetLocales|list<string>|Closure)Which locales get generated.
fallback(LocaleFallback), fallbackLocale(string|Closure|null)The reading/binding chain and its fallback locale.
routeKey(bool)This column becomes getRouteKeyName() (max. one).
bindByKeyFallback(bool)Try the primary key after a slug miss.
keepHistory(bool), redirectFromHistory(bool), avoidHistoricalSlugs(bool)Slug history.
retries(int)Race retries (0 disables).

Closure setters are generic, so typed closures such as ->lockWhen(fn (Post $post): bool => $post->is_published) pass Larastan level 7 in your code.

The attribute form

For the closure-free subset, declare the columns as repeatable #[Slug] class attributes:

use RoundlyConsulting\Sluggable\Attributes\Slug;

#[Slug('slug', from: 'name', uniqueWithin: ['kind_id'], routeKey: true)]
#[Slug('short', from: ['kind.name', 'name'], maxLength: 40, onUpdate: UpdatePolicy::Never)]
final class Breed extends Model implements Sluggable
{
    use HasSlug;
}

The attribute accepts column, from, separator, maxLength, maxWords, language, unicode, lowercase, uniqueness, uniqueWithin, includeTrashed, localeUniqueness, onUpdate, manual, emptySource, suffix, suffixStart, randomLength, storage, locales, fallback, routeKey, bindByKeyFallback, keepHistory, redirectFromHistory and reserved. SuffixStrategy::Custom cannot be expressed as an attribute.

Precedence

  • An overridden slugOptions() on the model — always wins.
  • #[Slug] class attributes — inherited from a parent class when the child declares none.
  • SlugOptions::defaults() — one definition: defaults.column generated from defaults.source.

Declaring attributes and overriding slugOptions() at the same time throws conflictingSources in the local and testing environments.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.