Checking API
Reference for the checking surface: the Warrant facade, the two guard objects
(WarrantGuard, WarrantGuardForSchema), the ergonomic entry points, and the
HasWarrantSchema query-time helpers. Conceptual coverage is in
Checking access.
Every check runs on the authorization engine, bound to a user (and, at the
inner layer, a schema). There is no longer any static check method on the model
or the schema — reach the engine one of the ways below. $user is always
optional and defaults to auth()->user() (an InvalidArgumentException is thrown
if none is available).
Reaching the engine
Section titled “Reaching the engine”// 1. Facade — schema-less; the target names the schema.Warrant::can('update', $document);
// 2. User-bound guard (WarrantGuard).Warrant::guard($user)->can('update', $document);$user->warrant()->can('update', $document); // AuthorizesWithWarrant trait
// 3. Schema-bound guard (WarrantGuardForSchema).Warrant::forSchema($document, $user)->can('update', $document);DocumentSchema::guard($user)->can('update', $document); // static on the schemaAuthorizesWithWarrant (user trait)
Section titled “AuthorizesWithWarrant (user trait)”Add to your User model to reach that user’s engine directly.
use Warrant\AuthorizesWithWarrant;
class User extends Authenticatable{ use AuthorizesWithWarrant;}
public function warrant(): WarrantGuard; // === Warrant::guard($this)WarrantSchema::guard() (static)
Section titled “WarrantSchema::guard() (static)”Every schema inherits this static shortcut for its own schema-bound guard.
public static function guard(?Authenticatable $user = null): WarrantGuardForSchema;// === Warrant::forSchema(static::class, $user)The Warrant facade
Section titled “The Warrant facade”Schema-less: the target names the schema (and, optionally, the row).
Warrant::guard(?Authenticatable $user = null): WarrantGuard;Warrant::forSchema(Model|WarrantSchema|string $schema, ?Authenticatable $user = null): WarrantGuardForSchema;
Warrant::can(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): bool; // ALLWarrant::canAny(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): bool; // ANYWarrant::cannot(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): bool;Warrant::authorize(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): void; // throws 403 (ALL)Warrant::authorizeAny(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): void; // throws 403 (ANY)Warrant::abilities(Model|string|array $target, array $context = [], ?Authenticatable $user = null): array;
Warrant::flush(?Authenticatable $user = null): void; // drop memoized rules — one user, or all- There is no
matchMode:argument. ALL iscan; ANY iscanAny. Likewiseauthorize/authorizeAny. authorize()/authorizeAny()are the throwing siblings — they returnvoidand throwWarrantAuthorizationException(rendered as HTTP 403) on denial, carrying a diagnosed denial context (see Denial messages).context:is a separate argument, merged over the schema’sdefaultContext().flush()drops the per-request memo so the next check re-runs your resolver. Its$userdoes not default to the current user — omitting it flushes everyone. See Resolution lifetime.
Target forms
Section titled “Target forms”The target on the facade / WarrantGuard names the schema and, optionally, a row:
Warrant::can('update', $document); // Model instance — the rowWarrant::can('update', [Document::class, $id]); // [class, id] — a row by keyWarrant::can('create', Document::class); // model class — no-targetWarrant::can('create', DocumentSchema::class); // schema class — no-targetWarrant::can('create', 'documents'); // schema key — no-targetA bare scalar here names the schema, never a row — which is why the facade and
WarrantGuard take no bare int: an int could not identify a schema. Use the
[Document::class, $id] tuple to name a row schema-lessly (its id may be a
string or an int), or reach for the schema-bound guard, where a bare key is
unambiguous.
WarrantGuard (user-bound)
Section titled “WarrantGuard (user-bound)”Reached with Warrant::guard($user) or $user->warrant(). Same target forms as
the facade; the user is fixed.
public function forSchema(Model|WarrantSchema|string $schema): WarrantGuardForSchema;
public function can(string|array $abilities, Model|string|array $target, array $context = []): bool;public function canAny(string|array $abilities, Model|string|array $target, array $context = []): bool;public function cannot(string|array $abilities, Model|string|array $target, array $context = []): bool;public function authorize(string|array $abilities, Model|string|array $target, array $context = []): void;public function authorizeAny(string|array $abilities, Model|string|array $target, array $context = []): void;public function abilities(Model|string|array $target, array $context = []): array;Reachability methods (schema-first) are listed under Reachability.
WarrantGuardForSchema (schema + user-bound)
Section titled “WarrantGuardForSchema (schema + user-bound)”Reached with Warrant::forSchema($schemaOrModel, $user), DocumentSchema::guard($user),
or $user->warrant()->forSchema(...). The schema is fixed, so the target is
just the row (or null for a no-target check). A row is named by instance or
by key, and a key may be a string or an int:
$guard->can('update', $document); // Model instance$guard->can('update', 42); // int key$guard->can('update', 'doc-9'); // string key$guard->can('create'); // no target$guard->can('create', Document::class); // no target, named positionallypublic function can(string|array $abilities, Model|string|int|null $target = null, array $context = []): bool;public function canAny(string|array $abilities, Model|string|int|null $target = null, array $context = []): bool;public function cannot(string|array $abilities, Model|string|int|null $target = null, array $context = []): bool;public function authorize(string|array $abilities, Model|string|int|null $target = null, array $context = []): void;public function authorizeAny(string|array $abilities, Model|string|int|null $target = null, array $context = []): void;public function abilities(Model|string|int|null $target = null, array $context = []): array;
public function schema(): WarrantSchema;public function user(): Authenticatable;Lower-level query builders
Section titled “Lower-level query builders”The HasWarrantSchema scopes below delegate to these; call them directly when you
hold a raw query builder. These do take an AbilityMatchMode.
public function filterQuery( Builder $query, string|array $abilities, AbilityMatchMode $matchMode = AbilityMatchMode::ALL, array $context = [],): Builder;
public function selectAbilitiesInQuery( Builder $query, string $selectedAbilitiesKey = 'abilities', ?array $onlyAbilities = null, array $context = [],): Builder;
public function getAbilitiesWithoutTarget( string|array|null $abilities = null, // null enumerates every held ability AbilityMatchMode $matchMode = AbilityMatchMode::ANY, array $context = [],): array;getAbilitiesWithoutTarget() defaults to ANY — the one exception to the ALL
default. For the common case, prefer Warrant::abilities(Document::class) /
->abilities().
The compiled where clause, before it becomes SQL
Section titled “The compiled where clause, before it becomes SQL”filterQuery() is a consumer of one lower-level step: compiling the gate into a
where clause. That clause is often not a clause at all — an unconditional
cannot, an ability no rule grants, an unconditional can, or (with no target)
anything gated on a row condition all settle the gate without reference to a
row. compileGate() hands you that result:
public function compileGate( Builder $query, // the host the clause is built off string|array $abilities, AbilityMatchMode $matchMode = AbilityMatchMode::ALL, array $context = [], ?Model $targetModel = null, // the loaded row, when there is exactly one): CompilationResult;CompilationResult offers the compile in whichever form you need:
$gate = $guard->compileGate($query, 'view');
$gate->decision(); // a Decision: what the rules settled on, if anything$gate->toQuery(); // the predicate as SQL (a constant becomes 1 = 1 / 1 = 0 / null)$gate->spliceInto($query); // attach it to a host query, returning the hostdecision() returns a Warrant\DSL\Compiling\Decision:
| case | meaning |
|---|---|
True |
the rules granted without consulting a row |
False |
the rules denied |
Unknown |
the compile reached a question it could not answer — see How it compiles |
NeedsQuery |
not settled here; the predicate has to be asked in SQL |
grants() is true for True alone, so a caller wanting a plain yes/no need not
care which of False and Unknown it got — both deny. isConstant() is false
for NeedsQuery alone.
Read decision() first and you can skip the query entirely; call
spliceInto() on the same result when you do need the SQL, and nothing is
compiled twice.
filterQuery() always needs SQL, so it spells a constant out as 1 = 1 /
1 = 0 / null — a row filter has to say something. The boolean checks do not: can(),
canAny(), cannot() and the authorize*() pair read the literal and return
without querying at all.
A folded false always short-circuits — no row could pass. A folded true is
one step short of an answer on a targeted check, because exists() was also
confirming the row is there. Only a hydrated model has already established that
(Model::$exists, which Eloquent sets on retrieval or insert and clears on
delete), so it alone short-circuits. Everything whose existence is unproven —
a bare key, an unsaved model, a deleted one — still costs one lookup:
$guard->can('view', $documentFromQuery); // no query$guard->can('view', 42); // one query, for existence$guard->can('view', new Document); // one query, for existenceAn empty ability set folds to true — the match-all an empty gate has always
meant. Callers that treat “nothing requested” as a failure handle that
themselves.
HasWarrantSchema — model query helpers
Section titled “HasWarrantSchema — model query helpers”The model trait no longer carries any static check methods. What remains are the query-time conveniences that belong on the model.
use Warrant\HasWarrantSchema;
class Document extends Model{ use HasWarrantSchema;
public static function warrantSchema(): string { return \App\Warrant\DocumentSchema::class; }}The trait declares warrantSchema(): string abstract and static. The returned
schema’s const model must equal this model’s class, or Warrant throws
LogicException (Model [...] names schema [...], but that schema names model [...])
when a scope resolves the schema. Because warrantSchema() is inherited, a model
subclass fails this check unless it declares its own schema.
Query scopes
Section titled “Query scopes”These keep AbilityMatchMode (the query layer supports both modes directly).
// ->userHasAbility(...)public function scopeUserHasAbility( EloquentBuilder $query, string|array $abilities, ?Authenticatable $user = null, AbilityMatchMode $matchMode = AbilityMatchMode::ALL, array $context = [],): EloquentBuilder;
// ->selectUserAbilities(...)public function scopeSelectUserAbilities( EloquentBuilder $query, ?Authenticatable $user = null, string $selectedAbilitiesKey = 'abilities', ?array $onlyAbilities = null, array $context = [],): EloquentBuilder;Instance method
Section titled “Instance method”public function loadUserAbilities( ?Authenticatable $user = null, string $selectedAbilitiesKey = 'abilities', array $context = [],): array; // computes, then setAttribute() on the instance$userdefaults toauth()->user(); scopes throwLogicExceptionif no user is available.userHasAbility([])(empty ability list) leaves the query unchanged.selectUserAbilitiestargets rows viagetQualifiedKeyName(). Its JSON column is in ability declaration order and requires a supported DB driver.
The global scope
Section titled “The global scope”Warrant\SelectUserAbilitiesScope implements Illuminate\Database\Eloquent\Scope.
Its apply() no-ops when there’s no authenticated user or the model lacks a
warrantSchema() method; otherwise it calls selectUserAbilities($currentUser).
Laravel Gate
Section titled “Laravel Gate”When register_gate is true (the default), Warrant registers a Gate::before
hook so its abilities resolve through Laravel’s native surfaces:
$user->can('view', $document); // targeted row check$user->can('approve', [$document, ['region' => 'us']]); // targeted + context$user->can('create', Document::class); // no-target via model class$user->can('create', [Document::class, ['region' => 'us']]); // no-target + contextGate::authorize('view', $document); // throws Warrant's 403 + message@can, @cannot, and the can: route middleware go through the same hook.
ALL/ANY across several abilities is native Laravel — can([...]) / canAny([...]).
Abilities not declared by a registered schema return null from the hook and fall
through to your own policies. Guests (unauthenticated) always fall through. See
Checking access → Laravel’s Gate.
The Gate arg convention accepts the tuple [$model, ['ctx' => …]] for context;
the facade/guard helpers take context: as their own argument instead.
Match modes
Section titled “Match modes”Warrant\AbilityMatchMode::ALL; // every listed ability requiredWarrant\AbilityMatchMode::ANY; // any one is enoughAbilityMatchMode is used by the query scopes, the lower-level query/reachability
methods, and the middleware. The facade/guard check helpers express it through
the method name instead (can vs canAny, authorize vs authorizeAny).
Reachability
Section titled “Reachability”Reachability answers “could this user ever have this ability, given the shape of the rules?” — a purely structural analysis of the resolved rule set. It evaluates no conditions and runs no SQL. Conceptual coverage is in Reachability.
On the facade / WarrantGuard the schema comes first; there is no matchMode
(use the *Any variants for ANY) and no context: (conditions are never
evaluated), but a $user is still required — the resolver may return a different
rule set per user.
Warrant::reachabilityOf(Model|WarrantSchema|string $schema, string $ability, ?Authenticatable $user = null): Reachability;
Warrant::couldEverHave($schema, string|array $abilities, ?Authenticatable $user = null): bool; // all !== NEVERWarrant::couldEverHaveAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;Warrant::alwaysHas($schema, string|array $abilities, ?Authenticatable $user = null): bool; // all === ALWAYSWarrant::alwaysHasAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;Warrant::neverHas($schema, string|array $abilities, ?Authenticatable $user = null): bool; // all === NEVERWarrant::neverHasAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;
Warrant::possibleAbilities($schema, ?Authenticatable $user = null): array; // reachability !== NEVERWarrant::guaranteedAbilities($schema, ?Authenticatable $user = null): array; // === ALWAYSWarrant::impossibleAbilities($schema, ?Authenticatable $user = null): array; // === NEVERWarrantGuard carries the same methods (schema-first). Example:
Warrant::guard($user)->couldEverHave(Document::class, 'update').
Warrant\Reachability is a pure enum with cases NEVER, MAYBE, ALWAYS. See
Schema API for the per-ability decision table.
On WarrantGuardForSchema
Section titled “On WarrantGuardForSchema”The schema is already bound, so no schema argument:
public function reachabilityOf(string $ability): Reachability;public function couldEverHave(string|array $abilities): bool;public function couldEverHaveAny(string|array $abilities): bool;public function alwaysHas(string|array $abilities): bool;public function alwaysHasAny(string|array $abilities): bool;public function neverHas(string|array $abilities): bool;public function neverHasAny(string|array $abilities): bool;public function possibleAbilities(): array;public function guaranteedAbilities(): array;public function impossibleAbilities(): array;
// building blocks the above are expressed in terms of:public function reachabilityMap(?array $abilities = null): array;public function reachabilitySatisfies(string|array $abilities, callable $passes, AbilityMatchMode $matchMode): bool;public function abilitiesWhereReachability(callable $passes): array;