Exceptions
All five live in Polyslug\Exceptions and extend RuntimeException.
MissingPolyslugConfig
Model [App\Models\Page] uses the HasPolyslug trait but has no #[Polyslug] attribute.
The model uses the trait but declares no configuration. Add a #[Polyslug(source: …)]
attribute, or implement
ConfiguresPolyslug and return a config from
polyslug().
MisconfiguredPolyslug
Thrown when a configuration combines options that cannot both hold:
idLess: truewithunique: false— an id-less URL is the slug alone, so the slug must stay unique to resolve to one model. Drop one of the two.reclaim: truewithoutidLess: true— on a model whose URL carries an encoded id a retired slug is already free to reuse, so the flag would do nothing at all.slugless: truewithidLess: true— the two drop opposite halves of the URL, so together there is nothing left to route on.slugless: truewithmaxLength,reservedorsource— each of them acts on a slug, and a slugless model has none.maxLengthis the one worth naming: it trims the slug and never the token, so on a token-only URL it would do nothing at all. The length you want is the encoder's.preserveCase: truewithunicode: 'native'— uniqueness folds with the database's ownlower(), and PostgreSQL folds non-ASCII letters where SQLite does not. An unfolded native slug would therefore collide on one engine and not on another.unicode: 'ascii'keeps every stored slug ASCII, so every engine folds it the same way.sourceomitted on any model that is notslugless— it had to become optional for a slugless model to declare none, and silently producing an empty slug for every record is worse than refusing.
See Slug-only URLs and Token-only URLs.
It is raised at configuration time, not at write time, so a wrong combination fails as soon as the model's config is built rather than on the first save.
CouldNotGenerateSlug
Could not generate a non-empty slug from source [🎉].
The source produced no sluggable characters and emptyFallback is 'throw'. Either
switch to the default 'id-only' — which stores an empty slug so the URL is just
_{encodedId} and the save succeeds — or set unicode: 'native' if the source is
non-Latin text that should survive into the slug. See
Transliteration and Unicode.
CouldNotWriteSlug
Could not write a slug for [App\Models\Page] from source [Hello] after repeated write conflicts.
A concurrent writer kept claiming the generated slug — or the one-current row — up to
polyslug.write.max_attempts. The model keeps whatever slug it had before; it is never left
slug-less or with two current slugs.
That outcome is reached by restoring the previous row, not by rolling anything back. The
write path runs in a transaction that always commits: it demotes the old row, inserts the
new one with insertOrIgnore, and on a lost race restores the row it demoted — inside that
same committed transaction. Rolling back would mean nesting a savepoint, which is exactly
what this design avoids, because a savepoint is unreliable once DDL has implicitly committed
an outer transaction.
Seeing this regularly means genuine write contention on the same
(type, id, locale, scope). Raise max_attempts, or look at why the same record is being
saved concurrently.
There is no database exception to inspect: a lost race is a return value here, not a
throw — insertOrIgnore reports zero affected rows instead of raising a unique-constraint
violation, which is what makes the retry portable across engines. So getPrevious() is
null, and there is nothing further underneath to read.
CouldNotIssueToken
Could not issue a random token for key [42] after 5 attempts.
Thrown by RandomTokenEncoder when
it lost every attempt to claim a token for a key.
Losing a single attempt is normal and invisible: two requests rendering the same
never-before-encoded model race, the loser adopts the winner's token, and both emit the
same canonical URL. This exception means losing five in a row — either the
polyslug_tokens table is under contention far beyond what URL rendering produces, or
something else is writing to it.
It is deliberately loud. The alternative would be returning a token that was never persisted — a URL that resolves to nothing, discovered much later by whoever clicked it.