Conditions
Conditions are the predicates a rule’s if may test. Each is a public method on
the schema, marked #[RowCondition] or #[GlobalCondition]. A condition’s
one job is to emit SQL — there is no in-memory evaluation path, so a
condition behaves identically when filtering a list or checking one row.
Condition names
Section titled “Condition names”The name a rule uses is the method name snake-cased, with no prefix added or
stripped: isSelf → is_self, managesTeam → manages_team.
Override it by passing a key to the attribute:
#[RowCondition('is_owner')]public function isSelf(RowConditionContext $c): Builder { /* ... */ }Row vs. global
Section titled “Row vs. global”The distinction is: does this predicate talk about a specific row?
#[RowCondition] — constrains which rows match
Section titled “#[RowCondition] — constrains which rows match”Its context is a RowConditionContext exposing $c->row() — which returns the
qualified primary-key SQL id of the row under test (documents.id), or the
qualified name of any column you name ($c->row('user_id') → documents.user_id).
Always build column references with it rather than writing the table yourself: the
name a row answers to is not always the model’s table — a caller may have aliased
the query, and a cross-schema reference gives its rows a name of their own.
Mutate $c->query to add the WHERE fragment and return the builder:
use Illuminate\Contracts\Database\Query\Builder;use Warrant\Schema\Conditions\RowConditionContext;use Warrant\Schema\RowCondition;
#[RowCondition]public function isSelf(RowConditionContext $c): Builder{ // $c->row() === "documents.id" (the correlated row under test) return $c->query->whereRaw( 'documents.user_id = ?', [$c->user->getAuthIdentifier()], );}Your predicate may reference any column of the entity’s table; it’s evaluated correlated to the row under test.
Answering in PHP when you already hold the row
Section titled “Answering in PHP when you already hold the row”A check aimed at one specific row often already has that row loaded —
$guard->can('update', $document). In that case $c->model is that model,
and the condition may decide outright instead of describing the row in SQL.
Return a bool and Warrant folds it like any other constant, so a check whose
conditions all answer this way never reaches the database:
#[RowCondition]public function isSelf(RowConditionContext $c): Builder|bool{ if ($c->model !== null) { return $c->model->user_id === $c->user->getAuthIdentifier(); }
return $c->query->whereRaw('documents.user_id = ?', [$c->user->getAuthIdentifier()]);}$c->model is null whenever one row is not enough — filtering a query,
listing per-row abilities — or when the row is unproven: a check given a bare
key, or an unsaved or deleted model. Warrant only passes a model Eloquent
regards as hydrated (Model::$exists), since anything else describes a row that
may not be there. So the SQL branch is never optional — it is the only form
that can filter, and it still runs for every check that names a row by key.
#[GlobalCondition] — about the user or the world
Section titled “#[GlobalCondition] — about the user or the world”Its context is a GlobalConditionContext (no row()). It may mutate
$c->query like a row condition, or simply return a bool:
use Warrant\Schema\GlobalCondition;use Warrant\Schema\Conditions\GlobalConditionContext;
#[GlobalCondition]public function isAdmin(GlobalConditionContext $c): bool{ return (bool) $c->user->is_admin; // true = holds for this user}Why the split matters
Section titled “Why the split matters”Some checks run with no row — no-target checks
and Warrant::abilities(Document::class) with no target. In that context a row
condition can’t be evaluated at all, so Warrant treats it as unanswerable
rather than false: it grants nothing, and not <row-condition> is unanswerable
too, so it cannot lift a deny either. See
How it compiles.
Global conditions still evaluate normally, which is why a no-model schema should
only use global conditions.
Answering unknown
Section titled “Answering unknown”A condition that cannot settle its question — a missing context value, a lookup
that came back empty, anything whose answer is genuinely absent rather than
negative — may return null:
#[GlobalCondition]public function inBillingPeriod(GlobalConditionContext $c): ?bool{ $period = $c->context['period'] ?? null;
return $period === null ? null // no answer, rather than "no" : $period === $c->user->billing_period;}null is not false. It compiles to the third truth value, which negates to
itself, so it grants nothing and cannot lift a cannot — the same treatment
the compiler gives its own unanswerable questions, described in
How it compiles.
Answering false instead would make a missing answer capable of granting
access, because not false is true.
The context object
Section titled “The context object”Every condition method takes the context object as its first parameter and
returns Builder (mutated), a bool that decides the outcome outright — for a
global condition always, and for a row condition when it was handed $c->model —
or null to answer unknown.
The object carries:
| Property | Type | Present on |
|---|---|---|
$c->user |
Authenticatable |
both |
$c->query |
Builder (query builder) |
both |
$c->arguments |
array |
both |
$c->context |
array |
both |
$c->row() |
string (method) |
row only |
$c->model |
?Model |
row only |
Arguments
Section titled “Arguments”A condition can take arguments from the rule (in_team('sales')). Declare them as
parameters after the context object: the context is always first, then
parameter #2 binds argument[0], #3 binds argument[1], and so on.
#[RowCondition]public function inTeam(RowConditionContext $c, string $team): Builder{ // in_team('sales') -> $team === 'sales' return $c->query->whereRaw('documents.team_id = ?', [$team]);}A variadic parameter collects a list argument:
#[RowCondition]public function inTeams(RowConditionContext $c, string ...$teams): Builder{ // in_teams('sales', 'eng') -> $teams === ['sales', 'eng'] return $c->query->whereIn('documents.team_id', $teams);}A parameter with a default value is optional — the rule may omit that
argument. Supplying fewer arguments than the required parameters is rejected
during rule validation; supplying more is fine — the extras are ignored by the
call but stay reachable via $c->arguments, the full positional array. A condition
that ignores arguments simply declares no parameters beyond the context.
Arguments come from inline literals, bindings, or @context;
a value passed via a binding reaches you verbatim — any PHP type, including
arrays and objects. (Type-hint a parameter only as loosely as its values allow —
use mixed or a nullable type for an argument that may be null, e.g. an absent
@context key.)
The ambient context bag
Section titled “The ambient context bag”Every condition also receives the full effective context on $c->context,
whether or not the rule passed a value via @context. Reach into it directly when
a condition is inherently tied to the frame — then the rule needn’t mention the
key at all:
#[RowCondition]public function inCurrentWorkspace(RowConditionContext $c): Builder{ // Rule is just `if in_current_workspace they can view` — no @context needed. return $c->query->where('documents.workspace_id', $c->context['workspace_id']);}The difference from @context: a condition reading $c->context decides for
itself what an absent key means, whereas a missing optional @context key is
passed positionally to the condition as null (standard SQL logic then applies —
typically UNKNOWN, which grants no access). See Check-time context
for that mechanism.
Always bind values
Section titled “Always bind values”How conditions become SQL
Section titled “How conditions become SQL”Each condition is spliced inline into the compiled WHERE as a nested
predicate, with negation pushed onto the leaves via De Morgan (a not becomes
not (…), or not exists (…) for a whereExists). Warrant does not normalize
SQL’s three-valued logic: a condition that touches a NULL column is UNKNOWN, so
it contributes no access — an unknown condition never grants and never lifts a deny.
The failure direction is always safe (worst case: a legitimate user is blocked,
never unauthorized access); handle NULL explicitly in the condition if you want a
different outcome. See How it compiles to SQL.
