Skip to main content

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).