Diagnostics
Verify the setup — the encoder configuration, the token settings, how much room the token spaces have left, and the uniqueness-guaranteeing indexes — at any time:
php artisan polyslug:doctor
It exits non-zero when a check fails, so it works as a deployment smoke test.
The checks
Encoders. The configured polyslug.encoder and every entry in
polyslug.legacy_decoders must be an existing class implementing
Polyslug\Contracts\IdentityEncoder. A typo in a class name, or a legacy decoder left
behind after a package was removed, is reported per class:
✗ [App\Encoders\Typo] is not a valid IdentityEncoder.
Uniqueness indexes. Both polyslug_slugs_current_unique and
polyslug_slugs_one_current must exist on polyslug_slugs. A missing index almost always
means the migrations never ran on this database:
✗ unique index [polyslug_slugs_one_current] is missing — run the migrations.
This one is worth taking seriously. Both guarantees are enforced by the database, not by application code, so a missing index does not fail loudly — it just quietly stops preventing duplicate or double-current slugs until the day two writes race. See the database reference.
Token schemes. The lengths and alphabets under polyslug.random_token,
polyslug.sequential_token and polyslug.short_links must be able to build a scheme — a
length of zero, a length past what counting can reach, a repeated character, a / in an
alphabet:
✗ A random token length must be between 1 and 255; got [0].
Worth running after any change to those settings. Without this check the refusal arrives the first time a URL is rendered — on a page, in production, for a setting that shipped with a green test suite, because nothing in a test suite renders a URL for a record that does not exist yet.
Token space. How full each token width is, reported per width because a table that has outlived a setting change legitimately holds several:
! identity tokens: 400 of 1,296 2-character tokens are taken (31%).
New tokens widen to 3 characters as this fills.
This is a report, not a failure, and quiet below 25%. A width filling up is not a fault — a scheme that runs out yields to one character more — but the URLs quietly get longer, and if you picked four characters for a printed code you want to hear about it before the printed codes stop matching the new ones. On a counted scheme a high number is simply what is supposed to happen, since counting fills a width completely before moving on.
Resolution gates. Every type in polyslug.types that never overrode
polyslugResolveQuery() is named:
! [App\Models\Consultation] does not override polyslugResolveQuery().
Any slug of those types resolves to any row.
This is a report, not a failure, and the command still exits zero. A model that
resolves globally is a legitimate choice for public content; the same model behind an
owner check is an authorization gap, because a stale-slug request for someone else's row
is answered with a 301 whose Location header carries that row's slug. The command
cannot tell the two apart — it makes the choice visible so it stops being accidental. If
global resolution is what you want, override the gate with return $query; and the line
goes away, now as a recorded decision.
See Access control.
When to run it
- After deploying to a new environment, before the first traffic hits it.
- After changing the encoder, a token length or an alphabet, or editing
legacy_decoders. - Periodically on a short token setting, so a width that is filling up is visible before it fills.
- After adding a sluggable model, to see whether it needs a resolution gate.
- After a manual schema change on
polyslug_slugs— an index dropped during an unrelated migration is exactly what this catches.
What it does not check
polyslug:doctor validates configuration and schema, not data. It will not tell you that
a model is missing its #[Polyslug] attribute (that throws
MissingPolyslugConfig on first use)
or that a PolyslugUrlResolver is unbound (the
sitemap command reports that itself).