phplrt 4.0

Rule Engine

A rule language of the kind that ends up in the database of every application with permissions or discounts in it - a condition written by someone who is not going to deploy PHP to change it:

1group in ['admin', 'moderator'] and points > 30
2user.group.name = 'admin' and user.points() >= 100
3count(user.roles) > 0 xor is_active(user, now())

The grammar is a precedence ladder, one rule per level: Disjunction for or and xor, Conjunction for and, Operation for everything that compares. Each is written in terms of the one below it, which is all that "binds tighter" ever means in a grammar.

Then comes the good part. The comparison operators are not in the grammar at all: T_IDENTIFIER is "anything but the characters written around it", so =, >= and in are read as ordinary names, and Operation reads some name between two operands. Adding an operator to this language is a change to the evaluator and not to the grammar.

Grammar

  1/**
  2 * -----------------------------------------------------------------------------
  3 *  Hoa Ruler
  4 * -----------------------------------------------------------------------------
  5 *
  6 * A rule language: a condition written of the values it compares, the
  7 * operators joining them and the functions it calls.
  8 *
  9 * @see https://github.com/hoaproject/Ruler/blob/master/Grammar.pp
 10 */
 11
 12%pragma root Expression
 13
 14%skip  T_WHITESPACE           \s++
 15
 16%token T_TRUE                 (?i)true\b
 17%token T_FALSE                (?i)false\b
 18%token T_NULL                 (?i)null\b
 19%token T_NOT                  (?i)not\b
 20%token T_AND                  (?i)and\b
 21%token T_OR                   (?i)or\b
 22%token T_XOR                  (?i)xor\b
 23
 24// A quote written inside a string is escaped with a backslash
 25%token T_STRING               "(?:[^"\\]|\\.)*+"|'(?:[^'\\]|\\.)*+'
 26
 27%token T_FLOAT                [0-9]++\.[0-9]++
 28%token T_INTEGER              [0-9]++
 29
 30%token T_PARENTHESIS_OPEN     \(
 31%token T_PARENTHESIS_CLOSE    \)
 32%token T_BRACKET_OPEN         \[
 33%token T_BRACKET_CLOSE        \]
 34%token T_COMMA                ,
 35%token T_DOT                  \.
 36
 37// Anything but the characters written around it, an operator included
 38%token T_IDENTIFIER           [^\s()\[\],.]++
 39
 40/**
 41 * -----------------------------------------------------------------------------
 42 *  Expression
 43 * -----------------------------------------------------------------------------
 44 *
 45 *  One rule per precedence, from the loosest binding one down to the tightest:
 46 *  - a or b, a xor b
 47 *  - a and b
 48 *  - a = b, a in b - the operator is written as a name of its own
 49 */
 50
 51Expression
 52  : Disjunction()
 53  ;
 54
 55Disjunction
 56  : Conjunction() ((::T_OR:: | ::T_XOR::) Disjunction())?
 57  ;
 58
 59Conjunction
 60  : Operation() (::T_AND:: Conjunction())?
 61  ;
 62
 63Operation
 64  : Operand() (<T_IDENTIFIER> Disjunction())?
 65  ;
 66
 67Operand
 68  : ::T_PARENTHESIS_OPEN:: Disjunction() ::T_PARENTHESIS_CLOSE::
 69  | Value()
 70  ;
 71
 72Value
 73  : ::T_NOT:: Disjunction()
 74  | <T_TRUE>
 75  | <T_FALSE>
 76  | <T_NULL>
 77  | <T_FLOAT>
 78  | <T_INTEGER>
 79  | <T_STRING>
 80  | ArrayDeclaration()
 81  | Chain()
 82  ;
 83
 84// ["a", "b"]
 85ArrayDeclaration
 86  : ::T_BRACKET_OPEN:: Value() (::T_COMMA:: Value())* ::T_BRACKET_CLOSE::
 87  ;
 88
 89// user.group[0].name(), points
 90Chain
 91  : (FunctionCall() | Variable()) (ArrayAccess() | ObjectAccess())*
 92  ;
 93
 94Variable
 95  : <T_IDENTIFIER>
 96  ;
 97
 98ArrayAccess
 99  : ::T_BRACKET_OPEN:: Value() ::T_BRACKET_CLOSE::
100  ;
101
102ObjectAccess
103  : ::T_DOT:: (FunctionCall() | <T_IDENTIFIER>)
104  ;
105
106FunctionCall
107  : <T_IDENTIFIER> ::T_PARENTHESIS_OPEN::
108      (Disjunction() (::T_COMMA:: Disjunction())*)?
109    ::T_PARENTHESIS_CLOSE::
110  ;

Usage

1use Phplrt\Compiler\Compiler;
2use Phplrt\Source\File;
3use Phplrt\Source\Source;
4
5$parser = new Compiler()
6    ->load(new File(__DIR__ . '/grammar.pp3'))
7    ->getParser();
8
9$rule = $parser->parse(new Source("group in ['admin'] and points > 30"));

Chain is the other half of the appeal: user.group[0].name() is read as a variable followed by a list of accesses, so the evaluator walks it against a context array or object without the grammar knowing anything about either.

25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.