installation de motranslator

via Composer
This commit is contained in:
2024-12-13 06:44:07 +01:00
parent 3ee1a9eaf6
commit db9f05f6f6
249 changed files with 29788 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
CHANGELOG
=========
6.3
---
* Add `enum` expression function
* Deprecate loose comparisons when using the "in" operator; normalize the array parameter
so it only has the expected types or implement loose matching in your own expression function
6.2
---
* Add support for null-coalescing syntax
6.1
---
* Add support for null-safe syntax when parsing object's methods and properties
* Add new operators: `contains`, `starts with` and `ends with`
* Support lexing numbers with the numeric literal separator `_`
* Support lexing decimals with no leading zero
5.1.0
-----
* added `lint` method to `ExpressionLanguage` class
* added `lint` method to `Parser` class
4.0.0
-----
* the first argument of the `ExpressionLanguage` constructor must be an instance
of `CacheItemPoolInterface`
* removed the `ArrayParserCache` and `ParserCacheAdapter` classes
* removed the `ParserCacheInterface`
2.6.0
-----
* Added ExpressionFunction and ExpressionFunctionProviderInterface
2.4.0
-----
* added the component

View File

@@ -0,0 +1,149 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
use Symfony\Contracts\Service\ResetInterface;
/**
* Compiles a node to PHP code.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Compiler implements ResetInterface
{
private string $source = '';
private array $functions;
public function __construct(array $functions)
{
$this->functions = $functions;
}
/**
* @return array
*/
public function getFunction(string $name)
{
return $this->functions[$name];
}
/**
* Gets the current PHP code after compilation.
*/
public function getSource(): string
{
return $this->source;
}
/**
* @return $this
*/
public function reset(): static
{
$this->source = '';
return $this;
}
/**
* Compiles a node.
*
* @return $this
*/
public function compile(Node\Node $node): static
{
$node->compile($this);
return $this;
}
/**
* @return string
*/
public function subcompile(Node\Node $node)
{
$current = $this->source;
$this->source = '';
$node->compile($this);
$source = $this->source;
$this->source = $current;
return $source;
}
/**
* Adds a raw string to the compiled code.
*
* @return $this
*/
public function raw(string $string): static
{
$this->source .= $string;
return $this;
}
/**
* Adds a quoted string to the compiled code.
*
* @return $this
*/
public function string(string $value): static
{
$this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
return $this;
}
/**
* Returns a PHP representation of a given value.
*
* @return $this
*/
public function repr(mixed $value): static
{
if (\is_int($value) || \is_float($value)) {
if (false !== $locale = setlocale(\LC_NUMERIC, 0)) {
setlocale(\LC_NUMERIC, 'C');
}
$this->raw($value);
if (false !== $locale) {
setlocale(\LC_NUMERIC, $locale);
}
} elseif (null === $value) {
$this->raw('null');
} elseif (\is_bool($value)) {
$this->raw($value ? 'true' : 'false');
} elseif (\is_array($value)) {
$this->raw('[');
$first = true;
foreach ($value as $key => $value) {
if (!$first) {
$this->raw(', ');
}
$first = false;
$this->repr($key);
$this->raw(' => ');
$this->repr($value);
}
$this->raw(']');
} else {
$this->string($value);
}
return $this;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Represents an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Expression
{
protected $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
/**
* Gets the expression.
*/
public function __toString(): string
{
return $this->expression;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Represents a function that can be used in an expression.
*
* A function is defined by two PHP callables. The callables are used
* by the language to compile and/or evaluate the function.
*
* The "compiler" function is used at compilation time and must return a
* PHP representation of the function call (it receives the function
* arguments as arguments).
*
* The "evaluator" function is used for expression evaluation and must return
* the value of the function call based on the values defined for the
* expression (it receives the values as a first argument and the function
* arguments as remaining arguments).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionFunction
{
private string $name;
private \Closure $compiler;
private \Closure $evaluator;
/**
* @param string $name The function name
* @param callable $compiler A callable able to compile the function
* @param callable $evaluator A callable able to evaluate the function
*/
public function __construct(string $name, callable $compiler, callable $evaluator)
{
$this->name = $name;
$this->compiler = $compiler(...);
$this->evaluator = $evaluator(...);
}
public function getName(): string
{
return $this->name;
}
public function getCompiler(): \Closure
{
return $this->compiler;
}
public function getEvaluator(): \Closure
{
return $this->evaluator;
}
/**
* Creates an ExpressionFunction from a PHP function name.
*
* @param string|null $expressionFunctionName The expression function name (default: same than the PHP function name)
*
* @throws \InvalidArgumentException if given PHP function name does not exist
* @throws \InvalidArgumentException if given PHP function name is in namespace
* and expression function name is not defined
*/
public static function fromPhp(string $phpFunctionName, ?string $expressionFunctionName = null): self
{
$phpFunctionName = ltrim($phpFunctionName, '\\');
if (!\function_exists($phpFunctionName)) {
throw new \InvalidArgumentException(sprintf('PHP function "%s" does not exist.', $phpFunctionName));
}
$parts = explode('\\', $phpFunctionName);
if (!$expressionFunctionName && \count($parts) > 1) {
throw new \InvalidArgumentException(sprintf('An expression function name must be defined when PHP function "%s" is namespaced.', $phpFunctionName));
}
$compiler = fn (...$args) => sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args));
$evaluator = fn ($p, ...$args) => $phpFunctionName(...$args);
return new self($expressionFunctionName ?: end($parts), $compiler, $evaluator);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExpressionFunctionProviderInterface
{
/**
* @return ExpressionFunction[]
*/
public function getFunctions();
}

View File

@@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
// Help opcache.preload discover always-needed symbols
class_exists(ParsedExpression::class);
/**
* Allows to compile and evaluate expressions written in your own DSL.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionLanguage
{
private CacheItemPoolInterface $cache;
private Lexer $lexer;
private Parser $parser;
private Compiler $compiler;
protected array $functions = [];
/**
* @param ExpressionFunctionProviderInterface[] $providers
*/
public function __construct(?CacheItemPoolInterface $cache = null, array $providers = [])
{
$this->cache = $cache ?? new ArrayAdapter();
$this->registerFunctions();
foreach ($providers as $provider) {
$this->registerProvider($provider);
}
}
/**
* Compiles an expression source code.
*/
public function compile(Expression|string $expression, array $names = []): string
{
return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource();
}
/**
* Evaluate an expression.
*/
public function evaluate(Expression|string $expression, array $values = []): mixed
{
return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values);
}
/**
* Parses an expression.
*/
public function parse(Expression|string $expression, array $names): ParsedExpression
{
if ($expression instanceof ParsedExpression) {
return $expression;
}
asort($names);
$cacheKeyItems = [];
foreach ($names as $nameKey => $name) {
$cacheKeyItems[] = \is_int($nameKey) ? $name : $nameKey.':'.$name;
}
$cacheItem = $this->cache->getItem(rawurlencode($expression.'//'.implode('|', $cacheKeyItems)));
if (null === $parsedExpression = $cacheItem->get()) {
$nodes = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names);
$parsedExpression = new ParsedExpression((string) $expression, $nodes);
$cacheItem->set($parsedExpression);
$this->cache->save($cacheItem);
}
return $parsedExpression;
}
/**
* Validates the syntax of an expression.
*
* @param array|null $names The list of acceptable variable names in the expression, or null to accept any names
*
* @throws SyntaxError When the passed expression is invalid
*/
public function lint(Expression|string $expression, ?array $names): void
{
if ($expression instanceof ParsedExpression) {
return;
}
$this->getParser()->lint($this->getLexer()->tokenize((string) $expression), $names);
}
/**
* Registers a function.
*
* @param callable $compiler A callable able to compile the function
* @param callable $evaluator A callable able to evaluate the function
*
* @return void
*
* @throws \LogicException when registering a function after calling evaluate(), compile() or parse()
*
* @see ExpressionFunction
*/
public function register(string $name, callable $compiler, callable $evaluator)
{
if (isset($this->parser)) {
throw new \LogicException('Registering functions after calling evaluate(), compile() or parse() is not supported.');
}
$this->functions[$name] = ['compiler' => $compiler, 'evaluator' => $evaluator];
}
/**
* @return void
*/
public function addFunction(ExpressionFunction $function)
{
$this->register($function->getName(), $function->getCompiler(), $function->getEvaluator());
}
/**
* @return void
*/
public function registerProvider(ExpressionFunctionProviderInterface $provider)
{
foreach ($provider->getFunctions() as $function) {
$this->addFunction($function);
}
}
/**
* @return void
*/
protected function registerFunctions()
{
$this->addFunction(ExpressionFunction::fromPhp('constant'));
$this->addFunction(new ExpressionFunction('enum',
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),
static function ($arguments, $str): \UnitEnum {
$value = \constant($str);
if (!$value instanceof \UnitEnum) {
throw new \TypeError(sprintf('The string "%s" is not the name of a valid enum case.', $str));
}
return $value;
}
));
}
private function getLexer(): Lexer
{
return $this->lexer ??= new Lexer();
}
private function getParser(): Parser
{
return $this->parser ??= new Parser($this->functions);
}
private function getCompiler(): Compiler
{
$this->compiler ??= new Compiler($this->functions);
return $this->compiler->reset();
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Lexes an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Lexer
{
/**
* Tokenizes an expression.
*
* @throws SyntaxError
*/
public function tokenize(string $expression): TokenStream
{
$expression = str_replace(["\r", "\n", "\t", "\v", "\f"], ' ', $expression);
$cursor = 0;
$tokens = [];
$brackets = [];
$end = \strlen($expression);
while ($cursor < $end) {
if (' ' == $expression[$cursor]) {
++$cursor;
continue;
}
if (preg_match('/
(?(DEFINE)(?P<LNUM>[0-9]+(_[0-9]+)*))
(?:\.(?&LNUM)|(?&LNUM)(?:\.(?!\.)(?&LNUM)?)?)(?:[eE][+-]?(?&LNUM))?/Ax',
$expression, $match, 0, $cursor)
) {
// numbers
$tokens[] = new Token(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0]), $cursor + 1);
$cursor += \strlen($match[0]);
} elseif (str_contains('([{', $expression[$cursor])) {
// opening bracket
$brackets[] = [$expression[$cursor], $cursor];
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (str_contains(')]}', $expression[$cursor])) {
// closing bracket
if (!$brackets) {
throw new SyntaxError(sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression);
}
[$expect, $cur] = array_pop($brackets);
if ($expression[$cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression);
}
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (preg_match('/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, 0, $cursor)) {
// strings
$tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1);
$cursor += \strlen($match[0]);
} 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(])and(?=[\s(])|\=\=\=|\!\=\=|(?<=^|[\s(])or(?=[\s(])|\|\||&&|\=\=|\!\=|\>\=|\<\=|(?<=^|[\s(])in(?=[\s(])|\.\.|\*\*|\!|\||\^|&|\<|\>|\+|\-|~|\*|\/|%/A', $expression, $match, 0, $cursor)) {
// operators
$tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1);
$cursor += \strlen($match[0]);
} elseif ('?' === $expression[$cursor] && '.' === ($expression[$cursor + 1] ?? '')) {
// null-safe
$tokens[] = new Token(Token::PUNCTUATION_TYPE, '?.', ++$cursor);
++$cursor;
} elseif ('?' === $expression[$cursor] && '?' === ($expression[$cursor + 1] ?? '')) {
// null-coalescing
$tokens[] = new Token(Token::PUNCTUATION_TYPE, '??', ++$cursor);
++$cursor;
} elseif (str_contains('.,?:', $expression[$cursor])) {
// punctuation
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, 0, $cursor)) {
// names
$tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1);
$cursor += \strlen($match[0]);
} else {
// unlexable
throw new SyntaxError(sprintf('Unexpected character "%s".', $expression[$cursor]), $cursor, $expression);
}
}
$tokens[] = new Token(Token::EOF_TYPE, null, $cursor + 1);
if ($brackets) {
[$expect, $cur] = array_pop($brackets);
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $cur, $expression);
}
return new TokenStream($tokens, $expression);
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ArgumentsNode extends ArrayNode
{
public function compile(Compiler $compiler): void
{
$this->compileArguments($compiler, false);
}
public function toArray(): array
{
$array = [];
foreach ($this->getKeyValuePairs() as $pair) {
$array[] = $pair['value'];
$array[] = ', ';
}
array_pop($array);
return $array;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ArrayNode extends Node
{
protected int $index;
public function __construct()
{
$this->index = -1;
}
public function addElement(Node $value, ?Node $key = null): void
{
$key ??= new ConstantNode(++$this->index);
array_push($this->nodes, $key, $value);
}
/**
* Compiles the node to PHP.
*/
public function compile(Compiler $compiler): void
{
$compiler->raw('[');
$this->compileArguments($compiler);
$compiler->raw(']');
}
public function evaluate(array $functions, array $values): array
{
$result = [];
foreach ($this->getKeyValuePairs() as $pair) {
$result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values);
}
return $result;
}
public function toArray(): array
{
$value = [];
foreach ($this->getKeyValuePairs() as $pair) {
$value[$pair['key']->attributes['value']] = $pair['value'];
}
$array = [];
if ($this->isHash($value)) {
foreach ($value as $k => $v) {
$array[] = ', ';
$array[] = new ConstantNode($k);
$array[] = ': ';
$array[] = $v;
}
$array[0] = '{';
$array[] = '}';
} else {
foreach ($value as $v) {
$array[] = ', ';
$array[] = $v;
}
$array[0] = '[';
$array[] = ']';
}
return $array;
}
protected function getKeyValuePairs(): array
{
$pairs = [];
foreach (array_chunk($this->nodes, 2) as $pair) {
$pairs[] = ['key' => $pair[0], 'value' => $pair[1]];
}
return $pairs;
}
protected function compileArguments(Compiler $compiler, bool $withKeys = true): void
{
$first = true;
foreach ($this->getKeyValuePairs() as $pair) {
if (!$first) {
$compiler->raw(', ');
}
$first = false;
if ($withKeys) {
$compiler
->compile($pair['key'])
->raw(' => ')
;
}
$compiler->compile($pair['value']);
}
}
}

View File

@@ -0,0 +1,204 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
use Symfony\Component\ExpressionLanguage\SyntaxError;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class BinaryNode extends Node
{
private const OPERATORS = [
'~' => '.',
'and' => '&&',
'or' => '||',
];
private const FUNCTIONS = [
'**' => 'pow',
'..' => 'range',
'in' => '\\'.self::class.'::inArray',
'not in' => '!\\'.self::class.'::inArray',
'contains' => 'str_contains',
'starts with' => 'str_starts_with',
'ends with' => 'str_ends_with',
];
public function __construct(string $operator, Node $left, Node $right)
{
parent::__construct(
['left' => $left, 'right' => $right],
['operator' => $operator]
);
}
public function compile(Compiler $compiler): void
{
$operator = $this->attributes['operator'];
if ('matches' == $operator) {
if ($this->nodes['right'] instanceof ConstantNode) {
$this->evaluateMatches($this->nodes['right']->evaluate([], []), '');
}
$compiler
->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(); } })(')
->compile($this->nodes['right'])
->raw(', ')
->compile($this->nodes['left'])
->raw(')')
;
return;
}
if (isset(self::FUNCTIONS[$operator])) {
$compiler
->raw(sprintf('%s(', self::FUNCTIONS[$operator]))
->compile($this->nodes['left'])
->raw(', ')
->compile($this->nodes['right'])
->raw(')')
;
return;
}
if (isset(self::OPERATORS[$operator])) {
$operator = self::OPERATORS[$operator];
}
$compiler
->raw('(')
->compile($this->nodes['left'])
->raw(' ')
->raw($operator)
->raw(' ')
->compile($this->nodes['right'])
->raw(')')
;
}
public function evaluate(array $functions, array $values): mixed
{
$operator = $this->attributes['operator'];
$left = $this->nodes['left']->evaluate($functions, $values);
if (isset(self::FUNCTIONS[$operator])) {
$right = $this->nodes['right']->evaluate($functions, $values);
if ('not in' === $operator) {
return !self::inArray($left, $right);
}
$f = self::FUNCTIONS[$operator];
return $f($left, $right);
}
switch ($operator) {
case 'or':
case '||':
return $left || $this->nodes['right']->evaluate($functions, $values);
case 'and':
case '&&':
return $left && $this->nodes['right']->evaluate($functions, $values);
}
$right = $this->nodes['right']->evaluate($functions, $values);
switch ($operator) {
case '|':
return $left | $right;
case '^':
return $left ^ $right;
case '&':
return $left & $right;
case '==':
return $left == $right;
case '===':
return $left === $right;
case '!=':
return $left != $right;
case '!==':
return $left !== $right;
case '<':
return $left < $right;
case '>':
return $left > $right;
case '>=':
return $left >= $right;
case '<=':
return $left <= $right;
case 'not in':
return !self::inArray($left, $right);
case 'in':
return self::inArray($left, $right);
case '+':
return $left + $right;
case '-':
return $left - $right;
case '~':
return $left.$right;
case '*':
return $left * $right;
case '/':
if (0 == $right) {
throw new \DivisionByZeroError('Division by zero.');
}
return $left / $right;
case '%':
if (0 == $right) {
throw new \DivisionByZeroError('Modulo by zero.');
}
return $left % $right;
case 'matches':
return $this->evaluateMatches($right, $left);
}
}
public function toArray(): array
{
return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')'];
}
/**
* @internal to be replaced by an inline strict call to in_array() in version 7.0
*/
public static function inArray($value, array $array): bool
{
if (false === $key = array_search($value, $array)) {
return false;
}
if (!\in_array($value, $array, true)) {
trigger_deprecation('symfony/expression-language', '6.3', 'The "in" operator will use strict comparisons in Symfony 7.0. Loose match found with key "%s" for value %s. Normalize the array parameter so it only has the expected types or implement loose matching in your own expression function.', $key, json_encode($value));
}
return true;
}
private function evaluateMatches(string $regexp, ?string $str): int
{
set_error_handler(static fn ($t, $m) => throw new 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();
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ConditionalNode extends Node
{
public function __construct(Node $expr1, Node $expr2, Node $expr3)
{
parent::__construct(
['expr1' => $expr1, 'expr2' => $expr2, 'expr3' => $expr3]
);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->compile($this->nodes['expr1'])
->raw(') ? (')
->compile($this->nodes['expr2'])
->raw(') : (')
->compile($this->nodes['expr3'])
->raw('))')
;
}
public function evaluate(array $functions, array $values): mixed
{
if ($this->nodes['expr1']->evaluate($functions, $values)) {
return $this->nodes['expr2']->evaluate($functions, $values);
}
return $this->nodes['expr3']->evaluate($functions, $values);
}
public function toArray(): array
{
return ['(', $this->nodes['expr1'], ' ? ', $this->nodes['expr2'], ' : ', $this->nodes['expr3'], ')'];
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ConstantNode extends Node
{
public readonly bool $isNullSafe;
private bool $isIdentifier;
public function __construct(mixed $value, bool $isIdentifier = false, bool $isNullSafe = false)
{
$this->isIdentifier = $isIdentifier;
$this->isNullSafe = $isNullSafe;
parent::__construct(
[],
['value' => $value]
);
}
public function compile(Compiler $compiler): void
{
$compiler->repr($this->attributes['value']);
}
public function evaluate(array $functions, array $values): mixed
{
return $this->attributes['value'];
}
public function toArray(): array
{
$array = [];
$value = $this->attributes['value'];
if ($this->isIdentifier) {
$array[] = $value;
} elseif (true === $value) {
$array[] = 'true';
} elseif (false === $value) {
$array[] = 'false';
} elseif (null === $value) {
$array[] = 'null';
} elseif (is_numeric($value)) {
$array[] = $value;
} elseif (!\is_array($value)) {
$array[] = $this->dumpString($value);
} elseif ($this->isHash($value)) {
foreach ($value as $k => $v) {
$array[] = ', ';
$array[] = new self($k);
$array[] = ': ';
$array[] = new self($v);
}
$array[0] = '{';
$array[] = '}';
} else {
foreach ($value as $v) {
$array[] = ', ';
$array[] = new self($v);
}
$array[0] = '[';
$array[] = ']';
}
return $array;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class FunctionNode extends Node
{
public function __construct(string $name, Node $arguments)
{
parent::__construct(
['arguments' => $arguments],
['name' => $name]
);
}
public function compile(Compiler $compiler): void
{
$arguments = [];
foreach ($this->nodes['arguments']->nodes as $node) {
$arguments[] = $compiler->subcompile($node);
}
$function = $compiler->getFunction($this->attributes['name']);
$compiler->raw($function['compiler'](...$arguments));
}
public function evaluate(array $functions, array $values): mixed
{
$arguments = [$values];
foreach ($this->nodes['arguments']->nodes as $node) {
$arguments[] = $node->evaluate($functions, $values);
}
return $functions[$this->attributes['name']]['evaluator'](...$arguments);
}
/**
* @return array
*/
public function toArray()
{
$array = [];
$array[] = $this->attributes['name'];
foreach ($this->nodes['arguments']->nodes as $node) {
$array[] = ', ';
$array[] = $node;
}
$array[1] = '(';
$array[] = ')';
return $array;
}
}

View File

@@ -0,0 +1,166 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class GetAttrNode extends Node
{
public const PROPERTY_CALL = 1;
public const METHOD_CALL = 2;
public const ARRAY_CALL = 3;
/**
* @param self::* $type
*/
public function __construct(Node $node, Node $attribute, ArrayNode $arguments, int $type)
{
parent::__construct(
['node' => $node, 'attribute' => $attribute, 'arguments' => $arguments],
['type' => $type, 'is_null_coalesce' => false, 'is_short_circuited' => false],
);
}
public function compile(Compiler $compiler): void
{
$nullSafe = $this->nodes['attribute'] instanceof ConstantNode && $this->nodes['attribute']->isNullSafe;
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
$compiler
->compile($this->nodes['node'])
->raw($nullSafe ? '?->' : '->')
->raw($this->nodes['attribute']->attributes['value'])
;
break;
case self::METHOD_CALL:
$compiler
->compile($this->nodes['node'])
->raw($nullSafe ? '?->' : '->')
->raw($this->nodes['attribute']->attributes['value'])
->raw('(')
->compile($this->nodes['arguments'])
->raw(')')
;
break;
case self::ARRAY_CALL:
$compiler
->compile($this->nodes['node'])
->raw('[')
->compile($this->nodes['attribute'])->raw(']')
;
break;
}
}
public function evaluate(array $functions, array $values): mixed
{
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
$obj = $this->nodes['node']->evaluate($functions, $values);
if (null === $obj && ($this->nodes['attribute']->isNullSafe || $this->attributes['is_null_coalesce'])) {
$this->attributes['is_short_circuited'] = true;
return null;
}
if (null === $obj && $this->isShortCircuited()) {
return null;
}
if (!\is_object($obj)) {
throw new \RuntimeException(sprintf('Unable to get property "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
}
$property = $this->nodes['attribute']->attributes['value'];
if ($this->attributes['is_null_coalesce']) {
return $obj->$property ?? null;
}
return $obj->$property;
case self::METHOD_CALL:
$obj = $this->nodes['node']->evaluate($functions, $values);
if (null === $obj && $this->nodes['attribute']->isNullSafe) {
$this->attributes['is_short_circuited'] = true;
return null;
}
if (null === $obj && $this->isShortCircuited()) {
return null;
}
if (!\is_object($obj)) {
throw new \RuntimeException(sprintf('Unable to call method "%s" of non-object "%s".', $this->nodes['attribute']->dump(), $this->nodes['node']->dump()));
}
if (!\is_callable($toCall = [$obj, $this->nodes['attribute']->attributes['value']])) {
throw new \RuntimeException(sprintf('Unable to call method "%s" of object "%s".', $this->nodes['attribute']->attributes['value'], get_debug_type($obj)));
}
return $toCall(...array_values($this->nodes['arguments']->evaluate($functions, $values)));
case self::ARRAY_CALL:
$array = $this->nodes['node']->evaluate($functions, $values);
if (null === $array && $this->isShortCircuited()) {
return null;
}
if (!\is_array($array) && !$array instanceof \ArrayAccess && !(null === $array && $this->attributes['is_null_coalesce'])) {
throw new \RuntimeException(sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump()));
}
if ($this->attributes['is_null_coalesce']) {
return $array[$this->nodes['attribute']->evaluate($functions, $values)] ?? null;
}
return $array[$this->nodes['attribute']->evaluate($functions, $values)];
}
}
private function isShortCircuited(): bool
{
return $this->attributes['is_short_circuited'] || ($this->nodes['node'] instanceof self && $this->nodes['node']->isShortCircuited());
}
public function toArray(): array
{
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
return [$this->nodes['node'], '.', $this->nodes['attribute']];
case self::METHOD_CALL:
return [$this->nodes['node'], '.', $this->nodes['attribute'], '(', $this->nodes['arguments'], ')'];
case self::ARRAY_CALL:
return [$this->nodes['node'], '[', $this->nodes['attribute'], ']'];
}
}
/**
* Provides BC with instances serialized before v6.2.
*/
public function __unserialize(array $data): void
{
$this->nodes = $data['nodes'];
$this->attributes = $data['attributes'];
$this->attributes['is_null_coalesce'] ??= false;
$this->attributes['is_short_circuited'] ??= $data["\x00Symfony\Component\ExpressionLanguage\Node\GetAttrNode\x00isShortCircuited"] ?? false;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class NameNode extends Node
{
public function __construct(string $name)
{
parent::__construct(
[],
['name' => $name]
);
}
public function compile(Compiler $compiler): void
{
$compiler->raw('$'.$this->attributes['name']);
}
public function evaluate(array $functions, array $values): mixed
{
return $values[$this->attributes['name']];
}
public function toArray(): array
{
return [$this->attributes['name']];
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* Represents a node in the AST.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Node
{
public $nodes = [];
public $attributes = [];
/**
* @param array $nodes An array of nodes
* @param array $attributes An array of attributes
*/
public function __construct(array $nodes = [], array $attributes = [])
{
$this->nodes = $nodes;
$this->attributes = $attributes;
}
public function __toString(): string
{
$attributes = [];
foreach ($this->attributes as $name => $value) {
$attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
}
$repr = [str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', static::class).'('.implode(', ', $attributes)];
if (\count($this->nodes)) {
foreach ($this->nodes as $node) {
foreach (explode("\n", (string) $node) as $line) {
$repr[] = ' '.$line;
}
}
$repr[] = ')';
} else {
$repr[0] .= ')';
}
return implode("\n", $repr);
}
/**
* @return void
*/
public function compile(Compiler $compiler)
{
foreach ($this->nodes as $node) {
$node->compile($compiler);
}
}
/**
* @return mixed
*/
public function evaluate(array $functions, array $values)
{
$results = [];
foreach ($this->nodes as $node) {
$results[] = $node->evaluate($functions, $values);
}
return $results;
}
/**
* @return array
*
* @throws \BadMethodCallException when this node cannot be transformed to an array
*/
public function toArray()
{
throw new \BadMethodCallException(sprintf('Dumping a "%s" instance is not supported yet.', static::class));
}
/**
* @return string
*/
public function dump()
{
$dump = '';
foreach ($this->toArray() as $v) {
$dump .= \is_scalar($v) ? $v : $v->dump();
}
return $dump;
}
/**
* @return string
*/
protected function dumpString(string $value)
{
return sprintf('"%s"', addcslashes($value, "\0\t\"\\"));
}
/**
* @return bool
*/
protected function isHash(array $value)
{
$expectedKey = 0;
foreach ($value as $key => $val) {
if ($key !== $expectedKey++) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class NullCoalesceNode extends Node
{
public function __construct(Node $expr1, Node $expr2)
{
parent::__construct(['expr1' => $expr1, 'expr2' => $expr2]);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('((')
->compile($this->nodes['expr1'])
->raw(') ?? (')
->compile($this->nodes['expr2'])
->raw('))')
;
}
public function evaluate(array $functions, array $values): mixed
{
if ($this->nodes['expr1'] instanceof GetAttrNode) {
$this->addNullCoalesceAttributeToGetAttrNodes($this->nodes['expr1']);
}
return $this->nodes['expr1']->evaluate($functions, $values) ?? $this->nodes['expr2']->evaluate($functions, $values);
}
public function toArray(): array
{
return ['(', $this->nodes['expr1'], ') ?? (', $this->nodes['expr2'], ')'];
}
private function addNullCoalesceAttributeToGetAttrNodes(Node $node): void
{
if (!$node instanceof GetAttrNode) {
return;
}
$node->attributes['is_null_coalesce'] = true;
foreach ($node->nodes as $node) {
$this->addNullCoalesceAttributeToGetAttrNodes($node);
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class UnaryNode extends Node
{
private const OPERATORS = [
'!' => '!',
'not' => '!',
'+' => '+',
'-' => '-',
];
public function __construct(string $operator, Node $node)
{
parent::__construct(
['node' => $node],
['operator' => $operator]
);
}
public function compile(Compiler $compiler): void
{
$compiler
->raw('(')
->raw(self::OPERATORS[$this->attributes['operator']])
->compile($this->nodes['node'])
->raw(')')
;
}
public function evaluate(array $functions, array $values): mixed
{
$value = $this->nodes['node']->evaluate($functions, $values);
return match ($this->attributes['operator']) {
'not',
'!' => !$value,
'-' => -$value,
default => $value,
};
}
public function toArray(): array
{
return ['(', $this->attributes['operator'].' ', $this->nodes['node'], ')'];
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Node\Node;
/**
* Represents an already parsed expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParsedExpression extends Expression
{
private Node $nodes;
public function __construct(string $expression, Node $nodes)
{
parent::__construct($expression);
$this->nodes = $nodes;
}
/**
* @return Node
*/
public function getNodes()
{
return $this->nodes;
}
}

View File

@@ -0,0 +1,439 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Parsers a token stream.
*
* This parser implements a "Precedence climbing" algorithm.
*
* @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
* @see http://en.wikipedia.org/wiki/Operator-precedence_parser
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Parser
{
public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2;
private TokenStream $stream;
private array $unaryOperators;
private array $binaryOperators;
private array $functions;
private ?array $names;
private bool $lint = false;
public function __construct(array $functions)
{
$this->functions = $functions;
$this->unaryOperators = [
'not' => ['precedence' => 50],
'!' => ['precedence' => 50],
'-' => ['precedence' => 500],
'+' => ['precedence' => 500],
];
$this->binaryOperators = [
'or' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT],
'||' => ['precedence' => 10, 'associativity' => self::OPERATOR_LEFT],
'and' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT],
'&&' => ['precedence' => 15, 'associativity' => self::OPERATOR_LEFT],
'|' => ['precedence' => 16, 'associativity' => self::OPERATOR_LEFT],
'^' => ['precedence' => 17, 'associativity' => self::OPERATOR_LEFT],
'&' => ['precedence' => 18, 'associativity' => self::OPERATOR_LEFT],
'==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'===' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'!=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'!==' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'<' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'>' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'>=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'<=' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'not in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'in' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'contains' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'starts with' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'ends with' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'matches' => ['precedence' => 20, 'associativity' => self::OPERATOR_LEFT],
'..' => ['precedence' => 25, 'associativity' => self::OPERATOR_LEFT],
'+' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT],
'-' => ['precedence' => 30, 'associativity' => self::OPERATOR_LEFT],
'~' => ['precedence' => 40, 'associativity' => self::OPERATOR_LEFT],
'*' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
'/' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
'%' => ['precedence' => 60, 'associativity' => self::OPERATOR_LEFT],
'**' => ['precedence' => 200, 'associativity' => self::OPERATOR_RIGHT],
];
}
/**
* Converts a token stream to a node tree.
*
* The valid names is an array where the values
* are the names that the user can use in an expression.
*
* If the variable name in the compiled PHP code must be
* different, define it as the key.
*
* For instance, ['this' => 'container'] means that the
* variable 'container' can be used in the expression
* but the compiled code will use 'this'.
*
* @throws SyntaxError
*/
public function parse(TokenStream $stream, array $names = []): Node\Node
{
$this->lint = false;
return $this->doParse($stream, $names);
}
/**
* Validates the syntax of an expression.
*
* The syntax of the passed expression will be checked, but not parsed.
* If you want to skip checking dynamic variable names, pass `null` instead of the array.
*
* @throws SyntaxError When the passed expression is invalid
*/
public function lint(TokenStream $stream, ?array $names = []): void
{
$this->lint = true;
$this->doParse($stream, $names);
}
/**
* @throws SyntaxError
*/
private function doParse(TokenStream $stream, ?array $names = []): Node\Node
{
$this->stream = $stream;
$this->names = $names;
$node = $this->parseExpression();
if (!$stream->isEOF()) {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $stream->current->type, $stream->current->value), $stream->current->cursor, $stream->getExpression());
}
unset($this->stream, $this->names);
return $node;
}
/**
* @return Node\Node
*/
public function parseExpression(int $precedence = 0)
{
$expr = $this->getPrimary();
$token = $this->stream->current;
while ($token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->value]) && $this->binaryOperators[$token->value]['precedence'] >= $precedence) {
$op = $this->binaryOperators[$token->value];
$this->stream->next();
$expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
$expr = new Node\BinaryNode($token->value, $expr, $expr1);
$token = $this->stream->current;
}
if (0 === $precedence) {
return $this->parseConditionalExpression($expr);
}
return $expr;
}
/**
* @return Node\Node
*/
protected function getPrimary()
{
$token = $this->stream->current;
if ($token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->value])) {
$operator = $this->unaryOperators[$token->value];
$this->stream->next();
$expr = $this->parseExpression($operator['precedence']);
return $this->parsePostfixExpression(new Node\UnaryNode($token->value, $expr));
}
if ($token->test(Token::PUNCTUATION_TYPE, '(')) {
$this->stream->next();
$expr = $this->parseExpression();
$this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
return $this->parsePostfixExpression($expr);
}
return $this->parsePrimaryExpression();
}
/**
* @return Node\Node
*/
protected function parseConditionalExpression(Node\Node $expr)
{
while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '??')) {
$this->stream->next();
$expr2 = $this->parseExpression();
$expr = new Node\NullCoalesceNode($expr, $expr2);
}
while ($this->stream->current->test(Token::PUNCTUATION_TYPE, '?')) {
$this->stream->next();
if (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) {
$expr2 = $this->parseExpression();
if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ':')) {
$this->stream->next();
$expr3 = $this->parseExpression();
} else {
$expr3 = new Node\ConstantNode(null);
}
} else {
$this->stream->next();
$expr2 = $expr;
$expr3 = $this->parseExpression();
}
$expr = new Node\ConditionalNode($expr, $expr2, $expr3);
}
return $expr;
}
/**
* @return Node\Node
*/
public function parsePrimaryExpression()
{
$token = $this->stream->current;
switch ($token->type) {
case Token::NAME_TYPE:
$this->stream->next();
switch ($token->value) {
case 'true':
case 'TRUE':
return new Node\ConstantNode(true);
case 'false':
case 'FALSE':
return new Node\ConstantNode(false);
case 'null':
case 'NULL':
return new Node\ConstantNode(null);
default:
if ('(' === $this->stream->current->value) {
if (false === isset($this->functions[$token->value])) {
throw new SyntaxError(sprintf('The function "%s" does not exist.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, array_keys($this->functions));
}
$node = new Node\FunctionNode($token->value, $this->parseArguments());
} else {
if (!$this->lint || \is_array($this->names)) {
if (!\in_array($token->value, $this->names, true)) {
throw new SyntaxError(sprintf('Variable "%s" is not valid.', $token->value), $token->cursor, $this->stream->getExpression(), $token->value, $this->names);
}
// is the name used in the compiled code different
// from the name used in the expression?
if (\is_int($name = array_search($token->value, $this->names))) {
$name = $token->value;
}
} else {
$name = $token->value;
}
$node = new Node\NameNode($name);
}
}
break;
case Token::NUMBER_TYPE:
case Token::STRING_TYPE:
$this->stream->next();
return new Node\ConstantNode($token->value);
default:
if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
$node = $this->parseArrayExpression();
} elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseHashExpression();
} else {
throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', $token->type, $token->value), $token->cursor, $this->stream->getExpression());
}
}
return $this->parsePostfixExpression($node);
}
/**
* @return Node\ArrayNode
*/
public function parseArrayExpression()
{
$this->stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
$node = new Node\ArrayNode();
$first = true;
while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) {
if (!$first) {
$this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
// trailing ,?
if ($this->stream->current->test(Token::PUNCTUATION_TYPE, ']')) {
break;
}
}
$first = false;
$node->addElement($this->parseExpression());
}
$this->stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
return $node;
}
/**
* @return Node\ArrayNode
*/
public function parseHashExpression()
{
$this->stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
$node = new Node\ArrayNode();
$first = true;
while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) {
$this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
// trailing ,?
if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '}')) {
break;
}
}
$first = false;
// a hash key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
if ($this->stream->current->test(Token::STRING_TYPE) || $this->stream->current->test(Token::NAME_TYPE) || $this->stream->current->test(Token::NUMBER_TYPE)) {
$key = new Node\ConstantNode($this->stream->current->value);
$this->stream->next();
} elseif ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) {
$key = $this->parseExpression();
} else {
$current = $this->stream->current;
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());
}
$this->stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
$value = $this->parseExpression();
$node->addElement($value, $key);
}
$this->stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
return $node;
}
/**
* @return Node\GetAttrNode|Node\Node
*/
public function parsePostfixExpression(Node\Node $node)
{
$token = $this->stream->current;
while (Token::PUNCTUATION_TYPE == $token->type) {
if ('.' === $token->value || '?.' === $token->value) {
$isNullSafe = '?.' === $token->value;
$this->stream->next();
$token = $this->stream->current;
$this->stream->next();
if (
Token::NAME_TYPE !== $token->type
// Operators like "not" and "matches" are valid method or property names,
//
// In other words, besides NAME_TYPE, OPERATOR_TYPE could also be parsed as a property or method.
// 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.
// But in fact, "not" and "matches" in such expressions shall be parsed as method or property names.
//
// And this ONLY works if the operator consists of valid characters for a property or method name.
//
// Other types, such as STRING_TYPE and NUMBER_TYPE, can't be parsed as property nor method names.
//
// 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.
&& (Token::OPERATOR_TYPE !== $token->type || !preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value))
) {
throw new SyntaxError('Expected name.', $token->cursor, $this->stream->getExpression());
}
$arg = new Node\ConstantNode($token->value, true, $isNullSafe);
$arguments = new Node\ArgumentsNode();
if ($this->stream->current->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Node\GetAttrNode::METHOD_CALL;
foreach ($this->parseArguments()->nodes as $n) {
$arguments->addElement($n);
}
} else {
$type = Node\GetAttrNode::PROPERTY_CALL;
}
$node = new Node\GetAttrNode($node, $arg, $arguments, $type);
} elseif ('[' === $token->value) {
$this->stream->next();
$arg = $this->parseExpression();
$this->stream->expect(Token::PUNCTUATION_TYPE, ']');
$node = new Node\GetAttrNode($node, $arg, new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL);
} else {
break;
}
$token = $this->stream->current;
}
return $node;
}
/**
* Parses arguments.
*
* @return Node\Node
*/
public function parseArguments()
{
$args = [];
$this->stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$this->stream->current->test(Token::PUNCTUATION_TYPE, ')')) {
if ($args) {
$this->stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
}
$args[] = $this->parseExpression();
}
$this->stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
return new Node\Node($args);
}
}

View File

@@ -0,0 +1,15 @@
ExpressionLanguage Component
============================
The ExpressionLanguage component provides an engine that can compile and
evaluate expressions. An expression is a one-liner that returns a value
(mostly, but not limited to, Booleans).
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/expression_language/introduction.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if ('cli' !== \PHP_SAPI) {
throw new Exception('This script must be run from the command line.');
}
$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', 'contains', 'starts with', 'ends with', 'matches', '**'];
$operators = array_combine($operators, array_map('strlen', $operators));
arsort($operators);
$regex = [];
foreach ($operators as $operator => $length) {
// Collisions of character operators:
// - an operator that begins with a character must have a space or a parenthesis before or starting at the beginning of a string
// - an operator that ends with a character must be followed by a whitespace or a parenthesis
$regex[] =
(ctype_alpha($operator[0]) ? '(?<=^|[\s(])' : '')
.preg_quote($operator, '/')
.(ctype_alpha($operator[$length - 1]) ? '(?=[\s(])' : '');
}
echo '/'.implode('|', $regex).'/A';

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\Node\Node;
/**
* Represents an already parsed expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class SerializedParsedExpression extends ParsedExpression
{
private string $nodes;
/**
* @param string $expression An expression
* @param string $nodes The serialized nodes for the expression
*/
public function __construct(string $expression, string $nodes)
{
$this->expression = $expression;
$this->nodes = $nodes;
}
/**
* @return Node
*/
public function getNodes()
{
return unserialize($this->nodes);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
class SyntaxError extends \LogicException
{
public function __construct(string $message, int $cursor = 0, string $expression = '', ?string $subject = null, ?array $proposals = null)
{
$message = sprintf('%s around position %d', rtrim($message, '.'), $cursor);
if ($expression) {
$message = sprintf('%s for expression `%s`', $message, $expression);
}
$message .= '.';
if (null !== $subject && null !== $proposals) {
$minScore = \INF;
foreach ($proposals as $proposal) {
$distance = levenshtein($subject, $proposal);
if ($distance < $minScore) {
$guess = $proposal;
$minScore = $distance;
}
}
if (isset($guess) && $minScore < 3) {
$message .= sprintf(' Did you mean "%s"?', $guess);
}
}
parent::__construct($message);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Represents a Token.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Token
{
public $value;
public $type;
public $cursor;
public const EOF_TYPE = 'end of expression';
public const NAME_TYPE = 'name';
public const NUMBER_TYPE = 'number';
public const STRING_TYPE = 'string';
public const OPERATOR_TYPE = 'operator';
public const PUNCTUATION_TYPE = 'punctuation';
/**
* @param self::*_TYPE $type
* @param int|null $cursor The cursor position in the source
*/
public function __construct(string $type, string|int|float|null $value, ?int $cursor)
{
$this->type = $type;
$this->value = $value;
$this->cursor = $cursor;
}
/**
* Returns a string representation of the token.
*/
public function __toString(): string
{
return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value);
}
/**
* Tests the current token for a type and/or a value.
*/
public function test(string $type, ?string $value = null): bool
{
return $this->type === $type && (null === $value || $this->value == $value);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Represents a token stream.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TokenStream
{
public $current;
private array $tokens;
private int $position = 0;
private string $expression;
public function __construct(array $tokens, string $expression = '')
{
$this->tokens = $tokens;
$this->current = $tokens[0];
$this->expression = $expression;
}
/**
* Returns a string representation of the token stream.
*/
public function __toString(): string
{
return implode("\n", $this->tokens);
}
/**
* Sets the pointer to the next token and returns the old one.
*
* @return void
*/
public function next()
{
++$this->position;
if (!isset($this->tokens[$this->position])) {
throw new SyntaxError('Unexpected end of expression.', $this->current->cursor, $this->expression);
}
$this->current = $this->tokens[$this->position];
}
/**
* @param string|null $message The syntax error message
*
* @return void
*/
public function expect(string $type, ?string $value = null, ?string $message = null)
{
$token = $this->current;
if (!$token->test($type, $value)) {
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);
}
$this->next();
}
/**
* Checks if end of stream was reached.
*/
public function isEOF(): bool
{
return Token::EOF_TYPE === $this->current->type;
}
/**
* @internal
*/
public function getExpression(): string
{
return $this->expression;
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "symfony/expression-language",
"type": "library",
"description": "Provides an engine that can compile and evaluate expressions",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}