Rule-building API
Reference for constructing rules. Conceptual coverage is in Providing rules and The rule language.
The $schema parameter throughout is a Model instance, a WarrantSchema
instance, a schema/model class-string, or a plain schema-key string.
Warrant facade — the authoring front door
Section titled “Warrant facade — the authoring front door”Four entry points, one per construct. Each parses Warrant syntax and takes exactly the parameters of the constructor it delegates to.
use Warrant\Facades\Warrant;
Warrant::condition(?string $syntax = null, array $bindings = []): IBooleanExpressionNode|WarrantConditionBuilder;Warrant::rule(?string $syntax = null, Model|WarrantSchema|string|null $schema = null, array $bindings = []): WarrantRule|WarrantRuleBuilder;Warrant::ruleSet(string $syntax, Model|WarrantSchema|string|null $schema = null, array $bindings = []): WarrantRuleSet;Warrant::group(string $syntax, array $bindings = []): RuleSetGroup;Given syntax, each returns the finished construct. Given nothing, condition() and
rule() return the builder that composes one — they are the two constructs that
are their fluent chain, so an empty call is meaningful. A rule set and a group are
collections, so their syntax is required; build those from values you already hold
with WarrantRuleSet::fromRules() or RuleSetGroup::fromRuleSets() below.
Warrant::condition('is_owner or is_admin'); // IBooleanExpressionNodeWarrant::condition()->if('is_owner')->orIf('is_admin'); // WarrantConditionBuilderWarrant::rule('for documents if is_self they can view'); // WarrantRuleWarrant::ruleSet('for documents { they can view }'); // WarrantRuleSetWarrant::group('for documents { … } for timesheets { … }'); // RuleSetGroupPrefer naming the schema in the string’s own for header rather than in the
$schema argument. The header travels with the string, so editor tooling reading
your source can tell which schema to check the names against; a string with no
header is simply left unchecked. A header and a $schema argument that disagree
are an error.
The header is accepted on a condition expression too, and discarded — an expression has no schema field to carry it, and it exists purely so a condition written as a string is as checkable as every other construct:
Warrant::condition('for documents is_owner or is_admin'); // header parsed, then droppedWarrantRuleSet (readonly)
Section titled “WarrantRuleSet (readonly)”public string $schemaKey;public array $rules;
public function __construct(Model|WarrantSchema|string $schema, array $rules);
public static function fromSyntax( string $syntax, Model|WarrantSchema|string|null $schema = null, array $bindings = [],): self;
public static function fromRules( Model|WarrantSchema|string $schema, WarrantRule|WarrantRuleBuilder|array ...$rules,): self; // flattens arrays; calls toRule() on builders; takes no bindings
public static function build( Model|WarrantSchema|string $schema, Closure $callback, // ($rule) => { $rule()->...; } — each call appends a rule): self;
public function toSyntax(): string; // canonical DSL, inline literalspublic function toBoundSyntax(): BoundSyntax; // DSL + a positional bindings arraypublic function validate(): void; // name-check against the registered schemapublic static function validateAll(WarrantRuleSet|array ...$ruleSets): void;validate() / validateAll() throw on the first unknown ability, condition, or
context-key name — useful for CI-checking stored rules.
They also reject a rule that carries a denial message
but has no they cannot clause (InvalidArgumentException).
WarrantRule (readonly)
Section titled “WarrantRule (readonly)”public ?IBooleanExpressionNode $conditions; // null = unconditionalpublic ?string $schemaKey; // null = schema-lesspublic array $canAbilities;public array $cannotClauses; // list<CannotClause>; each carries its own message
public static function fromSyntax(string $syntax, Model|WarrantSchema|string|null $schema = null, array $bindings = []): self; // exactly one rulepublic static function build(): WarrantRuleBuilder;
public function cannotAbilities(): array; // every denied ability, flattenedpublic function messageFor(string $ability): string|Closure|null;public function withDenialMessage(string|Closure $message, ?array $abilities = null): self; // a copy carrying the messagepublic function withSchemaKey(?string $schemaKey): self;public function toSyntax(): string;public function toBoundSyntax(): BoundSyntax;fromSyntax throws if the string parses to zero or more than one rule.
A denial message lives on a cannot clause, not on
the rule, so one rule can deny two sets of abilities for two different reasons;
messageFor() resolves the message for a given ability. withDenialMessage()
returns a new WarrantRule (the class is immutable), attaching the message to the
named abilities, or to every denied ability when $abilities is null. A message is
not representable in the string DSL, so toSyntax() / toBoundSyntax() drop it.
WarrantRuleBuilder
Section titled “WarrantRuleBuilder”Returned by WarrantRule::build(). Extends the condition builder with clause
methods.
Condition methods (from WarrantConditionBuilder)
Section titled “Condition methods (from WarrantConditionBuilder)”Each returns static and takes a condition name + parameters, or a closure (a
parenthesized group):
->if(string|Closure $condition, array $parameters = [])->andIf(...) // alias of if; both mean `and`->orIf(...) // `or`->ifNot(...) // `and not`->andIfNot(...) // `and not`->orIfNot(...) // `or not`
->ifRaw(string $expression, array $bindings = []) // splice a parsed DSL fragment as one group->orIfRaw(string $expression, array $bindings = [])
->when(mixed $condition, Closure $callback): static // Laravel-style conditionalCross-schema methods (from WarrantConditionBuilder)
Section titled “Cross-schema methods (from WarrantConditionBuilder)”->ifCan(string $ability, Model|WarrantSchema|string $schema, mixed $key = new NoRow, array $with = [])->andIfCan(...) // alias of ifCan->orIfCan(...) // `or can(...)`
->ifCheck(string|Closure $predicate, Model|WarrantSchema|string $schema, mixed $key = new NoRow, array $with = [])->andIfCheck(...) // alias of ifCheck->orIfCheck(...) // `or check(...)`NoRow and Ref
Section titled “NoRow and Ref”new Warrant\Builders\NoRow // the default $key: an unbound handleWarrant\Builders\Ref::context(string $key): ContextRef // @context <key>Warrant\Builders\Ref::column(string $column): ColumnRef // @column <column>Warrant\Builders\Ref::column(string $frame, string $column): ColumnRef // @column <name>.<column>Warrant\Builders\Ref::sql(string $sql): SqlRef // @sql "<sql>"A Ref is valid anywhere the builder takes an argument value: a condition
parameter, a cross-schema row selector, or a with map value.
Clause methods (from WarrantRuleBuilder)
Section titled “Clause methods (from WarrantRuleBuilder)”->theyCan(string ...$abilities): static // additive->theyCannot(string ...$abilities): static // additive->theyCannotBecause(string|list<string> $abilities, string|Closure $message): static // deny with a message->toRule(): WarrantRule // throws LogicException if no clause settheyCannotBecause() adds one clause per call, so separate calls give separate
abilities separate messages; abilities passed together share one message. To
attach a message to an existing rule instead, use
WarrantRule::withDenialMessage(). See
Denial messages for what a message closure receives
and where the message surfaces.
Semantics
Section titled “Semantics”- Precedence is
not>and>or, identical to the DSL — the builder produces a byte-for-byte identical AST. - A closure is a parenthesized group and receives a bare
WarrantConditionBuilder(notheyCan/theyCannot). - An empty group folds to
false— nothing in anor, a veto in anand. - Condition parameters may be any PHP value — nothing is stringified.
canandcheckhave no negated variants — negate one with a group,->ifNot(fn ($c) => $c->ifCan(...)).- Omitting
$keygives an unbound handle; an explicitkey: nullstays row-bound and is rejected byvalidate(), so a missing id fails loudly instead of widening a row question into a schema-wide one. - An empty
checkpredicate closure throwsLogicException— unlike a group it cannot fall back tofalse, because a predicate may not contain a constant. $schemais normalized to a schema key through the registry, so a model or schema class-string that resolves to nothing throwsOutOfBoundsExceptionat build time. A plain unregistered key string passes through, and a typo’d key is caught byvalidate().
WarrantParser (final)
Section titled “WarrantParser (final)”public static function parse(string $source, array $bindings = []): array; // WarrantRule[]public static function parseSingleRule(string $source, array $bindings = []): WarrantRule;public static function parseConditionExpression(string $source, array $bindings = []): IBooleanExpressionNode;Round-tripping
Section titled “Round-tripping”toSyntax() and toBoundSyntax() render a rule back to the DSL and parse-back
identically. toSyntax() can only render parameters that are expressible as
inline literals (scalars); a parameter that’s an array, object, NAN, INF,
or a float needing exponent notation throws a LogicException — use
toBoundSyntax(), which extracts every parameter as a positional binding.
@context references render as @context <key> in both forms and never consume a
positional binding, and so do @column and @sql. A built can/check renders
as can(<ability> for <schema>(<row>) with <k> = <v>), so a builder-authored
cross-schema rule round-trips like any other.
