Access control
Access control is your application's job. By default binding resolves any row — Polyslug does not own your tenant or publish state.
polyslugResolveQuery — the resolution gate
Override polyslugResolveQuery() to inject those scopes. A model outside the scope then
resolves to a 404 that is indistinguishable from a nonexistent one — no existence
oracle — and the gate is enforced uniformly across bound routes, the polymorphic
resolver, and /go short links:
use Illuminate\Database\Eloquent\Builder;
public function polyslugResolveQuery(Builder $query): Builder
{
return $query->where('tenant_id', currentTenant()->id)->where('published', true);
}
The default implementation is a no-op, so this is opt-in — but for a multi-tenant or
draft-carrying model it is the required isolation contract, not an optimization. Every
resolution path funnels through it, including polyslugResolveByKey(), which is what
makes the /go short-link route gated too.
polyslugIsRoutable — the output filter
polyslugIsRoutable(?string $locale = null): bool is the companion for output. Return
false to keep an unpublished model — or one specific locale — out of your hreflang sets
and sitemaps:
public function polyslugIsRoutable(?string $locale = null): bool
{
return $this->status === 'published';
}
The two are complementary and you usually want both: polyslugResolveQuery() decides
what a visitor may reach, polyslugIsRoutable() decides what you advertise. A
draft filtered only from the sitemap is still reachable by anyone who guesses the URL; a
draft blocked only at resolution still leaks its existence through your sitemap.
Do not rely on token opacity
An identity encoder chooses how much the URL reveals, but no encoder is an authorization check. A leaked or shared URL still carries a valid token, so the gate above is what decides whether it resolves.
See also the multi-tenant SaaS and marketplace recipes, which wire both hooks end to end.