Attribute options
use Polyslug\Attributes\Polyslug;
use Polyslug\Enums\TransliterationProfile;
#[Polyslug(
source: 'title',
separator: '-',
transliterate: TransliterationProfile::Simple,
maxLength: null,
unique: true,
scope: null,
reserved: [],
immutable: false,
encoder: null,
onDelete: 'keep',
emptyFallback: 'id-only',
encoderOptions: [],
unicode: 'ascii',
idLess: false,
slugless: false,
reclaim: false,
reclaimActive: false,
stored: true,
identity: null,
identityOf: null,
identitySecond: null,
)]
| Option | Type | Default | Effect |
|---|---|---|---|
source | string|list<string> | (required) | Column(s) the slug is built from. An array joins its values with a space. A cast column counts by its value: an enum by its value, a date by the string the model stores, a Stringable by its text. |
separator | string | '-' | Word separator within the slug, and the separator used by the uniqueness suffix. May use -, . and ~: _ separates the slug from the token and / the slugs of a nested URL, so both are refused. |
transliterate | TransliterationProfile | Simple | Simple (ü→u) or Din (ü→ue). Applies in ascii mode. |
maxLength | ?int | null | Trim the slug to at most this many characters, never leaving a trailing separator; a uniqueness suffix such as -2 comes on top. Without it the slug is still cut to fit its column, as long as Schema::defaultStringLength() (255 by default), with room for a -2 suffix. It does not touch the _token after it — that length is the encoder's. Rejected together with slugless, which has no slug to trim. |
unique | bool | true | true appends -2, -3, … on a collision. false lets records share a slug and skips the collision step entirely, including the reserved list. |
scope | string|list<string>|null | null | Column(s) that scope uniqueness, e.g. tenant_id or parent_id. A cast column counts by its value, as in source. A record whose scope column changes takes its current slug into the new scope. |
reserved | list<string> | [] | Slugs that may never be assigned, matched case-insensitively. Merged with polyslug.reserved.global. |
immutable | bool | false | Freeze the slug after first generation; later source edits do not move it. |
encoder | ?string | null | A fully-qualified IdentityEncoder class overriding the global encoder for this model. |
onDelete | string | 'keep' | 'keep' reserves slugs on soft-delete; 'release' frees them for reuse. A hard or force delete always cascades the slug rows. |
emptyFallback | string | 'id-only' | When slugification yields an empty string: 'id-only' stores it (URL is just _{id}); on an idLess model, whose URL has no id, the encoded id becomes the slug. 'throw' raises CouldNotGenerateSlug. |
encoderOptions | array<string, mixed> | [] | Per-model encoder settings. SqidsEncoder: alphabet, min_length; the one left out takes the Sqids default, not polyslug.sqids. RandomTokenEncoder / SequentialTokenEncoder: length, alphabet; the one left out keeps the application's setting. A key the effective encoder does not understand is ignored. |
unicode | string | 'ascii' | 'ascii' transliterates; 'native' keeps Unicode letters and numbers, lower-cased at generation. |
idLess | bool | false | Drop the _{encodedId} suffix; the URL is the slug alone and resolution is by slug. |
slugless | bool | false | Drop the slug: the URL is the encoder token alone (/lists/k3f9dlq7). The mirror image of idLess, and rejected together with it. Takes no source, maxLength or reserved. See Token-only URLs. |
reclaim | bool | false | Only with idLess. false reserves a retired slug forever; true releases it so another record may claim the name. See Reclaim. |
preserveCase | bool | false | Store the slug in the writing it was given (Octo-Org) instead of folding it to lower case. Resolution and uniqueness are unaffected — both already compare case-insensitively — so this changes what is displayed and nothing else. Refused together with unicode: 'native'. See Preserving the writing. |
stored | bool | true | false computes the slug from the source on every call and writes nothing to polyslug_slugs, so there is no history: an old URL resolves by its token, and the canonical middleware 301s it to the current slug. Requires unique: false, except on a slugless model, which then writes no row at all. Rejected together with idLess and immutable. See Addresses for a read model. |
identity | ?string | null | A column whose value is the token of the URL, in place of the encoded primary key; the URL resolves by that column. The value takes letters, digits and hyphens. Rejected together with encoder, encoderOptions and idLess. See Addresses for a read model. |
identityOf | ?string | null | The column the token in identity was taken from, such as the id of another system whose end TailTokens made the token. A URL whose token the identity column does not hold then resolves by that id as well, and the canonical middleware moves it to the token of today. Requires identity. See An address the column no longer holds. |
identitySecond | ?string | null | The column of a second id, which tells apart two records that carry the same id in identityOf. A URL with the second id after the whole id resolves to the record both name. Requires identityOf. |
reclaimActive | bool | false | Only with reclaim. Extends it from retired names to a name another record still holds: the holder's row is retired and the newcomer takes the name. The displaced record is then left with no current slug — see When the events arrive out of order. |
Mutually exclusive combinations
idLess: true with unique: false throws Polyslug\Exceptions\MisconfiguredPolyslug at
configuration time — an id-less URL has no id to disambiguate records that share a slug.
onDelete, emptyFallback and unicode take only the values in the table. Any other value,
a misspelling or another case, throws the same exception rather than falling back to the
default without a word.
preserveCase: true with unicode: 'native' throws the same exception. Uniqueness is enforced
by an index that folds with the database's lower(), and those disagree: PostgreSQL folds
non-ASCII letters, SQLite does not. A folded slug hides that, because every stored value is
already lower case. An unfolded native slug would not — Ärger would collide with ärger on
one engine and not on the other, so the guarantee would depend on where the application runs.
unicode: 'ascii' transliterates before storing, so a preserved-case slug is ASCII and every
engine folds it identically.
reclaim: true without idLess: true throws the same exception. On a model whose URL
carries an encoded id, a retired slug is already free to reuse, so the flag would change
nothing — and a flag that silently does nothing is worse than one that is refused: whoever
set it believes a guarantee has been relaxed.
reclaimActive: true without reclaim: true throws the same exception, for the same reason
one step further out. reclaimActive widens reclaim from retired names to actively held
ones; on its own it would take the name from a live holder and then be refused by that
holder's own retired rows — the least explicable of the three half-behaviors.
Computing the options at runtime
A model that implements Polyslug\Contracts\ConfiguresPolyslug returns a PolyslugConfig
from polyslug(), which takes precedence over the attribute and is resolved fresh on
every use. PolyslugConfig::fromAttribute() builds one from an attribute instance, so the
options above are the same in both paths. See
Dynamic configuration.
A model that uses HasPolyslug with neither the attribute nor a polyslug() override
throws Polyslug\Exceptions\MissingPolyslugConfig.