Skip to content

Checking access

Once schema, resolver, and rules are in place, you never touch the compiler directly. You ask questions through the model, query scopes, static helpers, or middleware.

Add the HasWarrantSchema trait and point it at the schema:

use Illuminate\Database\Eloquent\Model;
use Warrant\HasWarrantSchema;
class Document extends Model
{
use HasWarrantSchema;
public function warrantSchema(): string
{
return \App\Warrant\DocumentSchema::class;
}
}

Run as a scoped EXISTS query — no records are loaded:

Document::userHasAbilities('update', $document); // a model instance
Document::userHasAbilities('update', $documentId); // a key
Document::userHasAbilities(['view', 'update'], $document); // several at once
Document::userHasAbilities('create'); // no-target / capability
// Instance form (checks $this):
$document->hasAbility('update');

Each accepts an optional $user (defaults to auth()->user()) and, for userHasAbilities, an AbilityMatchMode.

The hasAbility scope restricts a query to the rows the user may act on — the “which records?” question, answered in SQL:

// Documents the current user can update:
Document::query()->hasAbility('update')->paginate();
// Rows they can BOTH view and approve:
Document::query()->hasAbility(['view', 'approve'], matchMode: AbilityMatchMode::ALL)->get();
// For a specific user:
Document::query()->hasAbility('delete', $user)->get();

The selectAbilities scope attaches a computed abilities column — a JSON array of what the user can do to that row — so your UI renders controls without N extra checks:

$rows = Document::query()->selectAbilities()->get();
$rows->first()->abilities; // ['view', 'update']

The list is ordered by ability declaration order in the schema.

On a list endpoint you often only care about a subset (say, just update for an Edit button). Narrowing it is a real saving — the attached subquery grows one UNION ALL branch per ability:

Document::query()->selectAbilities(onlyAbilities: ['update'])->get();

You can also change the column name and attach abilities to an already-loaded model:

Document::query()->selectAbilities(selectedAbilitiesKey: 'perms')->get();
$document->loadAbilities(); // sets $document->abilities
$document->getUserAbilities($document); // ['view', 'update'] — static form

When you check several abilities at once, AbilityMatchMode decides how they combine:

  • AbilityMatchMode::ALL — the row/user must satisfy every listed ability.
  • AbilityMatchMode::ANYany one is enough.
use Warrant\AbilityMatchMode;
Document::query()->hasAbility(['view', 'approve'], matchMode: AbilityMatchMode::ANY)->get();

Not every check is about a row. “Can this user create documents?” or “can they access settings?” have no target. Pass null as the target (or omit it):

Document::userHasAbilities('create'); // target defaults to null
DocumentSchema::getUserAbilities(); // all no-target abilities

For section-level capabilities with no model at all, define a capability schema with const model = '' and only #[GlobalCondition] conditions. In a no-target check, targeted conditions are treated as false, so only global logic contributes.

Every check API takes an optional context: array — values for any @context keys the rules reference. See Check-time context.

Call Question
Model::userHasAbilities($abilities, $target, $user, $matchMode, $context) Can they? (bool)
$model->hasAbility($abilities, $user, $matchMode, $context) Can they, for this instance?
Model::getUserAbilities($target, $user, $context) What can they do to this? (array)
->hasAbility(...) scope Which rows?
->selectAbilities(...) scope What per row?
$model->loadAbilities(...) Attach abilities to an instance
Model::authorize($abilities, $target, $user, $matchMode, $context) Can they? — throws a 403 with a message
Model::userCouldEverHave($abilities, $user, $matchMode) Could they ever? (bool)

Full signatures are in the Checking API reference.

Two questions this guide doesn’t cover live on their own pages: when a denial should explain itself, authorize throws a 403 carrying the responsible rule’s message; and when you only need to know whether an ability is conceivable — to render a nav or gate a section without a query — reachability answers structurally, no SQL.