Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 08:02:04 +01:00
parent 4413528994
commit 1d90fbf296
6865 changed files with 1091082 additions and 0 deletions

View file

@ -0,0 +1,26 @@
CHANGELOG
=========
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,147 @@
<?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 $source;
private $functions;
public function __construct(array $functions)
{
$this->functions = $functions;
}
public function getFunction(string $name)
{
return $this->functions[$name];
}
/**
* Gets the current PHP code after compilation.
*
* @return string
*/
public function getSource()
{
return $this->source;
}
/**
* @return $this
*/
public function reset()
{
$this->source = '';
return $this;
}
/**
* Compiles a node.
*
* @return $this
*/
public function compile(Node\Node $node)
{
$node->compile($this);
return $this;
}
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)
{
$this->source .= $string;
return $this;
}
/**
* Adds a quoted string to the compiled code.
*
* @return $this
*/
public function string(string $value)
{
$this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
return $this;
}
/**
* Returns a PHP representation of a given value.
*
* @param mixed $value The value to convert
*
* @return $this
*/
public function repr($value)
{
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,37 @@
<?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.
*
* @return string
*/
public function __toString()
{
return $this->expression;
}
}

View file

@ -0,0 +1,106 @@
<?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 $name;
private $compiler;
private $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 instanceof \Closure ? $compiler : \Closure::fromCallable($compiler);
$this->evaluator = $evaluator instanceof \Closure ? $evaluator : \Closure::fromCallable($evaluator);
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return \Closure
*/
public function getCompiler()
{
return $this->compiler;
}
/**
* @return \Closure
*/
public function getEvaluator()
{
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)
*
* @return self
*
* @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)
{
$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 = function (...$args) use ($phpFunctionName) {
return sprintf('\%s(%s)', $phpFunctionName, implode(', ', $args));
};
$evaluator = function ($p, ...$args) use ($phpFunctionName) {
return $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,182 @@
<?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 $cache;
private $lexer;
private $parser;
private $compiler;
protected $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.
*
* @param Expression|string $expression The expression to compile
*
* @return string
*/
public function compile($expression, array $names = [])
{
return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource();
}
/**
* Evaluate an expression.
*
* @param Expression|string $expression The expression to compile
*
* @return mixed
*/
public function evaluate($expression, array $values = [])
{
return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values);
}
/**
* Parses an expression.
*
* @param Expression|string $expression The expression to parse
*
* @return ParsedExpression
*/
public function parse($expression, array $names)
{
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 Expression|string $expression The expression to validate
* @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, ?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
*
* @throws \LogicException when registering a function after calling evaluate(), compile() or parse()
*
* @see ExpressionFunction
*/
public function register(string $name, callable $compiler, callable $evaluator)
{
if (null !== $this->parser) {
throw new \LogicException('Registering functions after calling evaluate(), compile() or parse() is not supported.');
}
$this->functions[$name] = ['compiler' => $compiler, 'evaluator' => $evaluator];
}
public function addFunction(ExpressionFunction $function)
{
$this->register($function->getName(), $function->getCompiler(), $function->getEvaluator());
}
public function registerProvider(ExpressionFunctionProviderInterface $provider)
{
foreach ($provider->getFunctions() as $function) {
$this->addFunction($function);
}
}
protected function registerFunctions()
{
$this->addFunction(ExpressionFunction::fromPhp('constant'));
}
private function getLexer(): Lexer
{
if (null === $this->lexer) {
$this->lexer = new Lexer();
}
return $this->lexer;
}
private function getParser(): Parser
{
if (null === $this->parser) {
$this->parser = new Parser($this->functions);
}
return $this->parser;
}
private function getCompiler(): Compiler
{
if (null === $this->compiler) {
$this->compiler = new Compiler($this->functions);
}
return $this->compiler->reset();
}
}

View file

@ -0,0 +1,19 @@
Copyright (c) 2004-2023 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,101 @@
<?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.
*
* @return TokenStream
*
* @throws SyntaxError
*/
public function tokenize(string $expression)
{
$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('/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A', $expression, $match, 0, $cursor)) {
// numbers
$number = (float) $match[0]; // floats
if (preg_match('/^[0-9]+$/', $match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum
}
$tokens[] = new Token(Token::NUMBER_TYPE, $number, $cursor + 1);
$cursor += \strlen($match[0]);
} elseif (false !== strpos('([{', $expression[$cursor])) {
// opening bracket
$brackets[] = [$expression[$cursor], $cursor];
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (false !== strpos(')]}', $expression[$cursor])) {
// closing bracket
if (empty($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(])not in(?=[\s(])|\!\=\=|(?<=^|[\s(])not(?=[\s(])|(?<=^|[\s(])and(?=[\s(])|\=\=\=|\>\=|(?<=^|[\s(])or(?=[\s(])|\<\=|\*\*|\.\.|(?<=^|[\s(])in(?=[\s(])|&&|\|\||(?<=^|[\s(])matches|\=\=|\!\=|\*|~|%|\/|\>|\||\!|\^|&|\+|\<|\-/A', $expression, $match, 0, $cursor)) {
// operators
$tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1);
$cursor += \strlen($match[0]);
} elseif (false !== strpos('.,?:', $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 (!empty($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)
{
$this->compileArguments($compiler, false);
}
public function toArray()
{
$array = [];
foreach ($this->getKeyValuePairs() as $pair) {
$array[] = $pair['value'];
$array[] = ', ';
}
array_pop($array);
return $array;
}
}

View file

@ -0,0 +1,118 @@
<?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 $index;
public function __construct()
{
$this->index = -1;
}
public function addElement(Node $value, Node $key = null)
{
if (null === $key) {
$key = new ConstantNode(++$this->index);
}
array_push($this->nodes, $key, $value);
}
/**
* Compiles the node to PHP.
*/
public function compile(Compiler $compiler)
{
$compiler->raw('[');
$this->compileArguments($compiler);
$compiler->raw(']');
}
public function evaluate(array $functions, array $values)
{
$result = [];
foreach ($this->getKeyValuePairs() as $pair) {
$result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values);
}
return $result;
}
public function toArray()
{
$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()
{
$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)
{
$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,187 @@
<?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' => 'in_array',
'not in' => '!in_array',
];
public function __construct(string $operator, Node $left, Node $right)
{
parent::__construct(
['left' => $left, 'right' => $right],
['operator' => $operator]
);
}
public function compile(Compiler $compiler)
{
$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(function ($t, $m) use ($regexp, $str) { 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)
{
$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 !\in_array($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 !\in_array($left, $right);
case 'in':
return \in_array($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()
{
return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')'];
}
private function evaluateMatches(string $regexp, ?string $str): int
{
set_error_handler(function ($t, $m) use ($regexp) {
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)
{
$compiler
->raw('((')
->compile($this->nodes['expr1'])
->raw(') ? (')
->compile($this->nodes['expr2'])
->raw(') : (')
->compile($this->nodes['expr3'])
->raw('))')
;
}
public function evaluate(array $functions, array $values)
{
if ($this->nodes['expr1']->evaluate($functions, $values)) {
return $this->nodes['expr2']->evaluate($functions, $values);
}
return $this->nodes['expr3']->evaluate($functions, $values);
}
public function toArray()
{
return ['(', $this->nodes['expr1'], ' ? ', $this->nodes['expr2'], ' : ', $this->nodes['expr3'], ')'];
}
}

View file

@ -0,0 +1,81 @@
<?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
{
private $isIdentifier;
public function __construct($value, bool $isIdentifier = false)
{
$this->isIdentifier = $isIdentifier;
parent::__construct(
[],
['value' => $value]
);
}
public function compile(Compiler $compiler)
{
$compiler->repr($this->attributes['value']);
}
public function evaluate(array $functions, array $values)
{
return $this->attributes['value'];
}
public function toArray()
{
$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,67 @@
<?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)
{
$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)
{
$arguments = [$values];
foreach ($this->nodes['arguments']->nodes as $node) {
$arguments[] = $node->evaluate($functions, $values);
}
return $functions[$this->attributes['name']]['evaluator'](...$arguments);
}
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,114 @@
<?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;
public function __construct(Node $node, Node $attribute, ArrayNode $arguments, int $type)
{
parent::__construct(
['node' => $node, 'attribute' => $attribute, 'arguments' => $arguments],
['type' => $type]
);
}
public function compile(Compiler $compiler)
{
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
$compiler
->compile($this->nodes['node'])
->raw('->')
->raw($this->nodes['attribute']->attributes['value'])
;
break;
case self::METHOD_CALL:
$compiler
->compile($this->nodes['node'])
->raw('->')
->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)
{
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
$obj = $this->nodes['node']->evaluate($functions, $values);
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'];
return $obj->$property;
case self::METHOD_CALL:
$obj = $this->nodes['node']->evaluate($functions, $values);
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 (!\is_array($array) && !$array instanceof \ArrayAccess) {
throw new \RuntimeException(sprintf('Unable to get an item of non-array "%s".', $this->nodes['node']->dump()));
}
return $array[$this->nodes['attribute']->evaluate($functions, $values)];
}
}
public function toArray()
{
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'], ']'];
}
}
}

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)
{
$compiler->raw('$'.$this->attributes['name']);
}
public function evaluate(array $functions, array $values)
{
return $values[$this->attributes['name']];
}
public function toArray()
{
return [$this->attributes['name']];
}
}

View file

@ -0,0 +1,113 @@
<?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;
}
/**
* @return string
*/
public function __toString()
{
$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);
}
public function compile(Compiler $compiler)
{
foreach ($this->nodes as $node) {
$node->compile($compiler);
}
}
public function evaluate(array $functions, array $values)
{
$results = [];
foreach ($this->nodes as $node) {
$results[] = $node->evaluate($functions, $values);
}
return $results;
}
public function toArray()
{
throw new \BadMethodCallException(sprintf('Dumping a "%s" instance is not supported yet.', static::class));
}
public function dump()
{
$dump = '';
foreach ($this->toArray() as $v) {
$dump .= \is_scalar($v) ? $v : $v->dump();
}
return $dump;
}
protected function dumpString(string $value)
{
return sprintf('"%s"', addcslashes($value, "\0\t\"\\"));
}
protected function isHash(array $value)
{
$expectedKey = 0;
foreach ($value as $key => $val) {
if ($key !== $expectedKey++) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,66 @@
<?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)
{
$compiler
->raw('(')
->raw(self::OPERATORS[$this->attributes['operator']])
->compile($this->nodes['node'])
->raw(')')
;
}
public function evaluate(array $functions, array $values)
{
$value = $this->nodes['node']->evaluate($functions, $values);
switch ($this->attributes['operator']) {
case 'not':
case '!':
return !$value;
case '-':
return -$value;
}
return $value;
}
public function toArray(): array
{
return ['(', $this->attributes['operator'].' ', $this->nodes['node'], ')'];
}
}

View file

@ -0,0 +1,36 @@
<?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 $nodes;
public function __construct(string $expression, Node $nodes)
{
parent::__construct($expression);
$this->nodes = $nodes;
}
public function getNodes()
{
return $this->nodes;
}
}

View file

@ -0,0 +1,409 @@
<?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 $stream;
private $unaryOperators;
private $binaryOperators;
private $functions;
private $names;
private $lint;
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],
'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'.
*
* @return Node\Node
*
* @throws SyntaxError
*/
public function parse(TokenStream $stream, array $names = [])
{
$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());
}
$this->stream = null;
$this->names = null;
return $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;
}
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();
}
protected function parseConditionalExpression(Node\Node $expr)
{
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;
}
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);
}
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;
}
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;
}
public function parsePostfixExpression(Node\Node $node)
{
$token = $this->stream->current;
while (Token::PUNCTUATION_TYPE == $token->type) {
if ('.' === $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);
$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.
*/
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 (!empty($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,37 @@
<?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 already parsed expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class SerializedParsedExpression extends ParsedExpression
{
private $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;
}
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,63 @@
<?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 string $type The type of the token (self::*_TYPE)
* @param string|int|float|null $value The token value
* @param int $cursor The cursor position in the source
*/
public function __construct(string $type, $value, ?int $cursor)
{
$this->type = $type;
$this->value = $value;
$this->cursor = $cursor;
}
/**
* Returns a string representation of the token.
*
* @return string
*/
public function __toString()
{
return sprintf('%3d %-11s %s', $this->cursor, strtoupper($this->type), $this->value);
}
/**
* Tests the current token for a type and/or a value.
*
* @return bool
*/
public function test(string $type, string $value = null)
{
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 $tokens;
private $position = 0;
private $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.
*
* @return string
*/
public function __toString()
{
return implode("\n", $this->tokens);
}
/**
* Sets the pointer to the next token and returns the old one.
*/
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
*/
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.
*
* @return bool
*/
public function isEOF()
{
return Token::EOF_TYPE === $this->current->type;
}
/**
* @internal
*/
public function getExpression(): string
{
return $this->expression;
}
}

View file

@ -0,0 +1,30 @@
{
"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": ">=7.2.5",
"symfony/cache": "^4.4|^5.0|^6.0",
"symfony/service-contracts": "^1.1|^2|^3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}