Skip to content

The rule builder

The rule language is one way to author a rule; the fluent rule builder is the other. When a rule’s shape depends on runtime data — a list of team ids, a feature flag, values that don’t belong in a string — WarrantRule::build() is often clearer than assembling DSL text.

It produces the same AST the parser does, so a built rule flows through identical validation and compilation. Nothing is serialized to a string, so arbitrary PHP values in condition parameters survive untouched.

use Warrant\RuleSyntaxTree\WarrantRule;
$rule = WarrantRule::build()
->if('is_self')
->orIf(fn ($c) => $c->if('is_manager')->andIf('in_region'))
->theyCan('view', 'update')
->theyCannot('delete')
->toRule();

That builds the same rule as:

if is_self or (is_manager and in_region)
they can view, update
they cannot delete

Each connective has a plain and a negated form, mirroring Laravel’s where/orWhere/whereNot:

Method DSL equivalent
if / andIf and (both are aliases; the first term’s connective is ignored)
orIf or
ifNot / andIfNot and not
orIfNot or not

Each takes a condition name (with optional parameters) or a closure:

->if('in_team', ['sales', 'eng']) // condition with parameters
->orIf(fn ($c) => $c->if('a')->orIf('b')) // closure = a parenthesized group

A closure is a parenthesized group. It receives a bare condition builder — it has if/orIf/… but no theyCan/theyCannot, because a group is only ever a condition, never a whole rule.

not > and > or, so the two front-ends produce byte-for-byte identical trees. ->if('a')->andIf('b')->orIf('c') is (a and b) or c, not a and (b or c). See operator precedence in the rule language.

Fold a list inside a group, or branch with when():

$rule = WarrantRule::build()
->if('is_self')
->orIf(function ($c) use ($teamIds) {
foreach ($teamIds as $id) {
$c->orIf('in_team', [$id]);
}
})
->when($includeManagers, fn ($c) => $c->orIf('is_manager'))
->theyCan('view')
->toRule();

An empty group folds to false, so it contributes nothing to an or and vetoes an and — folding an empty list is a safe no-op.

ifRaw() / orIfRaw() parse a DSL fragment and splice it in as one group — author the readable part as text, compose the rest structurally:

->ifRaw('is_admin or is_owner', $bindings = [])->andIf('in_region')

toRule() throws a LogicException if you call neither theyCan nor theyCannot — exactly as the DSL rejects a bare if with no clause.

withDenialMessage() is available mid-chain, to explain a cannot when it fires:

WarrantRule::build()
->if('is_locked')->theyCannot('update')
->withDenialMessage('This document is locked and can no longer be edited.')
->toRule();

See Denial messages for the full behaviour.


Built rules go into a WarrantRuleSet just like parsed ones — see Providing rules for the rule-set constructors, and the Rule-building API for every method signature.