Skip to main content

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,
)]
OptionTypeDefaultEffect
sourcestring|list<string>(required)Column(s) the slug is built from. An array joins its values with a space.
separatorstring'-'Word separator within the slug, and the separator used by the uniqueness suffix.
transliterateTransliterationProfileSimpleSimple (ü→u) or Din (ü→ue). Applies in ascii mode.
maxLength?intnullTrim 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.
uniquebooltruetrue appends -2, -3, … on a collision. false lets records share a slug and skips the collision step entirely, including the reserved list.
scopestring|list<string>|nullnullColumn(s) that scope uniqueness, e.g. tenant_id or parent_id.
reservedlist<string>[]Slugs that may never be assigned, matched case-insensitively. Merged with polyslug.reserved.global.
immutableboolfalseFreeze the slug after first generation; later source edits do not move it.
encoder?stringnullA fully-qualified IdentityEncoder class overriding the global encoder for this model.
onDeletestring'keep''keep' reserves slugs on soft-delete; 'release' frees them for reuse. A hard or force delete always cascades the slug rows.
emptyFallbackstring'id-only'When slugification yields an empty string: 'id-only' stores it (URL is just _{id}), 'throw' raises CouldNotGenerateSlug.
encoderOptionsarray<string, mixed>[]Per-model encoder settings. SqidsEncoder: alphabet, min_length. RandomTokenEncoder / SequentialTokenEncoder: length, alphabet. A key the effective encoder does not understand is ignored.
unicodestring'ascii''ascii' transliterates; 'native' keeps Unicode letters and numbers, lower-cased at generation.
idLessboolfalseDrop the _{encodedId} suffix; the URL is the slug alone and resolution is by slug.
sluglessboolfalseDrop 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.
reclaimboolfalseOnly with idLess. false reserves a retired slug forever; true releases it so another record may claim the name. See Reclaim.
preserveCaseboolfalseStore 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.
reclaimActiveboolfalseOnly 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.