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.