Skip to content

Commit c3c2e00

Browse files
author
Nico
committed
Implement custom tokens
1 parent c89aba7 commit c3c2e00

25 files changed

+308
-63
lines changed

README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Rules Parser and Evaluator for PHP 5.4+
1+
## Rules Parser and Evaluator for PHP 7
22

33
| | Build Status | Code Quality | Coverage | HHVM |
44
|:----------------:|:----------------:|:----------:|:----------:|:----------:|
@@ -8,6 +8,8 @@
88

99
[![Latest Stable Version](https://img.shields.io/packagist/v/nicoswd/php-rule-parser.svg)](https://packagist.org/packages/nicoswd/php-rule-parser)
1010

11+
*Note:* This is a development branch. Use at own risk until tagged stable.
12+
1113
You're looking at a PHP library to parse and evaluate text based rules with a Javascript-like syntax. This project was born out of the necessity to evaluate hundreds of rules that were originally written and evaluated in JavaScript, and now needed to be evaluated on the server-side, using PHP.
1214

1315
This library has initially been used to change and configure the behavior of certain "Workflows" (without changing actual code) in an intranet application, but it may serve a purpose elsewhere.
@@ -69,6 +71,56 @@ $rule = new Rule($ruleStr, $variables);
6971
var_dump($rule->isTrue()); // bool(true)
7072
```
7173

74+
## Custom Functions
75+
76+
```php
77+
$ruleStr = 'double(value) === result';
78+
79+
$variables = [
80+
'value' => 2,
81+
'result' => 4
82+
];
83+
84+
$rule = new Rule($ruleStr, $variables);
85+
86+
$rule->registerFunction('double', function (BaseToken $multiplier) : BaseToken {
87+
if (!$multiplier instanceof TokenInteger) {
88+
throw new \Exception;
89+
}
90+
91+
return new TokenInteger($multiplier->getValue() * 2);
92+
});
93+
94+
var_dump($rule->isTrue()); // bool(true)
95+
```
96+
97+
## Redefine Operators
98+
Operators can be overwritten with custom ones, if desired. Note that it's easy to break stuff by doing that.
99+
You may want to set a different `$priority` when registering a token. `::registerToken($type, $regex, $priority)`. Take a look at `Tokenizer::__construct()` for more info.
100+
101+
```php
102+
$ruleStr = ':this is greater than :that';
103+
104+
$variables = [
105+
':this' => 8,
106+
':that' => 7
107+
];
108+
109+
$rule = new Rule($ruleStr, $variables);
110+
111+
$rule->registerToken(
112+
Tokenizer::TOKEN_GREATER,
113+
'\bis\s+greater\s+than\b'
114+
);
115+
116+
$rule->registerToken(
117+
Tokenizer::TOKEN_VARIABLE,
118+
':\w+'
119+
);
120+
121+
var_dump($rule->isTrue()); // bool(true)
122+
```
123+
72124
## Error Handling
73125
Both, `$rule->isTrue()` and `$rule->isFalse()` will throw an exception if the syntax is invalid. These calls can either be placed inside a `try` / `catch` block, or it can be checked prior using `$rule->isValid()`.
74126

phpunit.xml.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
</testsuite>
1515
</testsuites>
1616

17+
<filter>
18+
<whitelist processUncoveredFilesFromWhitelist="true">
19+
<directory suffix=".php">src</directory>
20+
</whitelist>
21+
</filter>
22+
1723
<!--<logging>
1824
<log type="coverage-html"
1925
target="tests/log/report"

src/nicoSWD/Rules/Expressions/Factory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function createFromOperator(string $operator) : BaseExpression
4343
return new $this->classLookup[$operator]();
4444
}
4545

46-
public function mapOperatorToClass(string $operator, string $class)
46+
public function mapOperatorToClass(string $operator, $class)
4747
{
4848
$this->classLookup[$operator] = $class;
4949
}

src/nicoSWD/Rules/Parser.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ public function registerFunction(string $name, Closure $callback)
260260
$this->userDefinedFunctions[$name] = $callback;
261261
}
262262

263+
public function registerToken(string $token, string $regex, int $priority = 10)
264+
{
265+
$this->tokenizer->registerToken($token, $regex, $priority);
266+
}
267+
263268
/**
264269
* @param string $name
265270
* @return Closure|null

src/nicoSWD/Rules/Rule.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
namespace nicoSWD\Rules;
1111

1212
use Closure;
13+
use Exception;
14+
use nicoSWD\Rules\Expressions\BaseExpression;
1315

1416
class Rule
1517
{
@@ -40,7 +42,7 @@ class Rule
4042

4143
public function __construct(string $rule, array $variables = [])
4244
{
43-
$this->rule = (string) $rule;
45+
$this->rule = $rule;
4446
$this->parser = new Parser(new Tokenizer(), new Expressions\Factory());
4547
$this->evaluator = new Evaluator();
4648

@@ -67,7 +69,7 @@ public function isValid() : bool
6769
{
6870
try {
6971
$this->parsedRule = $this->parser->parse($this->rule);
70-
} catch (\Exception $e) {
72+
} catch (Exception $e) {
7173
$this->error = $e->getMessage();
7274
return false;
7375
}
@@ -80,6 +82,11 @@ public function registerFunction(string $name, Closure $callback)
8082
$this->parser->registerFunction($name, $callback);
8183
}
8284

85+
public function registerOperator(string $operator, BaseExpression $token)
86+
{
87+
$this->parser->registerToken($operator, $token);
88+
}
89+
8390
public function getError() : string
8491
{
8592
return $this->error;

src/nicoSWD/Rules/Tokenizer.php

Lines changed: 117 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,92 @@
55
* @link https://github.com/nicoSWD
66
* @author Nicolas Oelgart <nico@oelgart.com>
77
*/
8-
declare(strict_types=1);
8+
declare(strict_types = 1);
99

1010
namespace nicoSWD\Rules;
1111

12+
use SplPriorityQueue;
13+
use stdClass;
14+
1215
final class Tokenizer implements TokenizerInterface
1316
{
14-
/**
15-
* @var string
16-
*/
17-
private $tokens = '
18-
~(
19-
(?<And>&&)
20-
| (?<Or>\|\|)
21-
| (?<NotEqualStrict>!==)
22-
| (?<NotEqual><>|!=)
23-
| (?<EqualStrict>===)
24-
| (?<Equal>==)
25-
| (?<In>\bin\b)
26-
| (?<Bool>\b(?:true|false)\b)
27-
| (?<Null>\bnull\b)
28-
| (?<Method>\.\s*[a-zA-Z_]\w*\s*\()
29-
| (?<Function>[a-zA-Z_]\w*\s*\()
30-
| (?<Variable>[a-zA-Z_]\w*)
31-
| (?<Float>-?\d+(?:\.\d+))
32-
| (?<Integer>-?\d+)
33-
| (?<EncapsedString>"[^"]*"|\'[^\']*\')
34-
| (?<SmallerEqual><=)
35-
| (?<GreaterEqual>>=)
36-
| (?<Smaller><)
37-
| (?<Greater>>)
38-
| (?<OpeningParentheses>\()
39-
| (?<ClosingParentheses>\))
40-
| (?<OpeningArray>\[)
41-
| (?<ClosingArray>\])
42-
| (?<Comma>,)
43-
| (?<Regex>/[^/\*].*/[igm]{0,3})
44-
| (?<Comment>//[^\r\n]*|/\*.*?\*/)
45-
| (?<Newline>\r?\n)
46-
| (?<Space>\s+)
47-
| (?<Unknown>.)
48-
)~xAs';
17+
const TOKEN_AND = 'And';
18+
const TOKEN_OR = 'Or';
19+
const TOKEN_NOT_EQUAL_STRICT = 'NotEqualStrict';
20+
const TOKEN_NOT_EQUAL = 'NotEqual';
21+
const TOKEN_EQUAL_STRICT = 'EqualStrict';
22+
const TOKEN_EQUAL = 'Equal';
23+
const TOKEN_IN = 'In';
24+
const TOKEN_BOOL = 'Bool';
25+
const TOKEN_NULL = 'Null';
26+
const TOKEN_METHOD = 'Method';
27+
const TOKEN_FUNCTION = 'Function';
28+
const TOKEN_VARIABLE = 'Variable';
29+
const TOKEN_FLOAT = 'Float';
30+
const TOKEN_INTEGER = 'Integer';
31+
const TOKEN_ENCAPSED_STRING = 'EncapsedString';
32+
const TOKEN_SMALLER_EQUAL = 'SmallerEqual';
33+
const TOKEN_GREATER_EQUAL = 'GreaterEqual';
34+
const TOKEN_SMALLER = 'Smaller';
35+
const TOKEN_GREATER = 'Greater';
36+
const TOKEN_OPENING_PARENTHESIS = 'OpeningParentheses';
37+
const TOKEN_CLOSING_PARENTHESIS = 'ClosingParentheses';
38+
const TOKEN_OPENING_ARRAY = 'OpeningArray';
39+
const TOKEN_CLOSING_ARRAY = 'ClosingArray';
40+
const TOKEN_COMMA = 'Comma';
41+
const TOKEN_REGEX = 'Regex';
42+
const TOKEN_COMMENT = 'Comment';
43+
const TOKEN_NEWLINE = 'Newline';
44+
const TOKEN_SPACE = 'Space';
45+
const TOKEN_UNKNOWN = 'Unknown';
46+
47+
private $internalTokens = [];
48+
49+
private $regex = '';
50+
51+
private $regexRequiresReassambly = false;
52+
53+
public function __construct()
54+
{
55+
$this->registerToken(self::TOKEN_AND, '&&', 145);
56+
$this->registerToken(self::TOKEN_OR, '\|\|', 140);
57+
$this->registerToken(self::TOKEN_NOT_EQUAL_STRICT, '!==', 135);
58+
$this->registerToken(self::TOKEN_NOT_EQUAL, '<>|!=', 130);
59+
$this->registerToken(self::TOKEN_EQUAL_STRICT, '===', 125);
60+
$this->registerToken(self::TOKEN_EQUAL, '==', 120);
61+
$this->registerToken(self::TOKEN_IN, '\bin\b', 115);
62+
$this->registerToken(self::TOKEN_BOOL, '\b(?:true|false)\b', 110);
63+
$this->registerToken(self::TOKEN_NULL, '\bnull\b', 105);
64+
$this->registerToken(self::TOKEN_METHOD, '\.\s*[a-zA-Z_]\w*\s*\(', 100);
65+
$this->registerToken(self::TOKEN_FUNCTION, '[a-zA-Z_]\w*\s*\(', 95);
66+
$this->registerToken(self::TOKEN_FLOAT, '-?\d+(?:\.\d+)', 90);
67+
$this->registerToken(self::TOKEN_INTEGER, '-?\d+', 85);
68+
$this->registerToken(self::TOKEN_ENCAPSED_STRING, '"[^"]*"|\'[^\']*\'', 80);
69+
$this->registerToken(self::TOKEN_SMALLER_EQUAL, '<=', 75);
70+
$this->registerToken(self::TOKEN_GREATER_EQUAL, '>=', 70);
71+
$this->registerToken(self::TOKEN_SMALLER, '<', 65);
72+
$this->registerToken(self::TOKEN_GREATER, '>', 60);
73+
$this->registerToken(self::TOKEN_OPENING_PARENTHESIS, '\(', 55);
74+
$this->registerToken(self::TOKEN_CLOSING_PARENTHESIS, '\)', 50);
75+
$this->registerToken(self::TOKEN_OPENING_ARRAY, '\[', 45);
76+
$this->registerToken(self::TOKEN_CLOSING_ARRAY, '\]', 40);
77+
$this->registerToken(self::TOKEN_COMMA, ',', 35);
78+
$this->registerToken(self::TOKEN_REGEX, '/[^/\*].*/[igm]{0,3}', 30);
79+
$this->registerToken(self::TOKEN_COMMENT, '//[^\r\n]*|/\*.*?\*/', 25);
80+
$this->registerToken(self::TOKEN_NEWLINE, '\r?\n', 20);
81+
$this->registerToken(self::TOKEN_SPACE, '\s+', 15);
82+
$this->registerToken(self::TOKEN_VARIABLE, '[a-zA-Z_]\w*', 10);
83+
$this->registerToken(self::TOKEN_UNKNOWN, '.', 5);
84+
}
4985

5086
public function tokenize(string $string) : Stack
5187
{
5288
$stack = new Stack();
89+
$regex = $this->getRegex();
5390
$baseNameSpace = __NAMESPACE__ . '\\Tokens\\Token';
5491
$offset = 0;
5592

56-
while (preg_match($this->tokens, $string, $matches, 0, $offset)) {
93+
while (preg_match($regex, $string, $matches, 0, $offset)) {
5794
$token = $this->getMatchedToken($matches);
5895
$className = $baseNameSpace . $token;
5996

@@ -69,6 +106,17 @@ public function tokenize(string $string) : Stack
69106
return $stack;
70107
}
71108

109+
public function registerToken(string $class, string $regex, int $priority = null)
110+
{
111+
$token = new StdClass();
112+
$token->class = $class;
113+
$token->regex = $regex;
114+
$token->priority = $priority ?? $this->getPriority($class);
115+
116+
$this->internalTokens[$class] = $token;
117+
$this->regexRequiresReassambly = true;
118+
}
119+
72120
private function getMatchedToken(array $matches) : string
73121
{
74122
foreach ($matches as $key => $value) {
@@ -79,4 +127,36 @@ private function getMatchedToken(array $matches) : string
79127

80128
return 'Unknown';
81129
}
130+
131+
private function getRegex() : string
132+
{
133+
if (!$this->regex || $this->regexRequiresReassambly) {
134+
$regex = [];
135+
136+
foreach ($this->getQueue() as $token) {
137+
$regex[] = "(?<$token->class>$token->regex)";
138+
}
139+
140+
$this->regex = sprintf('~(%s)~As', implode('|', $regex));
141+
$this->regexRequiresReassambly = false;
142+
}
143+
144+
return $this->regex;
145+
}
146+
147+
private function getQueue() : SplPriorityQueue
148+
{
149+
$queue = new SplPriorityQueue();
150+
151+
foreach ($this->internalTokens as $class) {
152+
$queue->insert($class, $class->priority);
153+
}
154+
155+
return $queue;
156+
}
157+
158+
private function getPriority(string $class) : int
159+
{
160+
return $this->internalTokens[$class] ?? 10;
161+
}
82162
}

src/nicoSWD/Rules/TokenizerInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ interface TokenizerInterface
1515
* @throws \Exception
1616
*/
1717
public function tokenize(string $string) : Stack;
18+
19+
public function registerToken(string $token, string $regex, int $priority);
1820
}

src/nicoSWD/Rules/Tokens/BaseToken.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?php
22

3-
declare(strict_types=1);
4-
53
/**
64
* @license http://opensource.org/licenses/mit-license.php MIT
75
* @link https://github.com/nicoSWD
86
* @author Nicolas Oelgart <nico@oelgart.com>
97
*/
8+
declare(strict_types=1);
9+
1010
namespace nicoSWD\Rules\Tokens;
1111

1212
use nicoSWD\Rules\Stack;

src/nicoSWD/Rules/Tokens/TokenClosingArray.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public function getGroup() : int
1717
{
1818
return Constants::GROUP_SQUARE_BRACKETS;
1919
}
20+
21+
public function getValue()
22+
{
23+
return ']';
24+
}
2025
}

src/nicoSWD/Rules/Tokens/TokenClosingParentheses.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public function getGroup() : int
1717
{
1818
return Constants::GROUP_PARENTHESES;
1919
}
20+
21+
public function getValue()
22+
{
23+
return ')';
24+
}
2025
}

0 commit comments

Comments
 (0)