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,
)]
| Option | Type | Default | Effect |
|---|---|---|---|
source | string|list<string> | (required) | Column(s) the slug is built from. An array joins its values with a space. |
separator | string | '-' | Word separator within the slug, and the separator used by the uniqueness suffix. |
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. 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. |
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}), 'throw' raises CouldNotGenerateSlug. |
encoderOptions | array<string, mixed> | [] | Per-model encoder settings. SqidsEncoder: alphabet, min_length. RandomTokenEncoder / SequentialTokenEncoder: length, alphabet. 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. |
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.
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.