Skip to content
Laravel Warrant is in beta and still being tested — expect API changes between releases. Report an issue.

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

// 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 schema

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)

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)

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; // ALL
Warrant::canAny(string|array $abilities, Model|string|array $target, array $context = [], ?Authenticatable $user = null): bool; // ANY
Warrant::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 is can; ANY is canAny. Likewise authorize / authorizeAny.
  • authorize() / authorizeAny() are the throwing siblings — they return void and throw WarrantAuthorizationException (rendered as HTTP 403) on denial, carrying a diagnosed denial context (see Denial messages).
  • context: is a separate argument, merged over the schema’s defaultContext().
  • flush() drops the per-request memo so the next check re-runs your resolver. Its $user does not default to the current user — omitting it flushes everyone. See Resolution lifetime.

The target on the facade / WarrantGuard names the schema and, optionally, a row:

Warrant::can('update', $document); // Model instance — the row
Warrant::can('update', [Document::class, $id]); // [class, id] — a row by key
Warrant::can('create', Document::class); // model class — no-target
Warrant::can('create', DocumentSchema::class); // schema class — no-target
Warrant::can('create', 'documents'); // schema key — no-target

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

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 positionally
public 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;

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 host

decision() 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 existence

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

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.

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;
public function loadUserAbilities(
?Authenticatable $user = null,
string $selectedAbilitiesKey = 'abilities',
array $context = [],
): array; // computes, then setAttribute() on the instance
  • $user defaults to auth()->user(); scopes throw LogicException if no user is available.
  • userHasAbility([]) (empty ability list) leaves the query unchanged.
  • selectUserAbilities targets rows via getQualifiedKeyName(). Its JSON column is in ability declaration order and requires a supported DB driver.

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

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 + context
Gate::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.

Warrant\AbilityMatchMode::ALL; // every listed ability required
Warrant\AbilityMatchMode::ANY; // any one is enough

AbilityMatchMode 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 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 !== NEVER
Warrant::couldEverHaveAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;
Warrant::alwaysHas($schema, string|array $abilities, ?Authenticatable $user = null): bool; // all === ALWAYS
Warrant::alwaysHasAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;
Warrant::neverHas($schema, string|array $abilities, ?Authenticatable $user = null): bool; // all === NEVER
Warrant::neverHasAny($schema, string|array $abilities, ?Authenticatable $user = null): bool;
Warrant::possibleAbilities($schema, ?Authenticatable $user = null): array; // reachability !== NEVER
Warrant::guaranteedAbilities($schema, ?Authenticatable $user = null): array; // === ALWAYS
Warrant::impossibleAbilities($schema, ?Authenticatable $user = null): array; // === NEVER

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

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;