Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -0,0 +1,12 @@
CHANGELOG
=========
2.6.0
-----
* Added ExpressionFunction and ExpressionFunctionProviderInterface
2.4.0
-----
* added the component

View File

@@ -0,0 +1,146 @@
<?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;
/**
* Compiles a node to PHP code.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Compiler
{
private $source;
private $functions;
public function __construct(array $functions)
{
$this->functions = $functions;
}
public function getFunction($name)
{
return $this->functions[$name];
}
/**
* Gets the current PHP code after compilation.
*
* @return string The PHP code
*/
public function getSource()
{
return $this->source;
}
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.
*
* @param string $string The string
*
* @return $this
*/
public function raw($string)
{
$this->source .= $string;
return $this;
}
/**
* Adds a quoted string to the compiled code.
*
* @param string $value The string
*
* @return $this
*/
public function 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,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;
/**
* Represents an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Expression
{
protected $expression;
/**
* @param string $expression An expression
*/
public function __construct($expression)
{
$this->expression = (string) $expression;
}
/**
* Gets the expression.
*
* @return string The expression
*/
public function __toString()
{
return $this->expression;
}
}

View File

@@ -0,0 +1,100 @@
<?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($name, callable $compiler, callable $evaluator)
{
$this->name = $name;
$this->compiler = $compiler;
$this->evaluator = $evaluator;
}
public function getName()
{
return $this->name;
}
public function getCompiler()
{
return $this->compiler;
}
public function getEvaluator()
{
return $this->evaluator;
}
/**
* Creates an ExpressionFunction from a PHP function name.
*
* @param string $phpFunctionName The 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($phpFunctionName, $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 () use ($phpFunctionName) {
return sprintf('\%s(%s)', $phpFunctionName, implode(', ', \func_get_args()));
};
$evaluator = function () use ($phpFunctionName) {
$args = \func_get_args();
return \call_user_func_array($phpFunctionName, array_splice($args, 1));
};
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[] An array of Function instances
*/
public function getFunctions();
}

View File

@@ -0,0 +1,178 @@
<?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;
use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheAdapter;
use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface;
/**
* 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 CacheItemPoolInterface $cache
* @param ExpressionFunctionProviderInterface[] $providers
*/
public function __construct($cache = null, array $providers = [])
{
if (null !== $cache) {
if ($cache instanceof ParserCacheInterface) {
@trigger_error(sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of 3.2 and will be removed in 4.0. Pass an instance of %s instead.', ParserCacheInterface::class, self::class, CacheItemPoolInterface::class), \E_USER_DEPRECATED);
$cache = new ParserCacheAdapter($cache);
} elseif (!$cache instanceof CacheItemPoolInterface) {
throw new \InvalidArgumentException(sprintf('Cache argument has to implement "%s".', CacheItemPoolInterface::class));
}
}
$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
* @param array $names An array of valid names
*
* @return string The compiled PHP source code
*/
public function compile($expression, $names = [])
{
return $this->getCompiler()->compile($this->parse($expression, $names)->getNodes())->getSource();
}
/**
* Evaluate an expression.
*
* @param Expression|string $expression The expression to compile
* @param array $values An array of values
*
* @return mixed The result of the evaluation of the expression
*/
public function evaluate($expression, $values = [])
{
return $this->parse($expression, array_keys($values))->getNodes()->evaluate($this->functions, $values);
}
/**
* Parses an expression.
*
* @param Expression|string $expression The expression to parse
* @param array $names An array of valid names
*
* @return ParsedExpression A ParsedExpression instance
*/
public function parse($expression, $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;
}
/**
* Registers a function.
*
* @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
*
* @throws \LogicException when registering a function after calling evaluate(), compile() or parse()
*
* @see ExpressionFunction
*/
public function register($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()
{
if (null === $this->lexer) {
$this->lexer = new Lexer();
}
return $this->lexer;
}
private function getParser()
{
if (null === $this->parser) {
$this->parser = new Parser($this->functions);
}
return $this->parser;
}
private function getCompiler()
{
if (null === $this->compiler) {
$this->compiler = new Compiler($this->functions);
}
return $this->compiler->reset();
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2020 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,103 @@
<?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.
*
* @param string $expression The expression to tokenize
*
* @return TokenStream A token stream instance
*
* @throws SyntaxError
*/
public function tokenize($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]+)?/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);
}
list($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)) {
list($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($functions, $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, $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,170 @@
<?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 BinaryNode extends Node
{
private static $operators = [
'~' => '.',
'and' => '&&',
'or' => '||',
];
private static $functions = [
'**' => 'pow',
'..' => 'range',
'in' => 'in_array',
'not in' => '!in_array',
];
public function __construct($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) {
$compiler
->raw('preg_match(')
->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($functions, $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 preg_match($right, $left);
}
}
public function toArray()
{
return ['(', $this->nodes['left'], ' '.$this->attributes['operator'].' ', $this->nodes['right'], ')'];
}
}

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($functions, $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, $isIdentifier = false)
{
$this->isIdentifier = $isIdentifier;
parent::__construct(
[],
['value' => $value]
);
}
public function compile(Compiler $compiler)
{
$compiler->repr($this->attributes['value']);
}
public function evaluate($functions, $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($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(\call_user_func_array($function['compiler'], $arguments));
}
public function evaluate($functions, $values)
{
$arguments = [$values];
foreach ($this->nodes['arguments']->nodes as $node) {
$arguments[] = $node->evaluate($functions, $values);
}
return \call_user_func_array($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,120 @@
<?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
{
const PROPERTY_CALL = 1;
const METHOD_CALL = 2;
const ARRAY_CALL = 3;
public function __construct(Node $node, Node $attribute, ArrayNode $arguments, $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($functions, $values)
{
switch ($this->attributes['type']) {
case self::PROPERTY_CALL:
$obj = $this->nodes['node']->evaluate($functions, $values);
if (!\is_object($obj)) {
throw new \RuntimeException('Unable to get a property on a non-object.');
}
$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('Unable to get a property on a non-object.');
}
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_class($obj)));
}
$arguments = $this->nodes['arguments']->evaluate($functions, $values);
if (\PHP_VERSION_ID >= 80000) {
$arguments = array_values($arguments);
}
return \call_user_func_array($toCall, $arguments);
case self::ARRAY_CALL:
$array = $this->nodes['node']->evaluate($functions, $values);
if (!\is_array($array) && !$array instanceof \ArrayAccess) {
throw new \RuntimeException('Unable to get an item on a non-array.');
}
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($name)
{
parent::__construct(
[],
['name' => $name]
);
}
public function compile(Compiler $compiler)
{
$compiler->raw('$'.$this->attributes['name']);
}
public function evaluate($functions, $values)
{
return $values[$this->attributes['name']];
}
public function toArray()
{
return [$this->attributes['name']];
}
}

View File

@@ -0,0 +1,110 @@
<?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()
{
$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($functions, $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($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 static $operators = [
'!' => '!',
'not' => '!',
'+' => '+',
'-' => '-',
];
public function __construct($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($functions, $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()
{
return ['(', $this->attributes['operator'].' ', $this->nodes['node'], ')'];
}
}

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;
use Symfony\Component\ExpressionLanguage\Node\Node;
/**
* Represents an already parsed expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParsedExpression extends Expression
{
private $nodes;
/**
* @param string $expression An expression
* @param Node $nodes A Node representing the expression
*/
public function __construct($expression, Node $nodes)
{
parent::__construct($expression);
$this->nodes = $nodes;
}
public function getNodes()
{
return $this->nodes;
}
}

View File

@@ -0,0 +1,380 @@
<?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
{
const OPERATOR_LEFT = 1;
const OPERATOR_RIGHT = 2;
private $stream;
private $unaryOperators;
private $binaryOperators;
private $functions;
private $names;
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'.
*
* @param TokenStream $stream A token stream instance
* @param array $names An array of valid names
*
* @return Node\Node A node tree
*
* @throws SyntaxError
*/
public function parse(TokenStream $stream, $names = [])
{
$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());
}
return $node;
}
public function parseExpression($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($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 (!\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;
}
$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)
{
$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,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\ParserCache;
@trigger_error('The '.__NAMESPACE__.'\ArrayParserCache class is deprecated since Symfony 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead.', \E_USER_DEPRECATED);
use Symfony\Component\ExpressionLanguage\ParsedExpression;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*
* @deprecated ArrayParserCache class is deprecated since version 3.2 and will be removed in 4.0. Use the Symfony\Component\Cache\Adapter\ArrayAdapter class instead.
*/
class ArrayParserCache implements ParserCacheInterface
{
private $cache = [];
/**
* {@inheritdoc}
*/
public function fetch($key)
{
return isset($this->cache[$key]) ? $this->cache[$key] : null;
}
/**
* {@inheritdoc}
*/
public function save($key, ParsedExpression $expression)
{
$this->cache[$key] = $expression;
}
}

View File

@@ -0,0 +1,120 @@
<?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\ParserCache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
/**
* @author Alexandre GESLIN <alexandre@gesl.in>
*
* @internal and will be removed in Symfony 4.0.
*/
class ParserCacheAdapter implements CacheItemPoolInterface
{
private $pool;
private $createCacheItem;
public function __construct(ParserCacheInterface $pool)
{
$this->pool = $pool;
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
return $item;
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$value = $this->pool->fetch($key);
$f = $this->createCacheItem;
return $f($key, $value, null !== $value);
}
/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item)
{
$this->pool->save($item->getKey(), $item->get());
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function hasItem($key)
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function clear()
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function deleteItem($key)
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys)
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item)
{
throw new \BadMethodCallException('Not implemented.');
}
/**
* {@inheritdoc}
*/
public function commit()
{
throw new \BadMethodCallException('Not implemented.');
}
}

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\ParserCache;
@trigger_error('The '.__NAMESPACE__.'\ParserCacheInterface interface is deprecated since Symfony 3.2 and will be removed in 4.0. Use Psr\Cache\CacheItemPoolInterface instead.', \E_USER_DEPRECATED);
use Symfony\Component\ExpressionLanguage\ParsedExpression;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*
* @deprecated since version 3.2, to be removed in 4.0. Use Psr\Cache\CacheItemPoolInterface instead.
*/
interface ParserCacheInterface
{
/**
* Saves an expression in the cache.
*
* @param string $key The cache key
* @param ParsedExpression $expression A ParsedExpression instance to store in the cache
*/
public function save($key, ParsedExpression $expression);
/**
* Fetches an expression from the cache.
*
* @param string $key The cache key
*
* @return ParsedExpression|null
*/
public function fetch($key);
}

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,27 @@
<?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.
*/
$operators = ['not', '!', 'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', '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,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($expression, $nodes)
{
$this->expression = (string) $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($message, $cursor = 0, $expression = '', $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,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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
/**
* Tests ExpressionFunction.
*
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class ExpressionFunctionTest extends TestCase
{
public function testFunctionDoesNotExist()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('PHP function "fn_does_not_exist" does not exist.');
ExpressionFunction::fromPhp('fn_does_not_exist');
}
public function testFunctionNamespaced()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('An expression function name must be defined when PHP function "Symfony\Component\ExpressionLanguage\Tests\fn_namespaced" is namespaced.');
ExpressionFunction::fromPhp('Symfony\Component\ExpressionLanguage\Tests\fn_namespaced');
}
}
function fn_namespaced()
{
}

View File

@@ -0,0 +1,308 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\ParsedExpression;
use Symfony\Component\ExpressionLanguage\Tests\Fixtures\TestProvider;
class ExpressionLanguageTest extends TestCase
{
public function testCachedParse()
{
$cacheMock = $this->getMockBuilder('Psr\Cache\CacheItemPoolInterface')->getMock();
$cacheItemMock = $this->getMockBuilder('Psr\Cache\CacheItemInterface')->getMock();
$savedParsedExpression = null;
$expressionLanguage = new ExpressionLanguage($cacheMock);
$cacheMock
->expects($this->exactly(2))
->method('getItem')
->with('1%20%2B%201%2F%2F')
->willReturn($cacheItemMock)
;
$cacheItemMock
->expects($this->exactly(2))
->method('get')
->willReturnCallback(function () use (&$savedParsedExpression) {
return $savedParsedExpression;
})
;
$cacheItemMock
->expects($this->exactly(1))
->method('set')
->with($this->isInstanceOf(ParsedExpression::class))
->willReturnCallback(function ($parsedExpression) use (&$savedParsedExpression) {
$savedParsedExpression = $parsedExpression;
})
;
$cacheMock
->expects($this->exactly(1))
->method('save')
->with($cacheItemMock)
;
$parsedExpression = $expressionLanguage->parse('1 + 1', []);
$this->assertSame($savedParsedExpression, $parsedExpression);
$parsedExpression = $expressionLanguage->parse('1 + 1', []);
$this->assertSame($savedParsedExpression, $parsedExpression);
}
/**
* @group legacy
*/
public function testCachedParseWithDeprecatedParserCacheInterface()
{
$cacheMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$savedParsedExpression = null;
$expressionLanguage = new ExpressionLanguage($cacheMock);
$cacheMock
->expects($this->exactly(1))
->method('fetch')
->with('1%20%2B%201%2F%2F')
->willReturn($savedParsedExpression)
;
$cacheMock
->expects($this->exactly(1))
->method('save')
->with('1%20%2B%201%2F%2F', $this->isInstanceOf(ParsedExpression::class))
->willReturnCallback(function ($key, $expression) use (&$savedParsedExpression) {
$savedParsedExpression = $expression;
})
;
$parsedExpression = $expressionLanguage->parse('1 + 1', []);
$this->assertSame($savedParsedExpression, $parsedExpression);
}
public function testWrongCacheImplementation()
{
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Cache argument has to implement "Psr\Cache\CacheItemPoolInterface".');
$cacheMock = $this->getMockBuilder('Psr\Cache\CacheItemSpoolInterface')->getMock();
new ExpressionLanguage($cacheMock);
}
public function testConstantFunction()
{
$expressionLanguage = new ExpressionLanguage();
$this->assertEquals(\PHP_VERSION, $expressionLanguage->evaluate('constant("PHP_VERSION")'));
$expressionLanguage = new ExpressionLanguage();
$this->assertEquals('\constant("PHP_VERSION")', $expressionLanguage->compile('constant("PHP_VERSION")'));
}
public function testProviders()
{
$expressionLanguage = new ExpressionLanguage(null, [new TestProvider()]);
$this->assertEquals('foo', $expressionLanguage->evaluate('identity("foo")'));
$this->assertEquals('"foo"', $expressionLanguage->compile('identity("foo")'));
$this->assertEquals('FOO', $expressionLanguage->evaluate('strtoupper("foo")'));
$this->assertEquals('\strtoupper("foo")', $expressionLanguage->compile('strtoupper("foo")'));
$this->assertEquals('foo', $expressionLanguage->evaluate('strtolower("FOO")'));
$this->assertEquals('\strtolower("FOO")', $expressionLanguage->compile('strtolower("FOO")'));
$this->assertTrue($expressionLanguage->evaluate('fn_namespaced()'));
$this->assertEquals('\Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced()', $expressionLanguage->compile('fn_namespaced()'));
}
/**
* @dataProvider shortCircuitProviderEvaluate
*/
public function testShortCircuitOperatorsEvaluate($expression, array $values, $expected)
{
$expressionLanguage = new ExpressionLanguage();
$this->assertEquals($expected, $expressionLanguage->evaluate($expression, $values));
}
/**
* @dataProvider shortCircuitProviderCompile
*/
public function testShortCircuitOperatorsCompile($expression, array $names, $expected)
{
$result = null;
$expressionLanguage = new ExpressionLanguage();
eval(sprintf('$result = %s;', $expressionLanguage->compile($expression, $names)));
$this->assertSame($expected, $result);
}
public function testParseThrowsInsteadOfNotice()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Unexpected end of expression around position 6 for expression `node.`.');
$expressionLanguage = new ExpressionLanguage();
$expressionLanguage->parse('node.', ['node']);
}
public function shortCircuitProviderEvaluate()
{
$object = $this->getMockBuilder('stdClass')->setMethods(['foo'])->getMock();
$object->expects($this->never())->method('foo');
return [
['false and object.foo()', ['object' => $object], false],
['false && object.foo()', ['object' => $object], false],
['true || object.foo()', ['object' => $object], true],
['true or object.foo()', ['object' => $object], true],
];
}
public function shortCircuitProviderCompile()
{
return [
['false and foo', ['foo' => 'foo'], false],
['false && foo', ['foo' => 'foo'], false],
['true || foo', ['foo' => 'foo'], true],
['true or foo', ['foo' => 'foo'], true],
];
}
public function testCachingForOverriddenVariableNames()
{
$expressionLanguage = new ExpressionLanguage();
$expression = 'a + b';
$expressionLanguage->evaluate($expression, ['a' => 1, 'b' => 1]);
$result = $expressionLanguage->compile($expression, ['a', 'B' => 'b']);
$this->assertSame('($a + $B)', $result);
}
public function testStrictEquality()
{
$expressionLanguage = new ExpressionLanguage();
$expression = '123 === a';
$result = $expressionLanguage->compile($expression, ['a']);
$this->assertSame('(123 === $a)', $result);
}
public function testCachingWithDifferentNamesOrder()
{
$cacheMock = $this->getMockBuilder('Psr\Cache\CacheItemPoolInterface')->getMock();
$cacheItemMock = $this->getMockBuilder('Psr\Cache\CacheItemInterface')->getMock();
$expressionLanguage = new ExpressionLanguage($cacheMock);
$savedParsedExpression = null;
$cacheMock
->expects($this->exactly(2))
->method('getItem')
->with('a%20%2B%20b%2F%2Fa%7CB%3Ab')
->willReturn($cacheItemMock)
;
$cacheItemMock
->expects($this->exactly(2))
->method('get')
->willReturnCallback(function () use (&$savedParsedExpression) {
return $savedParsedExpression;
})
;
$cacheItemMock
->expects($this->exactly(1))
->method('set')
->with($this->isInstanceOf(ParsedExpression::class))
->willReturnCallback(function ($parsedExpression) use (&$savedParsedExpression) {
$savedParsedExpression = $parsedExpression;
})
;
$cacheMock
->expects($this->exactly(1))
->method('save')
->with($cacheItemMock)
;
$expression = 'a + b';
$expressionLanguage->compile($expression, ['a', 'B' => 'b']);
$expressionLanguage->compile($expression, ['B' => 'b', 'a']);
}
public function testOperatorCollisions()
{
$expressionLanguage = new ExpressionLanguage();
$expression = 'foo.not in [bar]';
$compiled = $expressionLanguage->compile($expression, ['foo', 'bar']);
$this->assertSame('in_array($foo->not, [0 => $bar])', $compiled);
$result = $expressionLanguage->evaluate($expression, ['foo' => (object) ['not' => 'test'], 'bar' => 'test']);
$this->assertTrue($result);
}
/**
* @dataProvider getRegisterCallbacks
*/
public function testRegisterAfterParse($registerCallback)
{
$this->expectException('LogicException');
$el = new ExpressionLanguage();
$el->parse('1 + 1', []);
$registerCallback($el);
}
/**
* @dataProvider getRegisterCallbacks
*/
public function testRegisterAfterEval($registerCallback)
{
$this->expectException('LogicException');
$el = new ExpressionLanguage();
$el->evaluate('1 + 1');
$registerCallback($el);
}
public function testCallBadCallable()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessageMatches('/Unable to call method "\w+" of object "\w+"./');
$el = new ExpressionLanguage();
$el->evaluate('foo.myfunction()', ['foo' => new \stdClass()]);
}
/**
* @dataProvider getRegisterCallbacks
*/
public function testRegisterAfterCompile($registerCallback)
{
$this->expectException('LogicException');
$el = new ExpressionLanguage();
$el->compile('1 + 1');
$registerCallback($el);
}
public function getRegisterCallbacks()
{
return [
[
function (ExpressionLanguage $el) {
$el->register('fn', function () {}, function () {});
},
],
[
function (ExpressionLanguage $el) {
$el->addFunction(new ExpressionFunction('fn', function () {}, function () {}));
},
],
[
function (ExpressionLanguage $el) {
$el->registerProvider(new TestProvider());
},
],
];
}
}

View File

@@ -0,0 +1,28 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Expression;
class ExpressionTest extends TestCase
{
public function testSerialization()
{
$expression = new Expression('kernel.boot()');
$serializedExpression = serialize($expression);
$unserializedExpression = unserialize($serializedExpression);
$this->assertEquals($expression, $unserializedExpression);
}
}

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\Tests\Fixtures;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionPhpFunction;
class TestProvider implements ExpressionFunctionProviderInterface
{
public function getFunctions()
{
return [
new ExpressionFunction('identity', function ($input) {
return $input;
}, function (array $values, $input) {
return $input;
}),
ExpressionFunction::fromPhp('strtoupper'),
ExpressionFunction::fromPhp('\strtolower'),
ExpressionFunction::fromPhp('Symfony\Component\ExpressionLanguage\Tests\Fixtures\fn_namespaced', 'fn_namespaced'),
];
}
}
function fn_namespaced()
{
return true;
}

View File

@@ -0,0 +1,129 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Lexer;
use Symfony\Component\ExpressionLanguage\Token;
use Symfony\Component\ExpressionLanguage\TokenStream;
class LexerTest extends TestCase
{
/**
* @var Lexer
*/
private $lexer;
protected function setUp()
{
$this->lexer = new Lexer();
}
/**
* @dataProvider getTokenizeData
*/
public function testTokenize($tokens, $expression)
{
$tokens[] = new Token('end of expression', null, \strlen($expression) + 1);
$this->assertEquals(new TokenStream($tokens, $expression), $this->lexer->tokenize($expression));
}
public function testTokenizeThrowsErrorWithMessage()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Unexpected character "\'" around position 33 for expression `service(faulty.expression.example\').dummyMethod()`.');
$expression = "service(faulty.expression.example').dummyMethod()";
$this->lexer->tokenize($expression);
}
public function testTokenizeThrowsErrorOnUnclosedBrace()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Unclosed "(" around position 7 for expression `service(unclosed.expression.dummyMethod()`.');
$expression = 'service(unclosed.expression.dummyMethod()';
$this->lexer->tokenize($expression);
}
public function getTokenizeData()
{
return [
[
[new Token('name', 'a', 3)],
' a ',
],
[
[new Token('name', 'a', 1)],
'a',
],
[
[new Token('string', 'foo', 1)],
'"foo"',
],
[
[new Token('number', '3', 1)],
'3',
],
[
[new Token('operator', '+', 1)],
'+',
],
[
[new Token('punctuation', '.', 1)],
'.',
],
[
[
new Token('punctuation', '(', 1),
new Token('number', '3', 2),
new Token('operator', '+', 4),
new Token('number', '5', 6),
new Token('punctuation', ')', 7),
new Token('operator', '~', 9),
new Token('name', 'foo', 11),
new Token('punctuation', '(', 14),
new Token('string', 'bar', 15),
new Token('punctuation', ')', 20),
new Token('punctuation', '.', 21),
new Token('name', 'baz', 22),
new Token('punctuation', '[', 25),
new Token('number', '4', 26),
new Token('punctuation', ']', 27),
],
'(3 + 5) ~ foo("bar").baz[4]',
],
[
[new Token('operator', '..', 1)],
'..',
],
[
[new Token('string', '#foo', 1)],
"'#foo'",
],
[
[new Token('string', '#foo', 1)],
'"#foo"',
],
[
[
new Token('name', 'foo', 1),
new Token('punctuation', '.', 4),
new Token('name', 'not', 5),
new Token('operator', 'in', 9),
new Token('punctuation', '[', 12),
new Token('name', 'bar', 13),
new Token('punctuation', ']', 16),
],
'foo.not in [bar]',
],
];
}
}

View File

@@ -0,0 +1,50 @@
<?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\Tests\Node;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Compiler;
abstract class AbstractNodeTest extends TestCase
{
/**
* @dataProvider getEvaluateData
*/
public function testEvaluate($expected, $node, $variables = [], $functions = [])
{
$this->assertSame($expected, $node->evaluate($functions, $variables));
}
abstract public function getEvaluateData();
/**
* @dataProvider getCompileData
*/
public function testCompile($expected, $node, $functions = [])
{
$compiler = new Compiler($functions);
$node->compile($compiler);
$this->assertSame($expected, $compiler->getSource());
}
abstract public function getCompileData();
/**
* @dataProvider getDumpData
*/
public function testDump($expected, $node)
{
$this->assertSame($expected, $node->dump());
}
abstract public function getDumpData();
}

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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ArgumentsNode;
class ArgumentsNodeTest extends ArrayNodeTest
{
public function getCompileData()
{
return [
['"a", "b"', $this->getArrayNode()],
];
}
public function getDumpData()
{
return [
['"a", "b"', $this->getArrayNode()],
];
}
protected function createArrayNode()
{
return new ArgumentsNode();
}
}

View File

@@ -0,0 +1,73 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ArrayNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class ArrayNodeTest extends AbstractNodeTest
{
public function testSerialization()
{
$node = $this->createArrayNode();
$node->addElement(new ConstantNode('foo'));
$serializedNode = serialize($node);
$unserializedNode = unserialize($serializedNode);
$this->assertEquals($node, $unserializedNode);
$this->assertNotEquals($this->createArrayNode(), $unserializedNode);
}
public function getEvaluateData()
{
return [
[['b' => 'a', 'b'], $this->getArrayNode()],
];
}
public function getCompileData()
{
return [
['["b" => "a", 0 => "b"]', $this->getArrayNode()],
];
}
public function getDumpData()
{
yield ['{"b": "a", 0: "b"}', $this->getArrayNode()];
$array = $this->createArrayNode();
$array->addElement(new ConstantNode('c'), new ConstantNode('a"b'));
$array->addElement(new ConstantNode('d'), new ConstantNode('a\b'));
yield ['{"a\\"b": "c", "a\\\\b": "d"}', $array];
$array = $this->createArrayNode();
$array->addElement(new ConstantNode('c'));
$array->addElement(new ConstantNode('d'));
yield ['["c", "d"]', $array];
}
protected function getArrayNode()
{
$array = $this->createArrayNode();
$array->addElement(new ConstantNode('a'), new ConstantNode('b'));
$array->addElement(new ConstantNode('b'));
return $array;
}
protected function createArrayNode()
{
return new ArrayNode();
}
}

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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ArrayNode;
use Symfony\Component\ExpressionLanguage\Node\BinaryNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class BinaryNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
$array = new ArrayNode();
$array->addElement(new ConstantNode('a'));
$array->addElement(new ConstantNode('b'));
return [
[true, new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))],
[true, new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))],
[false, new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))],
[false, new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))],
[0, new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))],
[6, new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))],
[6, new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))],
[true, new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))],
[true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))],
[true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))],
[false, new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))],
[false, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))],
[true, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))],
[true, new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))],
[false, new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))],
[false, new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))],
[true, new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))],
[-1, new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))],
[3, new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))],
[4, new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))],
[1, new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))],
[1, new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))],
[25, new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))],
['ab', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))],
[true, new BinaryNode('in', new ConstantNode('a'), $array)],
[false, new BinaryNode('in', new ConstantNode('c'), $array)],
[true, new BinaryNode('not in', new ConstantNode('c'), $array)],
[false, new BinaryNode('not in', new ConstantNode('a'), $array)],
[[1, 2, 3], new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))],
[1, new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+$/'))],
];
}
public function getCompileData()
{
$array = new ArrayNode();
$array->addElement(new ConstantNode('a'));
$array->addElement(new ConstantNode('b'));
return [
['(true || false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))],
['(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))],
['(true && false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))],
['(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))],
['(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))],
['(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))],
['(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))],
['(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))],
['(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))],
['(1 <= 1)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))],
['(1 > 2)', new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))],
['(1 >= 2)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))],
['(1 >= 1)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))],
['(true === true)', new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))],
['(true !== true)', new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))],
['(2 == 1)', new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))],
['(2 != 1)', new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))],
['(1 - 2)', new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))],
['(1 + 2)', new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))],
['(2 * 2)', new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))],
['(2 / 2)', new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))],
['(5 % 2)', new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))],
['pow(5, 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))],
['("a" . "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))],
['in_array("a", [0 => "a", 1 => "b"])', new BinaryNode('in', new ConstantNode('a'), $array)],
['in_array("c", [0 => "a", 1 => "b"])', new BinaryNode('in', new ConstantNode('c'), $array)],
['!in_array("c", [0 => "a", 1 => "b"])', new BinaryNode('not in', new ConstantNode('c'), $array)],
['!in_array("a", [0 => "a", 1 => "b"])', new BinaryNode('not in', new ConstantNode('a'), $array)],
['range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))],
['preg_match("/^[a-z]+/i\$/", "abc")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+/i$/'))],
];
}
public function getDumpData()
{
$array = new ArrayNode();
$array->addElement(new ConstantNode('a'));
$array->addElement(new ConstantNode('b'));
return [
['(true or false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))],
['(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))],
['(true and false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))],
['(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))],
['(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))],
['(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))],
['(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))],
['(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))],
['(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))],
['(1 <= 1)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))],
['(1 > 2)', new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))],
['(1 >= 2)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))],
['(1 >= 1)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))],
['(true === true)', new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))],
['(true !== true)', new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))],
['(2 == 1)', new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))],
['(2 != 1)', new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))],
['(1 - 2)', new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))],
['(1 + 2)', new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))],
['(2 * 2)', new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))],
['(2 / 2)', new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))],
['(5 % 2)', new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))],
['(5 ** 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))],
['("a" ~ "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))],
['("a" in ["a", "b"])', new BinaryNode('in', new ConstantNode('a'), $array)],
['("c" in ["a", "b"])', new BinaryNode('in', new ConstantNode('c'), $array)],
['("c" not in ["a", "b"])', new BinaryNode('not in', new ConstantNode('c'), $array)],
['("a" not in ["a", "b"])', new BinaryNode('not in', new ConstantNode('a'), $array)],
['(1 .. 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))],
['("abc" matches "/^[a-z]+/i$/")', new BinaryNode('matches', new ConstantNode('abc'), new ConstantNode('/^[a-z]+/i$/'))],
];
}
}

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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ConditionalNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class ConditionalNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
[1, new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))],
[2, new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))],
];
}
public function getCompileData()
{
return [
['((true) ? (1) : (2))', new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))],
['((false) ? (1) : (2))', new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))],
];
}
public function getDumpData()
{
return [
['(true ? 1 : 2)', new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))],
['(false ? 1 : 2)', new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))],
];
}
}

View File

@@ -0,0 +1,60 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class ConstantNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
[false, new ConstantNode(false)],
[true, new ConstantNode(true)],
[null, new ConstantNode(null)],
[3, new ConstantNode(3)],
[3.3, new ConstantNode(3.3)],
['foo', new ConstantNode('foo')],
[[1, 'b' => 'a'], new ConstantNode([1, 'b' => 'a'])],
];
}
public function getCompileData()
{
return [
['false', new ConstantNode(false)],
['true', new ConstantNode(true)],
['null', new ConstantNode(null)],
['3', new ConstantNode(3)],
['3.3', new ConstantNode(3.3)],
['"foo"', new ConstantNode('foo')],
['[0 => 1, "b" => "a"]', new ConstantNode([1, 'b' => 'a'])],
];
}
public function getDumpData()
{
return [
['false', new ConstantNode(false)],
['true', new ConstantNode(true)],
['null', new ConstantNode(null)],
['3', new ConstantNode(3)],
['3.3', new ConstantNode(3.3)],
['"foo"', new ConstantNode('foo')],
['foo', new ConstantNode('foo', true)],
['{0: 1, "b": "a", 1: true}', new ConstantNode([1, 'b' => 'a', true])],
['{"a\\"b": "c", "a\\\\b": "d"}', new ConstantNode(['a"b' => 'c', 'a\\b' => 'd'])],
['["c", "d"]', new ConstantNode(['c', 'd'])],
['{"a": ["b"]}', new ConstantNode(['a' => ['b']])],
];
}
}

View File

@@ -0,0 +1,52 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\Node\FunctionNode;
use Symfony\Component\ExpressionLanguage\Node\Node;
class FunctionNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
['bar', new FunctionNode('foo', new Node([new ConstantNode('bar')])), [], ['foo' => $this->getCallables()]],
];
}
public function getCompileData()
{
return [
['foo("bar")', new FunctionNode('foo', new Node([new ConstantNode('bar')])), ['foo' => $this->getCallables()]],
];
}
public function getDumpData()
{
return [
['foo("bar")', new FunctionNode('foo', new Node([new ConstantNode('bar')])), ['foo' => $this->getCallables()]],
];
}
protected function getCallables()
{
return [
'compiler' => function ($arg) {
return sprintf('foo(%s)', $arg);
},
'evaluator' => function ($variables, $arg) {
return $arg;
},
];
}
}

View File

@@ -0,0 +1,78 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ArrayNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\Node\GetAttrNode;
use Symfony\Component\ExpressionLanguage\Node\NameNode;
class GetAttrNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
['b', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), ['foo' => ['b' => 'a', 'b']]],
['a', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), ['foo' => ['b' => 'a', 'b']]],
['bar', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), ['foo' => new Obj()]],
['baz', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), ['foo' => new Obj()]],
['a', new GetAttrNode(new NameNode('foo'), new NameNode('index'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), ['foo' => ['b' => 'a', 'b'], 'index' => 'b']],
];
}
public function getCompileData()
{
return [
['$foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
['$foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
['$foo->foo', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), ['foo' => new Obj()]],
['$foo->foo(["b" => "a", 0 => "b"])', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), ['foo' => new Obj()]],
['$foo[$index]', new GetAttrNode(new NameNode('foo'), new NameNode('index'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
];
}
public function getDumpData()
{
return [
['foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
['foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
['foo.foo', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), ['foo' => new Obj()]],
['foo.foo({"b": "a", 0: "b"})', new GetAttrNode(new NameNode('foo'), new NameNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), ['foo' => new Obj()]],
['foo[index]', new GetAttrNode(new NameNode('foo'), new NameNode('index'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)],
];
}
protected function getArrayNode()
{
$array = new ArrayNode();
$array->addElement(new ConstantNode('a'), new ConstantNode('b'));
$array->addElement(new ConstantNode('b'));
return $array;
}
}
class Obj
{
public $foo = 'bar';
public function foo()
{
return 'baz';
}
}

View File

@@ -0,0 +1,38 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\NameNode;
class NameNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
['bar', new NameNode('foo'), ['foo' => 'bar']],
];
}
public function getCompileData()
{
return [
['$foo', new NameNode('foo')],
];
}
public function getDumpData()
{
return [
['foo', new NameNode('foo')],
];
}
}

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\Tests\Node;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\Node\Node;
class NodeTest extends TestCase
{
public function testToString()
{
$node = new Node([new ConstantNode('foo')]);
$this->assertEquals(<<<'EOF'
Node(
ConstantNode(value: 'foo')
)
EOF
, (string) $node);
}
public function testSerialization()
{
$node = new Node(['foo' => 'bar'], ['bar' => 'foo']);
$serializedNode = serialize($node);
$unserializedNode = unserialize($serializedNode);
$this->assertEquals($node, $unserializedNode);
}
}

View File

@@ -0,0 +1,48 @@
<?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\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\Node\UnaryNode;
class UnaryNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return [
[-1, new UnaryNode('-', new ConstantNode(1))],
[3, new UnaryNode('+', new ConstantNode(3))],
[false, new UnaryNode('!', new ConstantNode(true))],
[false, new UnaryNode('not', new ConstantNode(true))],
];
}
public function getCompileData()
{
return [
['(-1)', new UnaryNode('-', new ConstantNode(1))],
['(+3)', new UnaryNode('+', new ConstantNode(3))],
['(!true)', new UnaryNode('!', new ConstantNode(true))],
['(!true)', new UnaryNode('not', new ConstantNode(true))],
];
}
public function getDumpData()
{
return [
['(- 1)', new UnaryNode('-', new ConstantNode(1))],
['(+ 3)', new UnaryNode('+', new ConstantNode(3))],
['(! true)', new UnaryNode('!', new ConstantNode(true))],
['(not true)', new UnaryNode('not', new ConstantNode(true))],
];
}
}

View File

@@ -0,0 +1,29 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
use Symfony\Component\ExpressionLanguage\ParsedExpression;
class ParsedExpressionTest extends TestCase
{
public function testSerialization()
{
$expression = new ParsedExpression('25', new ConstantNode('25'));
$serializedExpression = serialize($expression);
$unserializedExpression = unserialize($serializedExpression);
$this->assertEquals($expression, $unserializedExpression);
}
}

View File

@@ -0,0 +1,140 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Node\Node;
use Symfony\Component\ExpressionLanguage\ParsedExpression;
use Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheAdapter;
/**
* @group legacy
*/
class ParserCacheAdapterTest extends TestCase
{
public function testGetItem()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$key = 'key';
$value = 'value';
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$poolMock
->expects($this->once())
->method('fetch')
->with($key)
->willReturn($value)
;
$cacheItem = $parserCacheAdapter->getItem($key);
$this->assertEquals($value, $cacheItem->get());
$this->assertTrue($cacheItem->isHit());
}
public function testSave()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$cacheItemMock = $this->getMockBuilder('Psr\Cache\CacheItemInterface')->getMock();
$key = 'key';
$value = new ParsedExpression('1 + 1', new Node([], []));
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$poolMock
->expects($this->once())
->method('save')
->with($key, $value)
;
$cacheItemMock
->expects($this->once())
->method('getKey')
->willReturn($key)
;
$cacheItemMock
->expects($this->once())
->method('get')
->willReturn($value)
;
$parserCacheAdapter->save($cacheItemMock);
}
public function testGetItems()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->getItems();
}
public function testHasItem()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$key = 'key';
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->hasItem($key);
}
public function testClear()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->clear();
}
public function testDeleteItem()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$key = 'key';
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->deleteItem($key);
}
public function testDeleteItems()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$keys = ['key'];
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->deleteItems($keys);
}
public function testSaveDeferred()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$cacheItemMock = $this->getMockBuilder('Psr\Cache\CacheItemInterface')->getMock();
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->saveDeferred($cacheItemMock);
}
public function testCommit()
{
$poolMock = $this->getMockBuilder('Symfony\Component\ExpressionLanguage\ParserCache\ParserCacheInterface')->getMock();
$parserCacheAdapter = new ParserCacheAdapter($poolMock);
$this->expectException(\BadMethodCallException::class);
$parserCacheAdapter->commit();
}
}

View File

@@ -0,0 +1,237 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ExpressionLanguage\Lexer;
use Symfony\Component\ExpressionLanguage\Node;
use Symfony\Component\ExpressionLanguage\Parser;
class ParserTest extends TestCase
{
public function testParseWithInvalidName()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Variable "foo" is not valid around position 1 for expression `foo`.');
$lexer = new Lexer();
$parser = new Parser([]);
$parser->parse($lexer->tokenize('foo'));
}
public function testParseWithZeroInNames()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Variable "foo" is not valid around position 1 for expression `foo`.');
$lexer = new Lexer();
$parser = new Parser([]);
$parser->parse($lexer->tokenize('foo'), [0]);
}
/**
* @dataProvider getParseData
*/
public function testParse($node, $expression, $names = [])
{
$lexer = new Lexer();
$parser = new Parser([]);
$this->assertEquals($node, $parser->parse($lexer->tokenize($expression), $names));
}
public function getParseData()
{
$arguments = new Node\ArgumentsNode();
$arguments->addElement(new Node\ConstantNode('arg1'));
$arguments->addElement(new Node\ConstantNode(2));
$arguments->addElement(new Node\ConstantNode(true));
$arrayNode = new Node\ArrayNode();
$arrayNode->addElement(new Node\NameNode('bar'));
return [
[
new Node\NameNode('a'),
'a',
['a'],
],
[
new Node\ConstantNode('a'),
'"a"',
],
[
new Node\ConstantNode(3),
'3',
],
[
new Node\ConstantNode(false),
'false',
],
[
new Node\ConstantNode(true),
'true',
],
[
new Node\ConstantNode(null),
'null',
],
[
new Node\UnaryNode('-', new Node\ConstantNode(3)),
'-3',
],
[
new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)),
'3 - 3',
],
[
new Node\BinaryNode('*',
new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)),
new Node\ConstantNode(2)
),
'(3 - 3) * 2',
],
[
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar', true), new Node\ArgumentsNode(), Node\GetAttrNode::PROPERTY_CALL),
'foo.bar',
['foo'],
],
[
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar', true), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL),
'foo.bar()',
['foo'],
],
[
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('not', true), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL),
'foo.not()',
['foo'],
],
[
new Node\GetAttrNode(
new Node\NameNode('foo'),
new Node\ConstantNode('bar', true),
$arguments,
Node\GetAttrNode::METHOD_CALL
),
'foo.bar("arg1", 2, true)',
['foo'],
],
[
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode(3), new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL),
'foo[3]',
['foo'],
],
[
new Node\ConditionalNode(new Node\ConstantNode(true), new Node\ConstantNode(true), new Node\ConstantNode(false)),
'true ? true : false',
],
[
new Node\BinaryNode('matches', new Node\ConstantNode('foo'), new Node\ConstantNode('/foo/')),
'"foo" matches "/foo/"',
],
// chained calls
[
$this->createGetAttrNode(
$this->createGetAttrNode(
$this->createGetAttrNode(
$this->createGetAttrNode(new Node\NameNode('foo'), 'bar', Node\GetAttrNode::METHOD_CALL),
'foo', Node\GetAttrNode::METHOD_CALL),
'baz', Node\GetAttrNode::PROPERTY_CALL),
'3', Node\GetAttrNode::ARRAY_CALL),
'foo.bar().foo().baz[3]',
['foo'],
],
[
new Node\NameNode('foo'),
'bar',
['foo' => 'bar'],
],
// Operators collisions
[
new Node\BinaryNode(
'in',
new Node\GetAttrNode(
new Node\NameNode('foo'),
new Node\ConstantNode('not', true),
new Node\ArgumentsNode(),
Node\GetAttrNode::PROPERTY_CALL
),
$arrayNode
),
'foo.not in [bar]',
['foo', 'bar'],
],
[
new Node\BinaryNode(
'or',
new Node\UnaryNode('not', new Node\NameNode('foo')),
new Node\GetAttrNode(
new Node\NameNode('foo'),
new Node\ConstantNode('not', true),
new Node\ArgumentsNode(),
Node\GetAttrNode::PROPERTY_CALL
)
),
'not foo or foo.not',
['foo'],
],
];
}
private function createGetAttrNode($node, $item, $type)
{
return new Node\GetAttrNode($node, new Node\ConstantNode($item, Node\GetAttrNode::ARRAY_CALL !== $type), new Node\ArgumentsNode(), $type);
}
/**
* @dataProvider getInvalidPostfixData
*/
public function testParseWithInvalidPostfixData($expr, $names = [])
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$lexer = new Lexer();
$parser = new Parser([]);
$parser->parse($lexer->tokenize($expr), $names);
}
public function getInvalidPostfixData()
{
return [
[
'foo."#"',
['foo'],
],
[
'foo."bar"',
['foo'],
],
[
'foo.**',
['foo'],
],
[
'foo.123',
['foo'],
],
];
}
public function testNameProposal()
{
$this->expectException('Symfony\Component\ExpressionLanguage\SyntaxError');
$this->expectExceptionMessage('Did you mean "baz"?');
$lexer = new Lexer();
$parser = new Parser([]);
$parser->parse($lexer->tokenize('foo > bar'), ['foo', 'baz']);
}
}

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;
/**
* Represents a Token.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Token
{
public $value;
public $type;
public $cursor;
const EOF_TYPE = 'end of expression';
const NAME_TYPE = 'name';
const NUMBER_TYPE = 'number';
const STRING_TYPE = 'string';
const OPERATOR_TYPE = 'operator';
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($type, $value, $cursor)
{
$this->type = $type;
$this->value = $value;
$this->cursor = $cursor;
}
/**
* Returns a string representation of the token.
*
* @return string A string representation of the token
*/
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.
*
* @param string $type The type to test
* @param string|null $value The token value
*
* @return bool
*/
public function test($type, $value = null)
{
return $this->type === $type && (null === $value || $this->value == $value);
}
}

View File

@@ -0,0 +1,97 @@
<?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;
/**
* @param array $tokens An array of tokens
* @param string $expression
*/
public function __construct(array $tokens, $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];
}
/**
* Tests a token.
*
* @param array|int $type The type to test
* @param string|null $value The token value
* @param string|null $message The syntax error message
*/
public function expect($type, $value = null, $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
*
* @return string
*/
public function getExpression()
{
return $this->expression;
}
}

View File

@@ -0,0 +1,30 @@
{
"name": "symfony/expression-language",
"type": "library",
"description": "Symfony ExpressionLanguage Component",
"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": "^5.5.9|>=7.0.8",
"symfony/cache": "~3.1|~4.0",
"symfony/polyfill-php70": "~1.6"
},
"autoload": {
"psr-4": { "Symfony\\Component\\ExpressionLanguage\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony ExpressionLanguage Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>