Check-time context
Some values a rule needs aren’t known when the schema is written or when the resolver builds the rules — they’re known only at the moment of the check: the current tenant, an academic year, an as-of date, an impersonated user. These are context keys.
Declaring context keys
Section titled “Declaring context keys”Declare each with #[ContextKey], mirroring #[Ability] — the constant’s
value is the key string; its name is irrelevant to Warrant:
use Warrant\ContextKey;
// Required by default: no check on this resource resolves without the frame.#[ContextKey] public const WORKSPACE = 'workspace_id';
// Opt out for a frame that only gates grants.#[ContextKey(required: false)] public const AS_OF = 'as_of_date';DocumentSchema::declaredContextKeys(); // ['workspace_id', 'as_of_date']DocumentSchema::requiredContextKeys(); // ['workspace_id']Two ways a condition reads context
Section titled “Two ways a condition reads context”1. @context in the rule
Section titled “1. @context in the rule”A rule references a key with @context <key>; the value is threaded into
$c->arguments positionally at check time:
if in_workspace(@context workspace_id) they can view, edit#[TargetedCondition]public function inWorkspace(TargetedConditionContext $c): Builder{ [$workspace] = $c->arguments; // supplied at the check via @context workspace_id return $c->query->where('documents.workspace_id', $workspace);}An undeclared @context reference is a compile-time error, exactly like an
unknown condition name. Unlike :name / ? bindings, a @context reference is
not subject to the parse-time “every binding used / no mixing” rules — it
carries no value at parse time, may sit alongside literals and bindings, and never
consumes a positional ?:
if scoped_to('projects', @context project_id, :region) they can view2. The ambient $c->context bag
Section titled “2. The ambient $c->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:
#[TargetedCondition]public function inCurrentWorkspace(TargetedConditionContext $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']);}Two styles, same value. @context threads a key positionally and soft-falses the
condition when an optional key is missing; $c->context hands every condition
the whole bag to read however it likes. Pick whichever makes your rules read the
way you want.
Passing context to a check
Section titled “Passing context to a check”Every check API takes an optional context: array. It threads through the model
helpers, the query scopes, and the capability checks alike:
// Boolean check:Document::userHasAbilities('update', $document, context: ['workspace_id' => $id]);
// Row filtering:Document::query()->hasAbility('update', context: ['workspace_id' => $id])->paginate();
// Per-row abilities, evaluated in one fixed frame:Document::query()->selectAbilities(context: ['workspace_id' => $id])->get();Whatever you pass is merged over defaultContext(), with explicit
values winning (a partial merge — you can override just one key).
Defaults
Section titled “Defaults”defaultContext() supplies defaults so callers may omit a key — and so
param-less paths (route middleware, the SelectAbilities
global scope) get a frame with no context: argument:
protected function defaultContext(): array{ return ['workspace_id' => app('tenant')->id];}A default can satisfy a required key, so a required key with a default never throws.
Required vs. optional
Section titled “Required vs. optional”Keys are required by default. Any check on the schema throws unless the key is present in the effective context (explicit + defaults):
Schema [...] requires context key(s) [workspace_id]; supply them at the checkor via defaultContext().That loud failure is a feature — a required frame is never silently skipped.
The fail-open caveat
Section titled “The fail-open caveat”Opt out with #[ContextKey(required: false)] only for a frame that never
gates a cannot. Here’s why:
When an optional @context key is absent at check time, its condition becomes
unevaluable and is treated as false — the same rule Warrant applies to a
targeted condition in a no-target check (and, by De Morgan, not <it> becomes
true).
- On a grant (
can), false is safe: no key, no grant (fail-closed). - On a deny (
cannot), false means the veto doesn’t apply — the deny lifts (fail-open).
So a missing optional key can silently remove a restriction. That’s exactly what
required: true forecloses. When in doubt, leave it required.
# If workspace_id is optional and absent, this cannot LIFTS — the user is no# longer blocked. Declare workspace_id required to prevent that.if outside_workspace(@context workspace_id) they cannot view