├── CHANGELOG.md ├── Compiler.php ├── Expression.php ├── ExpressionFunction.php ├── ExpressionFunctionProviderInterface.php ├── ExpressionLanguage.php ├── LICENSE ├── Lexer.php ├── Node ├── ArgumentsNode.php ├── ArrayNode.php ├── BinaryNode.php ├── ConditionalNode.php ├── ConstantNode.php ├── FunctionNode.php ├── GetAttrNode.php ├── NameNode.php ├── Node.php ├── NullCoalesceNode.php ├── NullCoalescedNameNode.php └── UnaryNode.php ├── ParsedExpression.php ├── Parser.php ├── README.md ├── Resources └── bin │ └── generate_operator_regex.php ├── SerializedParsedExpression.php ├── SyntaxError.php ├── Token.php ├── TokenStream.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.2 5 | --- 6 | 7 | * Add support for null-coalescing unknown variables 8 | * Add support for comments using `/*` & `*/` 9 | * Allow passing any iterable as `$providers` list to `ExpressionLanguage` constructor 10 | * Add support for `<<`, `>>`, and `~` bitwise operators 11 | * Add support for logical `xor` operator 12 | 13 | 7.1 14 | --- 15 | 16 | * Add support for PHP `min` and `max` functions 17 | * Add `Parser::IGNORE_UNKNOWN_VARIABLES` and `Parser::IGNORE_UNKNOWN_FUNCTIONS` flags to control whether 18 | parsing and linting should check for unknown variables and functions. 19 | * Deprecate passing `null` as the allowed variable names to `ExpressionLanguage::lint()` and `Parser::lint()`, 20 | pass the `IGNORE_UNKNOWN_VARIABLES` flag instead to ignore unknown variables during linting 21 | 22 | 7.0 23 | --- 24 | 25 | * The `in` and `not in` operators now use strict comparison 26 | 27 | 6.3 28 | --- 29 | 30 | * Add `enum` expression function 31 | * Deprecate loose comparisons when using the "in" operator; normalize the array parameter 32 | so it only has the expected types or implement loose matching in your own expression function 33 | 34 | 6.2 35 | --- 36 | 37 | * Add support for null-coalescing syntax 38 | 39 | 6.1 40 | --- 41 | 42 | * Add support for null-safe syntax when parsing object's methods and properties 43 | * Add new operators: `contains`, `starts with` and `ends with` 44 | * Support lexing numbers with the numeric literal separator `_` 45 | * Support lexing decimals with no leading zero 46 | 47 | 5.1.0 48 | ----- 49 | 50 | * added `lint` method to `ExpressionLanguage` class 51 | * added `lint` method to `Parser` class 52 | 53 | 4.0.0 54 | ----- 55 | 56 | * the first argument of the `ExpressionLanguage` constructor must be an instance 57 | of `CacheItemPoolInterface` 58 | * removed the `ArrayParserCache` and `ParserCacheAdapter` classes 59 | * removed the `ParserCacheInterface` 60 | 61 | 2.6.0 62 | ----- 63 | 64 | * Added ExpressionFunction and ExpressionFunctionProviderInterface 65 | 66 | 2.4.0 67 | ----- 68 | 69 | * added the component 70 | -------------------------------------------------------------------------------- /Compiler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | use Symfony\Contracts\Service\ResetInterface; 15 | 16 | /** 17 | * Compiles a node to PHP code. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class Compiler implements ResetInterface 22 | { 23 | private string $source = ''; 24 | 25 | public function __construct( 26 | private array $functions, 27 | ) { 28 | } 29 | 30 | public function getFunction(string $name): array 31 | { 32 | return $this->functions[$name]; 33 | } 34 | 35 | /** 36 | * Gets the current PHP code after compilation. 37 | */ 38 | public function getSource(): string 39 | { 40 | return $this->source; 41 | } 42 | 43 | /** 44 | * @return $this 45 | */ 46 | public function reset(): static 47 | { 48 | $this->source = ''; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Compiles a node. 55 | * 56 | * @return $this 57 | */ 58 | public function compile(Node\Node $node): static 59 | { 60 | $node->compile($this); 61 | 62 | return $this; 63 | } 64 | 65 | public function subcompile(Node\Node $node): string 66 | { 67 | $current = $this->source; 68 | $this->source = ''; 69 | 70 | $node->compile($this); 71 | 72 | $source = $this->source; 73 | $this->source = $current; 74 | 75 | return $source; 76 | } 77 | 78 | /** 79 | * Adds a raw string to the compiled code. 80 | * 81 | * @return $this 82 | */ 83 | public function raw(string $string): static 84 | { 85 | $this->source .= $string; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Adds a quoted string to the compiled code. 92 | * 93 | * @return $this 94 | */ 95 | public function string(string $value): static 96 | { 97 | $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Returns a PHP representation of a given value. 104 | * 105 | * @return $this 106 | */ 107 | public function repr(mixed $value): static 108 | { 109 | if (\is_int($value) || \is_float($value)) { 110 | if (false !== $locale = setlocale(\LC_NUMERIC, 0)) { 111 | setlocale(\LC_NUMERIC, 'C'); 112 | } 113 | 114 | $this->raw($value); 115 | 116 | if (false !== $locale) { 117 | setlocale(\LC_NUMERIC, $locale); 118 | } 119 | } elseif (null === $value) { 120 | $this->raw('null'); 121 | } elseif (\is_bool($value)) { 122 | $this->raw($value ? 'true' : 'false'); 123 | } elseif (\is_array($value)) { 124 | $this->raw('['); 125 | $first = true; 126 | foreach ($value as $key => $value) { 127 | if (!$first) { 128 | $this->raw(', '); 129 | } 130 | $first = false; 131 | $this->repr($key); 132 | $this->raw(' => '); 133 | $this->repr($value); 134 | } 135 | $this->raw(']'); 136 | } else { 137 | $this->string($value); 138 | } 139 | 140 | return $this; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Expression.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Represents an expression. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Expression 20 | { 21 | public function __construct( 22 | protected string $expression, 23 | ) { 24 | } 25 | 26 | /** 27 | * Gets the expression. 28 | */ 29 | public function __toString(): string 30 | { 31 | return $this->expression; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ExpressionFunction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Represents a function that can be used in an expression. 16 | * 17 | * A function is defined by two PHP callables. The callables are used 18 | * by the language to compile and/or evaluate the function. 19 | * 20 | * The "compiler" function is used at compilation time and must return a 21 | * PHP representation of the function call (it receives the function 22 | * arguments as arguments). 23 | * 24 | * The "evaluator" function is used for expression evaluation and must return 25 | * the value of the function call based on the values defined for the 26 | * expression (it receives the values as a first argument and the function 27 | * arguments as remaining arguments). 28 | * 29 | * @author Fabien Potencier 30 | */ 31 | class ExpressionFunction 32 | { 33 | private \Closure $compiler; 34 | private \Closure $evaluator; 35 | 36 | /** 37 | * @param string $name The function name 38 | * @param callable $compiler A callable able to compile the function 39 | * @param callable $evaluator A callable able to evaluate the function 40 | */ 41 | public function __construct( 42 | private string $name, 43 | callable $compiler, 44 | callable $evaluator, 45 | ) { 46 | $this->compiler = $compiler(...); 47 | $this->evaluator = $evaluator(...); 48 | } 49 | 50 | public function getName(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | public function getCompiler(): \Closure 56 | { 57 | return $this->compiler; 58 | } 59 | 60 | public function getEvaluator(): \Closure 61 | { 62 | return $this->evaluator; 63 | } 64 | 65 | /** 66 | * Creates an ExpressionFunction from a PHP function name. 67 | * 68 | * @param string|null $expressionFunctionName The expression function name (default: same than the PHP function name) 69 | * 70 | * @throws \InvalidArgumentException if given PHP function name does not exist 71 | * @throws \InvalidArgumentException if given PHP function name is in namespace 72 | * and expression function name is not defined 73 | */ 74 | public static function fromPhp(string $phpFunctionName, ?string $expressionFunctionName = null): self 75 | { 76 | $phpFunctionName = ltrim($phpFunctionName, '\\'); 77 | if (!\function_exists($phpFunctionName)) { 78 | throw new \InvalidArgumentException(\sprintf('PHP function "%s" does not exist.', $phpFunctionName)); 79 | } 80 | 81 | $parts = explode('\\', $phpFunctionName); 82 | if (!$expressionFunctionName && \count($parts) > 1) { 83 | throw new \InvalidArgumentException(\sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName)); 84 | } 85 | 86 | $compiler = fn (...$args) => \sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args)); 87 | 88 | $evaluator = fn ($p, ...$args) => $phpFunctionName(...$args); 89 | 90 | return new self($expressionFunctionName ?: end($parts), $compiler, $evaluator); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ExpressionFunctionProviderInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * @author Fabien Potencier 16 | */ 17 | interface ExpressionFunctionProviderInterface 18 | { 19 | /** 20 | * @return ExpressionFunction[] 21 | */ 22 | public function getFunctions(): array; 23 | } 24 | -------------------------------------------------------------------------------- /ExpressionLanguage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\Cache\Adapter\ArrayAdapter; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(ParsedExpression::class); 19 | 20 | /** 21 | * Allows to compile and evaluate expressions written in your own DSL. 22 | * 23 | * @author Fabien Potencier 24 | */ 25 | class ExpressionLanguage 26 | { 27 | private CacheItemPoolInterface $cache; 28 | private Lexer $lexer; 29 | private Parser $parser; 30 | private Compiler $compiler; 31 | 32 | protected array $functions = []; 33 | 34 | /** 35 | * @param iterable $providers 36 | */ 37 | public function __construct(?CacheItemPoolInterface $cache = null, iterable $providers = []) 38 | { 39 | $this->cache = $cache ?? new ArrayAdapter(); 40 | $this->registerFunctions(); 41 | foreach ($providers as $provider) { 42 | $this->registerProvider($provider); 43 | } 44 | } 45 | 46 | /** 47 | * Compiles an expression source code. 48 | */ 49 | public function compile(Expression|string $expression, array $names = []): string 50 | { 51 | return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource(); 52 | } 53 | 54 | /** 55 | * Evaluate an expression. 56 | */ 57 | public function evaluate(Expression|string $expression, array $values = []): mixed 58 | { 59 | return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values); 60 | } 61 | 62 | /** 63 | * Parses an expression. 64 | * 65 | * @param int-mask-of $flags 66 | */ 67 | public function parse(Expression|string $expression, array $names, int $flags = 0): ParsedExpression 68 | { 69 | if ($expression instanceof ParsedExpression) { 70 | return $expression; 71 | } 72 | 73 | asort($names); 74 | $cacheKeyItems = []; 75 | 76 | foreach ($names as $nameKey => $name) { 77 | $cacheKeyItems[] = \is_int($nameKey) ? $name : $nameKey.':'.$name; 78 | } 79 | 80 | $cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems))); 81 | 82 | if (null === $parsedExpression = $cacheItem->get()) { 83 | $nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names, $flags); 84 | $parsedExpression = new ParsedExpression((string) $expression, $nodes); 85 | 86 | $cacheItem->set($parsedExpression); 87 | $this->cache->save($cacheItem); 88 | } 89 | 90 | return $parsedExpression; 91 | } 92 | 93 | /** 94 | * Validates the syntax of an expression. 95 | * 96 | * @param array|null $names The list of acceptable variable names in the expression 97 | * @param int-mask-of $flags 98 | * 99 | * @throws SyntaxError When the passed expression is invalid 100 | */ 101 | public function lint(Expression|string $expression, ?array $names, int $flags = 0): void 102 | { 103 | if (null === $names) { 104 | trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s\Parser::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __NAMESPACE__); 105 | 106 | $flags |= Parser::IGNORE_UNKNOWN_VARIABLES; 107 | $names = []; 108 | } 109 | 110 | if ($expression instanceof ParsedExpression) { 111 | return; 112 | } 113 | 114 | $this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names, $flags); 115 | } 116 | 117 | /** 118 | * Registers a function. 119 | * 120 | * @param callable $compiler A callable able to compile the function 121 | * @param callable $evaluator A callable able to evaluate the function 122 | * 123 | * @throws \LogicException when registering a function after calling evaluate(), compile() or parse() 124 | * 125 | * @see ExpressionFunction 126 | */ 127 | public function register(string $name, callable $compiler, callable $evaluator): void 128 | { 129 | if (isset($this->parser)) { 130 | throw new \LogicException('Registering functions after calling evaluate(), compile() or parse() is not supported.'); 131 | } 132 | 133 | $this->functions[$name] = ['compiler' => $compiler, 'evaluator' => $evaluator]; 134 | } 135 | 136 | public function addFunction(ExpressionFunction $function): void 137 | { 138 | $this->register($function->getName(), $function->getCompiler(), $function->getEvaluator()); 139 | } 140 | 141 | public function registerProvider(ExpressionFunctionProviderInterface $provider): void 142 | { 143 | foreach ($provider->getFunctions() as $function) { 144 | $this->addFunction($function); 145 | } 146 | } 147 | 148 | /** 149 | * @return void 150 | */ 151 | protected function registerFunctions() 152 | { 153 | $basicPhpFunctions = ['constant', 'min', 'max']; 154 | foreach ($basicPhpFunctions as $function) { 155 | $this->addFunction(ExpressionFunction::fromPhp($function)); 156 | } 157 | 158 | $this->addFunction(new ExpressionFunction('enum', 159 | static fn ($str): string => \sprintf("(\constant(\$v = (%s))) instanceof \UnitEnum ? \constant(\$v) : throw new \TypeError(\sprintf('The string \"%%s\" is not the name of a valid enum case.', \$v))", $str), 160 | static function ($arguments, $str): \UnitEnum { 161 | $value = \constant($str); 162 | 163 | if (!$value instanceof \UnitEnum) { 164 | throw new \TypeError(\sprintf('The string "%s" is not the name of a valid enum case.', $str)); 165 | } 166 | 167 | return $value; 168 | } 169 | )); 170 | } 171 | 172 | private function getLexer(): Lexer 173 | { 174 | return $this->lexer ??= new Lexer(); 175 | } 176 | 177 | private function getParser(): Parser 178 | { 179 | return $this->parser ??= new Parser($this->functions); 180 | } 181 | 182 | private function getCompiler(): Compiler 183 | { 184 | $this->compiler ??= new Compiler($this->functions); 185 | 186 | return $this->compiler->reset(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Lexer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Lexes an expression. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Lexer 20 | { 21 | /** 22 | * Tokenizes an expression. 23 | * 24 | * @throws SyntaxError 25 | */ 26 | public function tokenize(string $expression): TokenStream 27 | { 28 | $expression = str_replace(["\r", "\n", "\t", "\v", "\f"], ' ', $expression); 29 | $cursor = 0; 30 | $tokens = []; 31 | $brackets = []; 32 | $end = \strlen($expression); 33 | 34 | while ($cursor < $end) { 35 | if (' ' == $expression[$cursor]) { 36 | ++$cursor; 37 | 38 | continue; 39 | } 40 | 41 | if (preg_match('/ 42 | (?(DEFINE)(?P[0-9]+(_[0-9]+)*)) 43 | (?:\.(?&LNUM)|(?&LNUM)(?:\.(?!\.)(?&LNUM)?)?)(?:[eE][+-]?(?&LNUM))?/Ax', 44 | $expression, $match, 0, $cursor) 45 | ) { 46 | // numbers 47 | $tokens[] = new Token(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0]), $cursor + 1); 48 | $cursor += \strlen($match[0]); 49 | } elseif (str_contains('([{', $expression[$cursor])) { 50 | // opening bracket 51 | $brackets[] = [$expression[$cursor], $cursor]; 52 | 53 | $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); 54 | ++$cursor; 55 | } elseif (str_contains(')]}', $expression[$cursor])) { 56 | // closing bracket 57 | if (!$brackets) { 58 | throw new SyntaxError(\sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression); 59 | } 60 | 61 | [$expect, $cur] = array_pop($brackets); 62 | if ($expression[$cursor] != strtr($expect, '([{', ')]}')) { 63 | throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $cur, $expression); 64 | } 65 | 66 | $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); 67 | ++$cursor; 68 | } elseif (preg_match('/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, 0, $cursor)) { 69 | // strings 70 | $tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1); 71 | $cursor += \strlen($match[0]); 72 | } elseif (preg_match('{/\*.*?\*/}A', $expression, $match, 0, $cursor)) { 73 | // comments 74 | $cursor += \strlen($match[0]); 75 | } elseif (preg_match('/(?<=^|[\s(])starts with(?=[\s(])|(?<=^|[\s(])ends with(?=[\s(])|(?<=^|[\s(])contains(?=[\s(])|(?<=^|[\s(])matches(?=[\s(])|(?<=^|[\s(])not in(?=[\s(])|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])xor(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\<\<|\>\>|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) { 76 | // operators 77 | $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); 78 | $cursor += \strlen($match[0]); 79 | } elseif ('?' === $expression[$cursor] && '.' === ($expression[$cursor + 1] ?? '')) { 80 | // null-safe 81 | $tokens[] = new Token(Token::PUNCTUATION_TYPE, '?.', ++$cursor); 82 | ++$cursor; 83 | } elseif ('?' === $expression[$cursor] && '?' === ($expression[$cursor + 1] ?? '')) { 84 | // null-coalescing 85 | $tokens[] = new Token(Token::PUNCTUATION_TYPE, '??', ++$cursor); 86 | ++$cursor; 87 | } elseif (str_contains('.,?:', $expression[$cursor])) { 88 | // punctuation 89 | $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); 90 | ++$cursor; 91 | } elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, 0, $cursor)) { 92 | // names 93 | $tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1); 94 | $cursor += \strlen($match[0]); 95 | } else { 96 | // unlexable 97 | throw new SyntaxError(\sprintf('Unexpected character "%s".', $expression[$cursor]), $cursor, $expression); 98 | } 99 | } 100 | 101 | $tokens[] = new Token(Token::EOF_TYPE, null, $cursor + 1); 102 | 103 | if ($brackets) { 104 | [$expect, $cur] = array_pop($brackets); 105 | throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $cur, $expression); 106 | } 107 | 108 | return new TokenStream($tokens, $expression); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Node/ArgumentsNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class ArgumentsNode extends ArrayNode 22 | { 23 | public function compile(Compiler $compiler): void 24 | { 25 | $this->compileArguments($compiler, false); 26 | } 27 | 28 | public function toArray(): array 29 | { 30 | $array = []; 31 | 32 | foreach ($this->getKeyValuePairs() as $pair) { 33 | $array[] = $pair['value']; 34 | $array[] = ', '; 35 | } 36 | array_pop($array); 37 | 38 | return $array; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Node/ArrayNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class ArrayNode extends Node 22 | { 23 | protected int $index; 24 | 25 | public function __construct() 26 | { 27 | $this->index = -1; 28 | } 29 | 30 | public function addElement(Node $value, ?Node $key = null): void 31 | { 32 | $key ??= new ConstantNode(++$this->index); 33 | 34 | array_push($this->nodes, $key, $value); 35 | } 36 | 37 | /** 38 | * Compiles the node to PHP. 39 | */ 40 | public function compile(Compiler $compiler): void 41 | { 42 | $compiler->raw('['); 43 | $this->compileArguments($compiler); 44 | $compiler->raw(']'); 45 | } 46 | 47 | public function evaluate(array $functions, array $values): array 48 | { 49 | $result = []; 50 | foreach ($this->getKeyValuePairs() as $pair) { 51 | $result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values); 52 | } 53 | 54 | return $result; 55 | } 56 | 57 | public function toArray(): array 58 | { 59 | $value = []; 60 | foreach ($this->getKeyValuePairs() as $pair) { 61 | $value[$pair['key']->attributes['value']] = $pair['value']; 62 | } 63 | 64 | $array = []; 65 | 66 | if ($this->isHash($value)) { 67 | foreach ($value as $k => $v) { 68 | $array[] = ', '; 69 | $array[] = new ConstantNode($k); 70 | $array[] = ': '; 71 | $array[] = $v; 72 | } 73 | $array[0] = '{'; 74 | $array[] = '}'; 75 | } else { 76 | foreach ($value as $v) { 77 | $array[] = ', '; 78 | $array[] = $v; 79 | } 80 | $array[0] = '['; 81 | $array[] = ']'; 82 | } 83 | 84 | return $array; 85 | } 86 | 87 | protected function getKeyValuePairs(): array 88 | { 89 | $pairs = []; 90 | foreach (array_chunk($this->nodes, 2) as $pair) { 91 | $pairs[] = ['key' => $pair[0], 'value' => $pair[1]]; 92 | } 93 | 94 | return $pairs; 95 | } 96 | 97 | protected function compileArguments(Compiler $compiler, bool $withKeys = true): void 98 | { 99 | $first = true; 100 | foreach ($this->getKeyValuePairs() as $pair) { 101 | if (!$first) { 102 | $compiler->raw(', '); 103 | } 104 | $first = false; 105 | 106 | if ($withKeys) { 107 | $compiler 108 | ->compile($pair['key']) 109 | ->raw(' => ') 110 | ; 111 | } 112 | 113 | $compiler->compile($pair['value']); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Node/BinaryNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | use Symfony\Component\ExpressionLanguage\SyntaxError; 16 | 17 | /** 18 | * @author Fabien Potencier 19 | * 20 | * @internal 21 | */ 22 | class BinaryNode extends Node 23 | { 24 | private const OPERATORS = [ 25 | '~' => '.', 26 | 'and' => '&&', 27 | 'or' => '||', 28 | ]; 29 | 30 | private const FUNCTIONS = [ 31 | '**' => 'pow', 32 | '..' => 'range', 33 | 'in' => '\\in_array', 34 | 'not in' => '!\\in_array', 35 | 'contains' => 'str_contains', 36 | 'starts with' => 'str_starts_with', 37 | 'ends with' => 'str_ends_with', 38 | ]; 39 | 40 | public function __construct(string $operator, Node $left, Node $right) 41 | { 42 | parent::__construct( 43 | ['left' => $left, 'right' => $right], 44 | ['operator' => $operator] 45 | ); 46 | } 47 | 48 | public function compile(Compiler $compiler): void 49 | { 50 | $operator = $this->attributes['operator']; 51 | 52 | if ('matches' == $operator) { 53 | if ($this->nodes['right'] instanceof ConstantNode) { 54 | $this->evaluateMatches($this->nodes['right']->evaluate([], []), ''); 55 | } elseif ($this->nodes['right'] instanceof self && '~' !== $this->nodes['right']->attributes['operator']) { 56 | throw new SyntaxError('The regex passed to "matches" must be a string.'); 57 | } 58 | 59 | $compiler 60 | ->raw('(static function ($regexp, $str) { set_error_handler(static fn ($t, $m) => throw new \Symfony\Component\ExpressionLanguage\SyntaxError(sprintf(\'Regexp "%s" passed to "matches" is not valid\', $regexp).substr($m, 12))); try { return preg_match($regexp, (string) $str); } finally { restore_error_handler(); } })(') 61 | ->compile($this->nodes['right']) 62 | ->raw(', ') 63 | ->compile($this->nodes['left']) 64 | ->raw(')') 65 | ; 66 | 67 | return; 68 | } 69 | 70 | if (isset(self::FUNCTIONS[$operator])) { 71 | $compiler 72 | ->raw(\sprintf('%s(', self::FUNCTIONS[$operator])) 73 | ->compile($this->nodes['left']) 74 | ->raw(', ') 75 | ->compile($this->nodes['right']) 76 | ; 77 | 78 | if ('in' === $operator || 'not in' === $operator) { 79 | $compiler->raw(', true'); 80 | } 81 | 82 | $compiler->raw(')'); 83 | 84 | return; 85 | } 86 | 87 | if (isset(self::OPERATORS[$operator])) { 88 | $operator = self::OPERATORS[$operator]; 89 | } 90 | 91 | $compiler 92 | ->raw('(') 93 | ->compile($this->nodes['left']) 94 | ->raw(' ') 95 | ->raw($operator) 96 | ->raw(' ') 97 | ->compile($this->nodes['right']) 98 | ->raw(')') 99 | ; 100 | } 101 | 102 | public function evaluate(array $functions, array $values): mixed 103 | { 104 | $operator = $this->attributes['operator']; 105 | $left = $this->nodes['left']->evaluate($functions, $values); 106 | 107 | if (isset(self::FUNCTIONS[$operator])) { 108 | $right = $this->nodes['right']->evaluate($functions, $values); 109 | 110 | return match ($operator) { 111 | 'in' => \in_array($left, $right, true), 112 | 'not in' => !\in_array($left, $right, true), 113 | default => self::FUNCTIONS[$operator]($left, $right), 114 | }; 115 | } 116 | 117 | switch ($operator) { 118 | case 'or': 119 | case '||': 120 | return $left || $this->nodes['right']->evaluate($functions, $values); 121 | case 'xor': 122 | return $left xor $this->nodes['right']->evaluate($functions, $values); 123 | case 'and': 124 | case '&&': 125 | return $left && $this->nodes['right']->evaluate($functions, $values); 126 | } 127 | 128 | $right = $this->nodes['right']->evaluate($functions, $values); 129 | 130 | switch ($operator) { 131 | case '|': 132 | return $left | $right; 133 | case '^': 134 | return $left ^ $right; 135 | case '&': 136 | return $left & $right; 137 | case '<<': 138 | return $left << $right; 139 | case '>>': 140 | return $left >> $right; 141 | case '==': 142 | return $left == $right; 143 | case '===': 144 | return $left === $right; 145 | case '!=': 146 | return $left != $right; 147 | case '!==': 148 | return $left !== $right; 149 | case '<': 150 | return $left < $right; 151 | case '>': 152 | return $left > $right; 153 | case '>=': 154 | return $left >= $right; 155 | case '<=': 156 | return $left <= $right; 157 | case '+': 158 | return $left + $right; 159 | case '-': 160 | return $left - $right; 161 | case '~': 162 | return $left.$right; 163 | case '*': 164 | return $left * $right; 165 | case '/': 166 | if (0 == $right) { 167 | throw new \DivisionByZeroError('Division by zero.'); 168 | } 169 | 170 | return $left / $right; 171 | case '%': 172 | if (0 == $right) { 173 | throw new \DivisionByZeroError('Modulo by zero.'); 174 | } 175 | 176 | return $left % $right; 177 | case 'matches': 178 | return $this->evaluateMatches($right, $left); 179 | } 180 | 181 | throw new \LogicException(\sprintf('"%s" does not support the "%s" operator.', __CLASS__, $operator)); 182 | } 183 | 184 | public function toArray(): array 185 | { 186 | return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')']; 187 | } 188 | 189 | private function evaluateMatches(string $regexp, ?string $str): int 190 | { 191 | set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12))); 192 | try { 193 | return preg_match($regexp, (string) $str); 194 | } finally { 195 | restore_error_handler(); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Node/ConditionalNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class ConditionalNode extends Node 22 | { 23 | public function __construct(Node $expr1, Node $expr2, Node $expr3) 24 | { 25 | parent::__construct( 26 | ['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3] 27 | ); 28 | } 29 | 30 | public function compile(Compiler $compiler): void 31 | { 32 | $compiler 33 | ->raw('((') 34 | ->compile($this->nodes['expr1']) 35 | ->raw(') ? (') 36 | ->compile($this->nodes['expr2']) 37 | ->raw(') : (') 38 | ->compile($this->nodes['expr3']) 39 | ->raw('))') 40 | ; 41 | } 42 | 43 | public function evaluate(array $functions, array $values): mixed 44 | { 45 | if ($this->nodes['expr1']->evaluate($functions, $values)) { 46 | return $this->nodes['expr2']->evaluate($functions, $values); 47 | } 48 | 49 | return $this->nodes['expr3']->evaluate($functions, $values); 50 | } 51 | 52 | public function toArray(): array 53 | { 54 | return ['(', $this->nodes['expr1'], ' ? ', $this->nodes['expr2'], ' : ', $this->nodes['expr3'], ')']; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Node/ConstantNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class ConstantNode extends Node 22 | { 23 | public function __construct( 24 | mixed $value, 25 | private bool $isIdentifier = false, 26 | public readonly bool $isNullSafe = false, 27 | ) { 28 | parent::__construct( 29 | [], 30 | ['value' => $value] 31 | ); 32 | } 33 | 34 | public function compile(Compiler $compiler): void 35 | { 36 | $compiler->repr($this->attributes['value']); 37 | } 38 | 39 | public function evaluate(array $functions, array $values): mixed 40 | { 41 | return $this->attributes['value']; 42 | } 43 | 44 | public function toArray(): array 45 | { 46 | $array = []; 47 | $value = $this->attributes['value']; 48 | 49 | if ($this->isIdentifier) { 50 | $array[] = $value; 51 | } elseif (true === $value) { 52 | $array[] = 'true'; 53 | } elseif (false === $value) { 54 | $array[] = 'false'; 55 | } elseif (null === $value) { 56 | $array[] = 'null'; 57 | } elseif (is_numeric($value)) { 58 | $array[] = $value; 59 | } elseif (!\is_array($value)) { 60 | $array[] = $this->dumpString($value); 61 | } elseif ($this->isHash($value)) { 62 | foreach ($value as $k => $v) { 63 | $array[] = ', '; 64 | $array[] = new self($k); 65 | $array[] = ': '; 66 | $array[] = new self($v); 67 | } 68 | $array[0] = '{'; 69 | $array[] = '}'; 70 | } else { 71 | foreach ($value as $v) { 72 | $array[] = ', '; 73 | $array[] = new self($v); 74 | } 75 | $array[0] = '['; 76 | $array[] = ']'; 77 | } 78 | 79 | return $array; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Node/FunctionNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class FunctionNode extends Node 22 | { 23 | public function __construct(string $name, Node $arguments) 24 | { 25 | parent::__construct( 26 | ['arguments' => $arguments], 27 | ['name' => $name] 28 | ); 29 | } 30 | 31 | public function compile(Compiler $compiler): void 32 | { 33 | $arguments = []; 34 | foreach ($this->nodes['arguments']->nodes as $node) { 35 | $arguments[] = $compiler->subcompile($node); 36 | } 37 | 38 | $function = $compiler->getFunction($this->attributes['name']); 39 | 40 | $compiler->raw($function['compiler'](...$arguments)); 41 | } 42 | 43 | public function evaluate(array $functions, array $values): mixed 44 | { 45 | $arguments = [$values]; 46 | foreach ($this->nodes['arguments']->nodes as $node) { 47 | $arguments[] = $node->evaluate($functions, $values); 48 | } 49 | 50 | return $functions[$this->attributes['name']]['evaluator'](...$arguments); 51 | } 52 | 53 | public function toArray(): array 54 | { 55 | $array = []; 56 | $array[] = $this->attributes['name']; 57 | 58 | foreach ($this->nodes['arguments']->nodes as $node) { 59 | $array[] = ', '; 60 | $array[] = $node; 61 | } 62 | $array[1] = '('; 63 | $array[] = ')'; 64 | 65 | return $array; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Node/GetAttrNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class GetAttrNode extends Node 22 | { 23 | public const PROPERTY_CALL = 1; 24 | public const METHOD_CALL = 2; 25 | public const ARRAY_CALL = 3; 26 | 27 | /** 28 | * @param self::* $type 29 | */ 30 | public function __construct(Node $node, Node $attribute, ArrayNode $arguments, int $type) 31 | { 32 | parent::__construct( 33 | ['node' => $node, 'attribute' => $attribute, 'arguments' => $arguments], 34 | ['type' => $type, 'is_null_coalesce' => false, 'is_short_circuited' => false], 35 | ); 36 | } 37 | 38 | public function compile(Compiler $compiler): void 39 | { 40 | $nullSafe = $this->nodes['attribute'] instanceof ConstantNode && $this->nodes['attribute']->isNullSafe; 41 | switch ($this->attributes['type']) { 42 | case self::PROPERTY_CALL: 43 | $compiler 44 | ->compile($this->nodes['node']) 45 | ->raw($nullSafe ? '?->' : '->') 46 | ->raw($this->nodes['attribute']->attributes['value']) 47 | ; 48 | break; 49 | 50 | case self::METHOD_CALL: 51 | $compiler 52 | ->compile($this->nodes['node']) 53 | ->raw($nullSafe ? '?->' : '->') 54 | ->raw($this->nodes['attribute']->attributes['value']) 55 | ->raw('(') 56 | ->compile($this->nodes['arguments']) 57 | ->raw(')') 58 | ; 59 | break; 60 | 61 | case self::ARRAY_CALL: 62 | $compiler 63 | ->compile($this->nodes['node']) 64 | ->raw('[') 65 | ->compile($this->nodes['attribute'])->raw(']') 66 | ; 67 | break; 68 | } 69 | } 70 | 71 | public function evaluate(array $functions, array $values): mixed 72 | { 73 | switch ($this->attributes['type']) { 74 | case self::PROPERTY_CALL: 75 | $obj = $this->nodes['node']->evaluate($functions, $values); 76 | if (null === $obj && ($this->nodes['attribute']->isNullSafe || $this->attributes['is_null_coalesce'])) { 77 | $this->attributes['is_short_circuited'] = true; 78 | 79 | return null; 80 | } 81 | if (null === $obj && $this->isShortCircuited()) { 82 | return null; 83 | } 84 | 85 | if (!\is_object($obj)) { 86 | throw new \RuntimeException(\sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); 87 | } 88 | 89 | $property = $this->nodes['attribute']->attributes['value']; 90 | 91 | if ($this->attributes['is_null_coalesce']) { 92 | return $obj->$property ?? null; 93 | } 94 | 95 | return $obj->$property; 96 | 97 | case self::METHOD_CALL: 98 | $obj = $this->nodes['node']->evaluate($functions, $values); 99 | 100 | if (null === $obj && $this->nodes['attribute']->isNullSafe) { 101 | $this->attributes['is_short_circuited'] = true; 102 | 103 | return null; 104 | } 105 | if (null === $obj && $this->isShortCircuited()) { 106 | return null; 107 | } 108 | 109 | if (!\is_object($obj)) { 110 | throw new \RuntimeException(\sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump())); 111 | } 112 | if (!\is_callable($toCall = [$obj, $this->nodes['attribute']->attributes['value']])) { 113 | throw new \RuntimeException(\sprintf('Unable to call method "%s" of object "%s".', $this->nodes['attribute']->attributes['value'], get_debug_type($obj))); 114 | } 115 | 116 | return $toCall(...array_values($this->nodes['arguments']->evaluate($functions, $values))); 117 | 118 | case self::ARRAY_CALL: 119 | $array = $this->nodes['node']->evaluate($functions, $values); 120 | 121 | if (null === $array && $this->isShortCircuited()) { 122 | return null; 123 | } 124 | 125 | if (!\is_array($array) && !$array instanceof \ArrayAccess && !(null === $array && $this->attributes['is_null_coalesce'])) { 126 | throw new \RuntimeException(\sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump())); 127 | } 128 | 129 | if ($this->attributes['is_null_coalesce']) { 130 | return $array[$this->nodes['attribute']->evaluate($functions, $values)] ?? null; 131 | } 132 | 133 | return $array[$this->nodes['attribute']->evaluate($functions, $values)]; 134 | } 135 | } 136 | 137 | private function isShortCircuited(): bool 138 | { 139 | return $this->attributes['is_short_circuited'] || ($this->nodes['node'] instanceof self && $this->nodes['node']->isShortCircuited()); 140 | } 141 | 142 | public function toArray(): array 143 | { 144 | switch ($this->attributes['type']) { 145 | case self::PROPERTY_CALL: 146 | return [$this->nodes['node'], '.', $this->nodes['attribute']]; 147 | 148 | case self::METHOD_CALL: 149 | return [$this->nodes['node'], '.', $this->nodes['attribute'], '(', $this->nodes['arguments'], ')']; 150 | 151 | case self::ARRAY_CALL: 152 | return [$this->nodes['node'], '[', $this->nodes['attribute'], ']']; 153 | } 154 | } 155 | 156 | /** 157 | * Provides BC with instances serialized before v6.2. 158 | */ 159 | public function __unserialize(array $data): void 160 | { 161 | $this->nodes = $data['nodes']; 162 | $this->attributes = $data['attributes']; 163 | $this->attributes['is_null_coalesce'] ??= false; 164 | $this->attributes['is_short_circuited'] ??= $data["\x00Symfony\Component\ExpressionLanguage\Node\GetAttrNode\x00isShortCircuited"] ?? false; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Node/NameNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class NameNode extends Node 22 | { 23 | public function __construct(string $name) 24 | { 25 | parent::__construct( 26 | [], 27 | ['name' => $name] 28 | ); 29 | } 30 | 31 | public function compile(Compiler $compiler): void 32 | { 33 | $compiler->raw('$'.$this->attributes['name']); 34 | } 35 | 36 | public function evaluate(array $functions, array $values): mixed 37 | { 38 | return $values[$this->attributes['name']]; 39 | } 40 | 41 | public function toArray(): array 42 | { 43 | return [$this->attributes['name']]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Node/Node.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * Represents a node in the AST. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class Node 22 | { 23 | public array $nodes = []; 24 | public array $attributes = []; 25 | 26 | /** 27 | * @param array $nodes An array of nodes 28 | * @param array $attributes An array of attributes 29 | */ 30 | public function __construct(array $nodes = [], array $attributes = []) 31 | { 32 | $this->nodes = $nodes; 33 | $this->attributes = $attributes; 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | $attributes = []; 39 | foreach ($this->attributes as $name => $value) { 40 | $attributes[] = \sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); 41 | } 42 | 43 | $repr = [str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', static::class).'('.implode(', ', $attributes)]; 44 | 45 | if (\count($this->nodes)) { 46 | foreach ($this->nodes as $node) { 47 | foreach (explode("\n", (string) $node) as $line) { 48 | $repr[] = ' '.$line; 49 | } 50 | } 51 | 52 | $repr[] = ')'; 53 | } else { 54 | $repr[0] .= ')'; 55 | } 56 | 57 | return implode("\n", $repr); 58 | } 59 | 60 | public function compile(Compiler $compiler): void 61 | { 62 | foreach ($this->nodes as $node) { 63 | $node->compile($compiler); 64 | } 65 | } 66 | 67 | public function evaluate(array $functions, array $values): mixed 68 | { 69 | $results = []; 70 | foreach ($this->nodes as $node) { 71 | $results[] = $node->evaluate($functions, $values); 72 | } 73 | 74 | return $results; 75 | } 76 | 77 | /** 78 | * @throws \BadMethodCallException when this node cannot be transformed to an array 79 | */ 80 | public function toArray(): array 81 | { 82 | throw new \BadMethodCallException(\sprintf('Dumping a "%s" instance is not supported yet.', static::class)); 83 | } 84 | 85 | public function dump(): string 86 | { 87 | $dump = ''; 88 | 89 | foreach ($this->toArray() as $v) { 90 | $dump .= \is_scalar($v) ? $v : $v->dump(); 91 | } 92 | 93 | return $dump; 94 | } 95 | 96 | protected function dumpString(string $value): string 97 | { 98 | return \sprintf('"%s"', addcslashes($value, "\0\t\"\\")); 99 | } 100 | 101 | protected function isHash(array $value): bool 102 | { 103 | $expectedKey = 0; 104 | 105 | foreach ($value as $key => $val) { 106 | if ($key !== $expectedKey++) { 107 | return true; 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Node/NullCoalesceNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class NullCoalesceNode extends Node 22 | { 23 | public function __construct(Node $expr1, Node $expr2) 24 | { 25 | parent::__construct(['expr1' => $expr1, 'expr2' => $expr2]); 26 | } 27 | 28 | public function compile(Compiler $compiler): void 29 | { 30 | $compiler 31 | ->raw('((') 32 | ->compile($this->nodes['expr1']) 33 | ->raw(') ?? (') 34 | ->compile($this->nodes['expr2']) 35 | ->raw('))') 36 | ; 37 | } 38 | 39 | public function evaluate(array $functions, array $values): mixed 40 | { 41 | if ($this->nodes['expr1'] instanceof GetAttrNode) { 42 | $this->addNullCoalesceAttributeToGetAttrNodes($this->nodes['expr1']); 43 | } 44 | 45 | return $this->nodes['expr1']->evaluate($functions, $values) ?? $this->nodes['expr2']->evaluate($functions, $values); 46 | } 47 | 48 | public function toArray(): array 49 | { 50 | return ['(', $this->nodes['expr1'], ') ?? (', $this->nodes['expr2'], ')']; 51 | } 52 | 53 | private function addNullCoalesceAttributeToGetAttrNodes(Node $node): void 54 | { 55 | if (!$node instanceof GetAttrNode) { 56 | return; 57 | } 58 | 59 | $node->attributes['is_null_coalesce'] = true; 60 | 61 | foreach ($node->nodes as $node) { 62 | $this->addNullCoalesceAttributeToGetAttrNodes($node); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Node/NullCoalescedNameNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Adam Kiss 18 | * 19 | * @internal 20 | */ 21 | class NullCoalescedNameNode extends Node 22 | { 23 | public function __construct(string $name) 24 | { 25 | parent::__construct( 26 | [], 27 | ['name' => $name] 28 | ); 29 | } 30 | 31 | public function compile(Compiler $compiler): void 32 | { 33 | $compiler->raw('$'.$this->attributes['name'].' ?? null'); 34 | } 35 | 36 | public function evaluate(array $functions, array $values): null 37 | { 38 | return null; 39 | } 40 | 41 | public function toArray(): array 42 | { 43 | return [$this->attributes['name'].' ?? null']; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Node/UnaryNode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage\Node; 13 | 14 | use Symfony\Component\ExpressionLanguage\Compiler; 15 | 16 | /** 17 | * @author Fabien Potencier 18 | * 19 | * @internal 20 | */ 21 | class UnaryNode extends Node 22 | { 23 | private const OPERATORS = [ 24 | '!' => '!', 25 | 'not' => '!', 26 | '+' => '+', 27 | '-' => '-', 28 | '~' => '~', 29 | ]; 30 | 31 | public function __construct(string $operator, Node $node) 32 | { 33 | parent::__construct( 34 | ['node' => $node], 35 | ['operator' => $operator] 36 | ); 37 | } 38 | 39 | public function compile(Compiler $compiler): void 40 | { 41 | $compiler 42 | ->raw('(') 43 | ->raw(self::OPERATORS[$this->attributes['operator']]) 44 | ->compile($this->nodes['node']) 45 | ->raw(')') 46 | ; 47 | } 48 | 49 | public function evaluate(array $functions, array $values): mixed 50 | { 51 | $value = $this->nodes['node']->evaluate($functions, $values); 52 | 53 | return match ($this->attributes['operator']) { 54 | 'not', 55 | '!' => !$value, 56 | '-' => -$value, 57 | '~' => ~$value, 58 | default => $value, 59 | }; 60 | } 61 | 62 | public function toArray(): array 63 | { 64 | return ['(', $this->attributes['operator'].' ', $this->nodes['node'], ')']; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ParsedExpression.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | use Symfony\Component\ExpressionLanguage\Node\Node; 15 | 16 | /** 17 | * Represents an already parsed expression. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class ParsedExpression extends Expression 22 | { 23 | public function __construct( 24 | string $expression, 25 | private Node $nodes, 26 | ) { 27 | parent::__construct($expression); 28 | } 29 | 30 | public function getNodes(): Node 31 | { 32 | return $this->nodes; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Parser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Parses a token stream. 16 | * 17 | * This parser implements a "Precedence climbing" algorithm. 18 | * 19 | * @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm 20 | * @see http://en.wikipedia.org/wiki/Operator-precedence_parser 21 | * 22 | * @author Fabien Potencier 23 | */ 24 | class Parser 25 | { 26 | public const OPERATOR_LEFT = 1; 27 | public const OPERATOR_RIGHT = 2; 28 | 29 | public const IGNORE_UNKNOWN_VARIABLES = 1; 30 | public const IGNORE_UNKNOWN_FUNCTIONS = 2; 31 | 32 | private TokenStream $stream; 33 | private array $unaryOperators; 34 | private array $binaryOperators; 35 | private array $names; 36 | private int $flags = 0; 37 | 38 | public function __construct( 39 | private array $functions, 40 | ) { 41 | $this->unaryOperators = [ 42 | 'not' => ['precedence' => 50], 43 | '!' => ['precedence' => 50], 44 | '-' => ['precedence' => 500], 45 | '+' => ['precedence' => 500], 46 | '~' => ['precedence' => 500], 47 | ]; 48 | $this->binaryOperators = [ 49 | 'or' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT], 50 | '||' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT], 51 | 'xor' => ['precedence' => 12, 'associativity' => self::OPERATOR_LEFT], 52 | 'and' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT], 53 | '&&' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT], 54 | '|' => ['precedence' => 16, 'associativity' => self::OPERATOR_LEFT], 55 | '^' => ['precedence' => 17, 'associativity' => self::OPERATOR_LEFT], 56 | '&' => ['precedence' => 18, 'associativity' => self::OPERATOR_LEFT], 57 | '==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 58 | '===' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 59 | '!=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 60 | '!==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 61 | '<' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 62 | '>' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 63 | '>=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 64 | '<=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 65 | 'not in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 66 | 'in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 67 | 'contains' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 68 | 'starts with' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 69 | 'ends with' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 70 | 'matches' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT], 71 | '..' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], 72 | '<<' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], 73 | '>>' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT], 74 | '+' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT], 75 | '-' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT], 76 | '~' => ['precedence' => 40, 'associativity' => self::OPERATOR_LEFT], 77 | '*' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT], 78 | '/' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT], 79 | '%' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT], 80 | '**' => ['precedence' => 200, 'associativity' => self::OPERATOR_RIGHT], 81 | ]; 82 | } 83 | 84 | /** 85 | * Converts a token stream to a node tree. 86 | * 87 | * The valid names is an array where the values 88 | * are the names that the user can use in an expression. 89 | * 90 | * If the variable name in the compiled PHP code must be 91 | * different, define it as the key. 92 | * 93 | * For instance, ['this' => 'container'] means that the 94 | * variable 'container' can be used in the expression 95 | * but the compiled code will use 'this'. 96 | * 97 | * @param int-mask-of $flags 98 | * 99 | * @throws SyntaxError 100 | */ 101 | public function parse(TokenStream $stream, array $names = [], int $flags = 0): Node\Node 102 | { 103 | return $this->doParse($stream, $names, $flags); 104 | } 105 | 106 | /** 107 | * Validates the syntax of an expression. 108 | * 109 | * The syntax of the passed expression will be checked, but not parsed. 110 | * If you want to skip checking dynamic variable names, pass `Parser::IGNORE_UNKNOWN_VARIABLES` instead of the array. 111 | * 112 | * @param int-mask-of $flags 113 | * 114 | * @throws SyntaxError When the passed expression is invalid 115 | */ 116 | public function lint(TokenStream $stream, ?array $names = [], int $flags = 0): void 117 | { 118 | if (null === $names) { 119 | trigger_deprecation('symfony/expression-language', '7.1', 'Passing "null" as the second argument of "%s()" is deprecated, pass "%s::IGNORE_UNKNOWN_VARIABLES" instead as a third argument.', __METHOD__, __CLASS__); 120 | 121 | $flags |= self::IGNORE_UNKNOWN_VARIABLES; 122 | $names = []; 123 | } 124 | 125 | $this->doParse($stream, $names, $flags); 126 | } 127 | 128 | /** 129 | * @param int-mask-of $flags 130 | * 131 | * @throws SyntaxError 132 | */ 133 | private function doParse(TokenStream $stream, array $names, int $flags): Node\Node 134 | { 135 | $this->flags = $flags; 136 | $this->stream = $stream; 137 | $this->names = $names; 138 | 139 | $node = $this->parseExpression(); 140 | if (!$stream->isEOF()) { 141 | throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression()); 142 | } 143 | 144 | unset($this->stream, $this->names); 145 | 146 | return $node; 147 | } 148 | 149 | public function parseExpression(int $precedence = 0): Node\Node 150 | { 151 | $expr = $this->getPrimary(); 152 | $token = $this->stream->current; 153 | while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) { 154 | $op = $this->binaryOperators[$token->value]; 155 | $this->stream->next(); 156 | 157 | $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); 158 | $expr = new Node\BinaryNode($token->value, $expr, $expr1); 159 | 160 | $token = $this->stream->current; 161 | } 162 | 163 | if (0 === $precedence) { 164 | return $this->parseConditionalExpression($expr); 165 | } 166 | 167 | return $expr; 168 | } 169 | 170 | protected function getPrimary(): Node\Node 171 | { 172 | $token = $this->stream->current; 173 | 174 | if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) { 175 | $operator = $this->unaryOperators[$token->value]; 176 | $this->stream->next(); 177 | $expr = $this->parseExpression($operator['precedence']); 178 | 179 | return $this->parsePostfixExpression(new Node\UnaryNode($token->value, $expr)); 180 | } 181 | 182 | if ($token->test(Token::PUNCTUATION_TYPE, '(')) { 183 | $this->stream->next(); 184 | $expr = $this->parseExpression(); 185 | $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); 186 | 187 | return $this->parsePostfixExpression($expr); 188 | } 189 | 190 | return $this->parsePrimaryExpression(); 191 | } 192 | 193 | protected function parseConditionalExpression(Node\Node $expr): Node\Node 194 | { 195 | while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '??')) { 196 | $this->stream->next(); 197 | $expr2 = $this->parseExpression(); 198 | 199 | $expr = new Node\NullCoalesceNode($expr, $expr2); 200 | } 201 | 202 | while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) { 203 | $this->stream->next(); 204 | if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { 205 | $expr2 = $this->parseExpression(); 206 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) { 207 | $this->stream->next(); 208 | $expr3 = $this->parseExpression(); 209 | } else { 210 | $expr3 = new Node\ConstantNode(null); 211 | } 212 | } else { 213 | $this->stream->next(); 214 | $expr2 = $expr; 215 | $expr3 = $this->parseExpression(); 216 | } 217 | 218 | $expr = new Node\ConditionalNode($expr, $expr2, $expr3); 219 | } 220 | 221 | return $expr; 222 | } 223 | 224 | public function parsePrimaryExpression(): Node\Node 225 | { 226 | $token = $this->stream->current; 227 | switch ($token->type) { 228 | case Token::NAME_TYPE: 229 | $this->stream->next(); 230 | switch ($token->value) { 231 | case 'true': 232 | case 'TRUE': 233 | return new Node\ConstantNode(true); 234 | 235 | case 'false': 236 | case 'FALSE': 237 | return new Node\ConstantNode(false); 238 | 239 | case 'null': 240 | case 'NULL': 241 | return new Node\ConstantNode(null); 242 | 243 | default: 244 | if ('(' === $this->stream->current->value) { 245 | if (!($this->flags & self::IGNORE_UNKNOWN_FUNCTIONS) && false === isset($this->functions[$token->value])) { 246 | throw new SyntaxError(\sprintf('The function "%s" does not exist.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions)); 247 | } 248 | 249 | $node = new Node\FunctionNode($token->value, $this->parseArguments()); 250 | } else { 251 | if (!($this->flags & self::IGNORE_UNKNOWN_VARIABLES)) { 252 | if (!\in_array($token->value, $this->names, true)) { 253 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '??')) { 254 | return new Node\NullCoalescedNameNode($token->value); 255 | } 256 | 257 | throw new SyntaxError(\sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names); 258 | } 259 | 260 | // is the name used in the compiled code different 261 | // from the name used in the expression? 262 | if (\is_int($name = array_search($token->value, $this->names))) { 263 | $name = $token->value; 264 | } 265 | } else { 266 | $name = $token->value; 267 | } 268 | 269 | $node = new Node\NameNode($name); 270 | } 271 | } 272 | break; 273 | 274 | case Token::NUMBER_TYPE: 275 | case Token::STRING_TYPE: 276 | $this->stream->next(); 277 | 278 | return new Node\ConstantNode($token->value); 279 | 280 | default: 281 | if ($token->test(Token::PUNCTUATION_TYPE, '[')) { 282 | $node = $this->parseArrayExpression(); 283 | } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { 284 | $node = $this->parseHashExpression(); 285 | } else { 286 | throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->type, $token->value), $token->cursor, $this->stream->getExpression()); 287 | } 288 | } 289 | 290 | return $this->parsePostfixExpression($node); 291 | } 292 | 293 | public function parseArrayExpression(): Node\ArrayNode 294 | { 295 | $this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected'); 296 | 297 | $node = new Node\ArrayNode(); 298 | $first = true; 299 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { 300 | if (!$first) { 301 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma'); 302 | 303 | // trailing ,? 304 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) { 305 | break; 306 | } 307 | } 308 | $first = false; 309 | 310 | $node->addElement($this->parseExpression()); 311 | } 312 | $this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed'); 313 | 314 | return $node; 315 | } 316 | 317 | public function parseHashExpression(): Node\ArrayNode 318 | { 319 | $this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected'); 320 | 321 | $node = new Node\ArrayNode(); 322 | $first = true; 323 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { 324 | if (!$first) { 325 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma'); 326 | 327 | // trailing ,? 328 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) { 329 | break; 330 | } 331 | } 332 | $first = false; 333 | 334 | // a hash key can be: 335 | // 336 | // * a number -- 12 337 | // * a string -- 'a' 338 | // * a name, which is equivalent to a string -- a 339 | // * an expression, which must be enclosed in parentheses -- (1 + 2) 340 | if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) { 341 | $key = new Node\ConstantNode($this->stream->current->value); 342 | $this->stream->next(); 343 | } elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { 344 | $key = $this->parseExpression(); 345 | } else { 346 | $current = $this->stream->current; 347 | 348 | throw new SyntaxError(\sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->type, $current->value), $current->cursor, $this->stream->getExpression()); 349 | } 350 | 351 | $this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)'); 352 | $value = $this->parseExpression(); 353 | 354 | $node->addElement($value, $key); 355 | } 356 | $this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed'); 357 | 358 | return $node; 359 | } 360 | 361 | public function parsePostfixExpression(Node\Node $node): Node\GetAttrNode|Node\Node 362 | { 363 | $token = $this->stream->current; 364 | while (Token::PUNCTUATION_TYPE == $token->type) { 365 | if ('.' === $token->value || '?.' === $token->value) { 366 | $isNullSafe = '?.' === $token->value; 367 | $this->stream->next(); 368 | $token = $this->stream->current; 369 | $this->stream->next(); 370 | 371 | if ( 372 | Token::NAME_TYPE !== $token->type 373 | // Operators like "not" and "matches" are valid method or property names, 374 | // 375 | // In other words, besides NAME_TYPE, OPERATOR_TYPE could also be parsed as a property or method. 376 | // This is because operators are processed by the lexer prior to names. So "not" in "foo.not()" or "matches" in "foo.matches" will be recognized as an operator first. 377 | // But in fact, "not" and "matches" in such expressions shall be parsed as method or property names. 378 | // 379 | // And this ONLY works if the operator consists of valid characters for a property or method name. 380 | // 381 | // Other types, such as STRING_TYPE and NUMBER_TYPE, can't be parsed as property nor method names. 382 | // 383 | // As a result, if $token is NOT an operator OR $token->value is NOT a valid property or method name, an exception shall be thrown. 384 | && (Token::OPERATOR_TYPE !== $token->type || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value)) 385 | ) { 386 | throw new SyntaxError('Expected name.', $token->cursor, $this->stream->getExpression()); 387 | } 388 | 389 | $arg = new Node\ConstantNode($token->value, true, $isNullSafe); 390 | 391 | $arguments = new Node\ArgumentsNode(); 392 | if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) { 393 | $type = Node\GetAttrNode::METHOD_CALL; 394 | foreach ($this->parseArguments()->nodes as $n) { 395 | $arguments->addElement($n); 396 | } 397 | } else { 398 | $type = Node\GetAttrNode::PROPERTY_CALL; 399 | } 400 | 401 | $node = new Node\GetAttrNode($node, $arg, $arguments, $type); 402 | } elseif ('[' === $token->value) { 403 | $this->stream->next(); 404 | $arg = $this->parseExpression(); 405 | $this->stream->expect(Token::PUNCTUATION_TYPE, ']'); 406 | 407 | $node = new Node\GetAttrNode($node, $arg, new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL); 408 | } else { 409 | break; 410 | } 411 | 412 | $token = $this->stream->current; 413 | } 414 | 415 | return $node; 416 | } 417 | 418 | /** 419 | * Parses arguments. 420 | */ 421 | public function parseArguments(): Node\Node 422 | { 423 | $args = []; 424 | $this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); 425 | while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) { 426 | if ($args) { 427 | $this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); 428 | } 429 | 430 | $args[] = $this->parseExpression(); 431 | } 432 | $this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); 433 | 434 | return new Node\Node($args); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExpressionLanguage Component 2 | ============================ 3 | 4 | The ExpressionLanguage component provides an engine that can compile and 5 | evaluate expressions. An expression is a one-liner that returns a value 6 | (mostly, but not limited to, Booleans). 7 | 8 | Resources 9 | --------- 10 | 11 | * [Documentation](https://symfony.com/doc/current/components/expression_language/introduction.html) 12 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 13 | * [Report issues](https://github.com/symfony/symfony/issues) and 14 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 15 | in the [main Symfony repository](https://github.com/symfony/symfony) 16 | -------------------------------------------------------------------------------- /Resources/bin/generate_operator_regex.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | if ('cli' !== \PHP_SAPI) { 13 | throw new Exception('This script must be run from the command line.'); 14 | } 15 | 16 | $operators = ['not', '!', 'or', '||', 'xor', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**', '<<', '>>']; 17 | $operators = array_combine($operators, array_map('strlen', $operators)); 18 | arsort($operators); 19 | 20 | $regex = []; 21 | foreach ($operators as $operator => $length) { 22 | // Collisions of character operators: 23 | // - an operator that begins with a character must have a space or a parenthesis before or starting at the beginning of a string 24 | // - an operator that ends with a character must be followed by a whitespace or a parenthesis 25 | $regex[] = 26 | (ctype_alpha($operator[0]) ? '(?<=^|[\s(])' : '') 27 | .preg_quote($operator, '/') 28 | .(ctype_alpha($operator[$length - 1]) ? '(?=[\s(])' : ''); 29 | } 30 | 31 | echo '/'.implode('|', $regex).'/A'; 32 | -------------------------------------------------------------------------------- /SerializedParsedExpression.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | use Symfony\Component\ExpressionLanguage\Node\Node; 15 | 16 | /** 17 | * Represents an already serialized parsed expression. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class SerializedParsedExpression extends ParsedExpression 22 | { 23 | /** 24 | * @param string $expression An expression 25 | * @param string $nodes The serialized nodes for the expression 26 | */ 27 | public function __construct( 28 | string $expression, 29 | private string $nodes, 30 | ) { 31 | $this->expression = $expression; 32 | } 33 | 34 | public function getNodes(): Node 35 | { 36 | return unserialize($this->nodes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SyntaxError.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | class SyntaxError extends \LogicException 15 | { 16 | public function __construct(string $message, int $cursor = 0, string $expression = '', ?string $subject = null, ?array $proposals = null) 17 | { 18 | $message = \sprintf('%s around position %d', rtrim($message, '.'), $cursor); 19 | if ($expression) { 20 | $message = \sprintf('%s for expression `%s`', $message, $expression); 21 | } 22 | $message .= '.'; 23 | 24 | if (null !== $subject && null !== $proposals) { 25 | $minScore = \INF; 26 | foreach ($proposals as $proposal) { 27 | $distance = levenshtein($subject, $proposal); 28 | if ($distance < $minScore) { 29 | $guess = $proposal; 30 | $minScore = $distance; 31 | } 32 | } 33 | 34 | if (isset($guess) && $minScore < 3) { 35 | $message .= \sprintf(' Did you mean "%s"?', $guess); 36 | } 37 | } 38 | 39 | parent::__construct($message); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Token.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Represents a token. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Token 20 | { 21 | public const EOF_TYPE = 'end of expression'; 22 | public const NAME_TYPE = 'name'; 23 | public const NUMBER_TYPE = 'number'; 24 | public const STRING_TYPE = 'string'; 25 | public const OPERATOR_TYPE = 'operator'; 26 | public const PUNCTUATION_TYPE = 'punctuation'; 27 | 28 | /** 29 | * @param self::*_TYPE $type 30 | * @param int|null $cursor The cursor position in the source 31 | */ 32 | public function __construct( 33 | public string $type, 34 | public string|int|float|null $value, 35 | public ?int $cursor, 36 | ) { 37 | } 38 | 39 | /** 40 | * Returns a string representation of the token. 41 | */ 42 | public function __toString(): string 43 | { 44 | return \sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value); 45 | } 46 | 47 | /** 48 | * Tests the current token for a type and/or a value. 49 | */ 50 | public function test(string $type, ?string $value = null): bool 51 | { 52 | return $this->type === $type && (null === $value || $this->value == $value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /TokenStream.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\ExpressionLanguage; 13 | 14 | /** 15 | * Represents a token stream. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class TokenStream 20 | { 21 | public Token $current; 22 | 23 | private int $position = 0; 24 | 25 | public function __construct( 26 | private array $tokens, 27 | private string $expression = '', 28 | ) { 29 | $this->current = $tokens[0]; 30 | } 31 | 32 | /** 33 | * Returns a string representation of the token stream. 34 | */ 35 | public function __toString(): string 36 | { 37 | return implode("\n", $this->tokens); 38 | } 39 | 40 | /** 41 | * Sets the pointer to the next token and returns the old one. 42 | */ 43 | public function next(): void 44 | { 45 | ++$this->position; 46 | 47 | if (!isset($this->tokens[$this->position])) { 48 | throw new SyntaxError('Unexpected end of expression.', $this->current->cursor, $this->expression); 49 | } 50 | 51 | $this->current = $this->tokens[$this->position]; 52 | } 53 | 54 | /** 55 | * @param string|null $message The syntax error message 56 | */ 57 | public function expect(string $type, ?string $value = null, ?string $message = null): void 58 | { 59 | $token = $this->current; 60 | if (!$token->test($type, $value)) { 61 | throw new SyntaxError(\sprintf('%sUnexpected token "%s" of value "%s" ("%s" expected%s).', $message ? $message.'. ' : '', $token->type, $token->value, $type, $value ? \sprintf(' with value "%s"', $value) : ''), $token->cursor, $this->expression); 62 | } 63 | $this->next(); 64 | } 65 | 66 | /** 67 | * Checks if end of stream was reached. 68 | */ 69 | public function isEOF(): bool 70 | { 71 | return Token::EOF_TYPE === $this->current->type; 72 | } 73 | 74 | /** 75 | * @internal 76 | */ 77 | public function getExpression(): string 78 | { 79 | return $this->expression; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/expression-language", 3 | "type": "library", 4 | "description": "Provides an engine that can compile and evaluate expressions", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/cache": "^6.4|^7.0", 21 | "symfony/deprecation-contracts": "^2.5|^3", 22 | "symfony/service-contracts": "^2.5|^3" 23 | }, 24 | "autoload": { 25 | "psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" }, 26 | "exclude-from-classmap": [ 27 | "/Tests/" 28 | ] 29 | }, 30 | "minimum-stability": "dev" 31 | } 32 | --------------------------------------------------------------------------------