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

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.

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 { /* ... */ }

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
}

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.

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.

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

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

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.

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.