---
title: "Slug definitions — Sluggable for Laravel | Roundly"
description: "Declare any number of slug columns with slugOptions() or #[Slug] attributes — sources, formatting, uniqueness, lifecycle and routing."
url: https://roundly-consulting.com/open-source/docs/sluggable-for-laravel/slug-definitions
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Sluggable for Laravel](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)

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

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

| Method | Meaning |
| --- | --- |
| `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 = null), randomSuffix(?int $length), suffixUsing(SuffixGenerator\|Closure)` | Collision strategy; sequentialSuffix() starts at defaults.suffix\_start unless a start is given. |
| `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:

```php
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. Without suffixStart, a sequential suffix starts at defaults.suffix\_start.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/sluggable-for-laravel.md)
