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,
stored: true,
identity: null,
identityOf: null,
identitySecond: null,
)]
OptionTypeDefaultEffect
sourcestring|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.
separatorstring'-'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.
transliterateTransliterationProfileSimpleSimple (ü→u) or Din (ü→ue). Applies in ascii mode.
maxLength?intnullTrim 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.
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. A cast column counts by its value, as in source. A record whose scope column changes takes its current slug into the new scope.
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}); on an idLess model, whose URL has no id, the encoded id becomes the slug. 'throw' raises CouldNotGenerateSlug.
encoderOptionsarray<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.
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.
storedbooltruefalse 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?stringnullA 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?stringnullThe 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?stringnullThe 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.
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.

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.