[ExpressionLanguage] added the component

This commit is contained in:
Fabien Potencier 2013-09-01 21:41:49 +02:00
parent 091a96ca3d
commit 9d98fa25ec
38 changed files with 2414 additions and 0 deletions

View File

@ -33,6 +33,7 @@
"symfony/doctrine-bridge": "self.version",
"symfony/dom-crawler": "self.version",
"symfony/event-dispatcher": "self.version",
"symfony/expression-language": "self.version",
"symfony/filesystem": "self.version",
"symfony/finder": "self.version",
"symfony/form": "self.version",

View File

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

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
2.4.0
-----
* added the component

View File

@ -0,0 +1,148 @@
<?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.
*
* @param Node\Node $node The node to compile
*
* @return Compiler The current compiler instance
*/
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 Compiler The current compiler instance
*/
public function raw($string)
{
$this->source .= $string;
return $this;
}
/**
* Adds a quoted string to the compiled code.
*
* @param string $value The string
*
* @return Compiler The current compiler instance
*/
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 Compiler The current compiler instance
*/
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('array(');
$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,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;
/**
* Represents an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Expression
{
private $expression;
/**
* Constructor.
*
* @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,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;
/**
* Allows to compile and evaluate expressions written in your own DSL.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionLanguage
{
private $lexer;
private $parser;
private $compiler;
private $cache;
protected $functions;
public function __construct()
{
$this->functions = array();
$this->registerFunctions();
}
/**
* Compiles an expression source code.
*
* @param 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 = array())
{
return $this->getCompiler()->compile($this->parse($expression, $names))->getSource();
}
public function evaluate($expression, $values = array())
{
return $this->parse($expression, array_keys($values))->evaluate($this->functions, $values);
}
public function addFunction($name, $compiler, $evaluator)
{
$this->functions[$name] = array('compiler' => $compiler, 'evaluator' => $evaluator);
}
protected function registerFunctions()
{
$this->addFunction('constant', function ($constant) {
return sprintf('constant(%s)', $constant);
}, function (array $values, $constant) {
return constant($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();
}
private function parse($expression, $names)
{
$key = $expression.'//'.implode('-', $names);
if (!isset($this->cache[$key])) {
$this->cache[$key] = $this->getParser()->parse($this->getLexer()->tokenize((string) $expression), $names);
}
return $this->cache[$key];
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2013 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,119 @@
<?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
*/
public function tokenize($expression)
{
$expression = str_replace(array("\r\n", "\r"), "\n", $expression);
$cursor = 0;
$tokens = array();
$brackets = array();
$operatorRegex = $this->getOperatorRegex();
$end = strlen($expression);
while ($cursor < $end) {
if (preg_match('/\s+/A', $expression, $match, null, $cursor)) {
// whitespace
$cursor += strlen($match[0]);
} elseif (preg_match($operatorRegex, $expression, $match, null, $cursor)) {
// operators
$tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1);
$cursor += strlen($match[0]);
} elseif (preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $expression, $match, null, $cursor)) {
// names
$tokens[] = new Token(Token::NAME_TYPE, $match[0], $cursor + 1);
$cursor += strlen($match[0]);
} elseif (preg_match('/[0-9]+(?:\.[0-9]+)?/A', $expression, $match, null, $cursor)) {
// numbers
$number = (float) $match[0]; // floats
if (ctype_digit($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[] = array($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);
}
list($expect, $cur) = array_pop($brackets);
if ($expression[$cursor] != strtr($expect, '([{', ')]}')) {
throw new SyntaxError(sprintf('Unclosed "%s"', $expect), $cur);
}
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (false !== strpos('.,?:', $expression[$cursor])) {
// punctuation
$tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1);
++$cursor;
} elseif (preg_match('/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As', $expression, $match, null, $cursor)) {
// strings
$tokens[] = new Token(Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)), $cursor + 1);
$cursor += strlen($match[0]);
} else {
// unlexable
throw new SyntaxError(sprintf('Unexpected character "%s"', $expression[$cursor]), $cursor);
}
}
$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);
}
return new TokenStream($tokens);
}
private function getOperatorRegex()
{
$operators = array(
'not', '!', '-', '+',
'or', '||', '&&', 'and', '|', '^', '&', '==', '===', '!=', '!==', '<', '>', '>=', '<=', 'not in', 'in', '..', '+', '-', '~', '*', '/', '%', '**',
);
$operators = array_combine($operators, array_map('strlen', $operators));
arsort($operators);
$regex = array();
foreach ($operators as $operator => $length) {
// an operator that ends with a character must be followed by
// a whitespace or a parenthesis
$regex[] = preg_quote($operator, '/').(ctype_alpha($operator[$length - 1]) ? '(?=[\s()])' : '');
}
return '/'.implode('|', $regex).'/A';
}
}

View File

@ -0,0 +1,22 @@
<?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;
class ArgumentsNode extends ArrayNode
{
public function compile(Compiler $compiler)
{
$this->compileArguments($compiler, false);
}
}

View File

@ -0,0 +1,85 @@
<?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;
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.
*
* @param Compiler A Compiler instance
*/
public function compile(Compiler $compiler)
{
$compiler->raw('array(');
$this->compileArguments($compiler);
$compiler->raw(')');
}
public function evaluate($functions, $values)
{
$result = array();
foreach ($this->getKeyValuePairs() as $pair) {
$result[$pair['key']->evaluate($functions, $values)] = $pair['value']->evaluate($functions, $values);
}
return $result;
}
protected function getKeyValuePairs()
{
$pairs = array();
foreach (array_chunk($this->nodes, 2) as $pair) {
$pairs[] = array('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,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\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
class BinaryNode extends Node
{
private $operators = array(
'~' => '.',
'and' => '&&',
'or' => '||',
);
private $functions = array(
'**' => 'pow',
'..' => 'range',
'in' => 'in_array',
'not in' => '!in_array',
);
public function __construct($operator, Node $left, Node $right)
{
$this->nodes = array('left' => $left, 'right' => $right);
$this->attributes = array('operator' => $operator);
}
public function compile(Compiler $compiler)
{
$operator = $this->attributes['operator'];
if (isset($this->functions[$operator])) {
$compiler
->raw(sprintf('%s(', $this->functions[$operator]))
->compile($this->nodes['left'])
->raw(', ')
->compile($this->nodes['right'])
->raw(')')
;
return;
}
if (isset($this->operators[$operator])) {
$operator = $this->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);
$right = $this->nodes['right']->evaluate($functions, $values);
if (isset($this->functions[$operator])) {
if ('not in' == $operator) {
return !call_user_func('in_array', $left, $right);
}
return call_user_func($this->functions[$operator], $left, $right);
}
switch ($operator) {
case 'or':
case '||':
return $left || $right;
case 'and':
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 '<=':
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 '/':
return $left / $right;
case '%':
return $left % $right;
}
}
}

View File

@ -0,0 +1,44 @@
<?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;
class ConditionalNode extends Node
{
public function __construct(Node $expr1, Node $expr2, Node $expr3)
{
$this->nodes = array('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);
}
}

View File

@ -0,0 +1,32 @@
<?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;
class ConstantNode extends Node
{
public function __construct($value)
{
$this->attributes = array('value' => $value);
}
public function compile(Compiler $compiler)
{
$compiler->repr($this->attributes['value']);
}
public function evaluate($functions, $values)
{
return $this->attributes['value'];
}
}

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;
class FunctionNode extends Node
{
public function __construct($name, Node $arguments)
{
$this->nodes = array('arguments' => $arguments);
$this->attributes = array('name' => $name);
}
public function compile(Compiler $compiler)
{
$arguments = array();
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 = array($values);
foreach ($this->nodes['arguments']->nodes as $node) {
$arguments[] = $node->evaluate($functions, $values);
}
return call_user_func_array($functions[$this->attributes['name']]['evaluator'], $arguments);
}
}

View File

@ -0,0 +1,90 @@
<?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;
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)
{
$this->nodes = array('node' => $node, 'attribute' => $attribute, 'arguments' => $arguments);
$this->attributes = array('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.');
}
return call_user_func_array(array($obj, $this->nodes['attribute']->evaluate($functions, $values)), $this->nodes['arguments']->evaluate($functions, $values));
case self::ARRAY_CALL:
$values = $this->nodes['node']->evaluate($functions, $values);
if (!is_array($values) && !$values instanceof \ArrayAccess) {
throw new \RuntimeException('Unable to get an item on a non-array.');
}
return $values[$this->nodes['attribute']->evaluate($functions, $values)];
}
}
}

View File

@ -0,0 +1,32 @@
<?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;
class NameNode extends Node
{
public function __construct($name)
{
$this->attributes = array('name' => $name);
}
public function compile(Compiler $compiler)
{
$compiler->raw('$'.$this->attributes['name']);
}
public function evaluate($functions, $values)
{
return $values[$this->attributes['name']];
}
}

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\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
/**
* Represents a node in the AST.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Node
{
public $nodes = array();
public $attributes = array();
/**
* Constructor.
*
* @param array $nodes An array of nodes
* @param array $attributes An array of attributes
*/
public function __construct(array $nodes = array(), array $attributes = array())
{
$this->nodes = $nodes;
$this->attributes = $attributes;
}
public function __toString()
{
$attributes = array();
foreach ($this->attributes as $name => $value) {
$attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true)));
}
$repr = array(str_replace('Symfony\Component\ExpressionLanguage\Node\\', '', get_class($this)).'('.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 = array();
foreach ($this->nodes as $node) {
$results[] = $node->evaluate($functions, $values);
}
return $results;
}
}

View File

@ -0,0 +1,54 @@
<?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;
class UnaryNode extends Node
{
private $operators = array(
'!' => '!',
'not' => '!',
'+' => '+',
'-' => '-',
);
public function __construct($operator, Node $node)
{
$this->nodes = array('node' => $node);
$this->attributes = array('operator' => $operator);
}
public function compile(Compiler $compiler)
{
$compiler
->raw('(')
->raw($this->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;
}
}

View File

@ -0,0 +1,356 @@
<?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 = array(
'not' => array('precedence' => 50),
'!' => array('precedence' => 50),
'-' => array('precedence' => 500),
'+' => array('precedence' => 500),
);
$this->binaryOperators = array(
'or' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT),
'||' => array('precedence' => 10, 'associativity' => Parser::OPERATOR_LEFT),
'and' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT),
'&&' => array('precedence' => 15, 'associativity' => Parser::OPERATOR_LEFT),
'|' => array('precedence' => 16, 'associativity' => Parser::OPERATOR_LEFT),
'^' => array('precedence' => 17, 'associativity' => Parser::OPERATOR_LEFT),
'&' => array('precedence' => 18, 'associativity' => Parser::OPERATOR_LEFT),
'==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'===' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'!=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'!==' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'<' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'>' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'>=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'<=' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'not in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'in' => array('precedence' => 20, 'associativity' => Parser::OPERATOR_LEFT),
'..' => array('precedence' => 25, 'associativity' => Parser::OPERATOR_LEFT),
'+' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT),
'-' => array('precedence' => 30, 'associativity' => Parser::OPERATOR_LEFT),
'~' => array('precedence' => 40, 'associativity' => Parser::OPERATOR_LEFT),
'*' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT),
'/' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT),
'%' => array('precedence' => 60, 'associativity' => Parser::OPERATOR_LEFT),
'**' => array('precedence' => 200, 'associativity' => Parser::OPERATOR_RIGHT),
);
}
/**
* Converts a token stream to a node tree.
*
* @param TokenStream $stream A token stream instance
* @param array $names An array of valid names
*
* @return Node A node tree
*/
public function parse(TokenStream $stream, $names = array())
{
$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);
}
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);
}
$node = new Node\FunctionNode($token->value, $this->parseArguments());
} else {
if (!in_array($token->value, $this->names)) {
throw new SyntaxError(sprintf('Variable "%s" is not valid', $token->value), $token->cursor);
}
$node = new Node\NameNode($token->value);
}
}
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);
}
}
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->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->type == Token::PUNCTUATION_TYPE) {
if ('.' === $token->value) {
$this->stream->next();
$token = $this->stream->current;
$this->stream->next();
if (
$token->type !== Token::NAME_TYPE
&&
$token->type !== Token::NUMBER_TYPE
&&
// operators line "not" are valid method or property names
($token->type !== Token::OPERATOR_TYPE && preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A', $token->value))
) {
throw new SyntaxError('Expected name or number', $token->cursor);
}
$arg = new Node\ConstantNode($token->value);
$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) {
if ($node instanceof Node\GetAttrNode && Node\GetAttrNode::METHOD_CALL === $node->attributes['type'] && version_compare(PHP_VERSION, '5.4.0', '<')) {
throw new SyntaxError('Array calls on a method call is only supported on PHP 5.4+', $token->cursor);
}
$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 = array();
$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,43 @@
ExpressionLanguage Component
============================
The ExpressionLanguage component provides an engine that can compile and
evaluate expressions:
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
$language = new ExpressionLanguage();
echo $language->evaluate('1 + foo', array('foo' => 2));
// would output 3
echo $language->compile('1 + foo');
// would output (1 + $foo)
By default, the engine implements simple math and logic functions, method
calls, property accesses, and array accesses.
You can extend your DSL with functions:
$compiler = function ($arg) {
return sprintf('strtoupper(%s)', $arg);
};
$evaluator = function (array $variables, $value) {
return strtoupper($value);
};
$language->addFunction('upper', $compiler, $evaluator);
echo $language->evaluate('"foo" ~ upper(foo)', array('foo' => 'bar'));
// would output fooBAR
echo $language->compile('"foo" ~ upper(foo)');
// would output ("foo" . strtoupper($foo))
Resources
---------
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/ExpressionLanguage/
$ composer.phar install --dev
$ phpunit

View File

@ -0,0 +1,20 @@
<?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)
{
parent::__construct(sprintf('%s around position %d.', $message, $cursor));
}
}

View File

@ -0,0 +1,75 @@
<?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\Lexer;
use Symfony\Component\ExpressionLanguage\Token;
use Symfony\Component\ExpressionLanguage\TokenStream;
class LexerTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getTokenizeData
*/
public function testTokenize($tokens, $expression)
{
$tokens[] = new Token('end of expression', null, strlen($expression) + 1);
$lexer = new Lexer();
$this->assertEquals(new TokenStream($tokens), $lexer->tokenize($expression));
}
public function getTokenizeData()
{
return array(
array(
array(new Token('name', 'a', 1)),
'a',
),
array(
array(new Token('string', 'foo', 1)),
'"foo"',
),
array(
array(new Token('number', '3', 1)),
'3',
),
array(
array(new Token('operator', '+', 1)),
'+',
),
array(
array(new Token('punctuation', '.', 1)),
'.',
),
array(
array(
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]',
),
);
}
}

View File

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Tests\Node;
use Symfony\Component\ExpressionLanguage\Compiler;
abstract class AbstractNodeTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getEvaluateData
*/
public function testEvaluate($expected, $node, $variables = array(), $functions = array())
{
$this->assertSame($expected, $node->evaluate($functions, $variables));
}
abstract public function getEvaluateData();
/**
* @dataProvider getCompileData
*/
public function testCompile($expected, $node, $functions = array())
{
$compiler = new Compiler($functions);
$node->compile($compiler);
$this->assertSame($expected, $compiler->getSource());
}
abstract public function getCompileData();
}

View File

@ -0,0 +1,30 @@
<?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;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class ArgumentsNodeTest extends ArrayNodeTest
{
public function getCompileData()
{
return array(
array('"a", "b"', $this->getArrayNode()),
);
}
protected function createArrayNode()
{
return new ArgumentsNode();
}
}

View File

@ -0,0 +1,46 @@
<?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 getEvaluateData()
{
return array(
array(array('b' => 'a', 'b'), $this->getArrayNode()),
);
}
public function getCompileData()
{
return array(
array('array("b" => "a", 0 => "b")', $this->getArrayNode()),
);
}
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,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\BinaryNode;
use Symfony\Component\ExpressionLanguage\Node\ArrayNode;
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 array(
array(true, new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))),
array(true, new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))),
array(false, new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))),
array(false, new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))),
array(0, new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))),
array(6, new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))),
array(6, new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))),
array(true, new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))),
array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))),
array(true, new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))),
array(false, new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))),
array(false, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))),
array(true, new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))),
array(true, new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))),
array(false, new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))),
array(false, new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))),
array(true, new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))),
array(-1, new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))),
array(3, new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))),
array(4, new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))),
array(1, new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))),
array(1, new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))),
array(25, new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))),
array('ab', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))),
array(true, new BinaryNode('in', new ConstantNode('a'), $array)),
array(false, new BinaryNode('in', new ConstantNode('c'), $array)),
array(true, new BinaryNode('not in', new ConstantNode('c'), $array)),
array(false, new BinaryNode('not in', new ConstantNode('a'), $array)),
array(array(1, 2, 3), new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))),
);
}
public function getCompileData()
{
$array = new ArrayNode();
$array->addElement(new ConstantNode('a'));
$array->addElement(new ConstantNode('b'));
return array(
array('(true || false)', new BinaryNode('or', new ConstantNode(true), new ConstantNode(false))),
array('(true || false)', new BinaryNode('||', new ConstantNode(true), new ConstantNode(false))),
array('(true && false)', new BinaryNode('and', new ConstantNode(true), new ConstantNode(false))),
array('(true && false)', new BinaryNode('&&', new ConstantNode(true), new ConstantNode(false))),
array('(2 & 4)', new BinaryNode('&', new ConstantNode(2), new ConstantNode(4))),
array('(2 | 4)', new BinaryNode('|', new ConstantNode(2), new ConstantNode(4))),
array('(2 ^ 4)', new BinaryNode('^', new ConstantNode(2), new ConstantNode(4))),
array('(1 < 2)', new BinaryNode('<', new ConstantNode(1), new ConstantNode(2))),
array('(1 <= 2)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(2))),
array('(1 <= 1)', new BinaryNode('<=', new ConstantNode(1), new ConstantNode(1))),
array('(1 > 2)', new BinaryNode('>', new ConstantNode(1), new ConstantNode(2))),
array('(1 >= 2)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(2))),
array('(1 >= 1)', new BinaryNode('>=', new ConstantNode(1), new ConstantNode(1))),
array('(true === true)', new BinaryNode('===', new ConstantNode(true), new ConstantNode(true))),
array('(true !== true)', new BinaryNode('!==', new ConstantNode(true), new ConstantNode(true))),
array('(2 == 1)', new BinaryNode('==', new ConstantNode(2), new ConstantNode(1))),
array('(2 != 1)', new BinaryNode('!=', new ConstantNode(2), new ConstantNode(1))),
array('(1 - 2)', new BinaryNode('-', new ConstantNode(1), new ConstantNode(2))),
array('(1 + 2)', new BinaryNode('+', new ConstantNode(1), new ConstantNode(2))),
array('(2 * 2)', new BinaryNode('*', new ConstantNode(2), new ConstantNode(2))),
array('(2 / 2)', new BinaryNode('/', new ConstantNode(2), new ConstantNode(2))),
array('(5 % 2)', new BinaryNode('%', new ConstantNode(5), new ConstantNode(2))),
array('pow(5, 2)', new BinaryNode('**', new ConstantNode(5), new ConstantNode(2))),
array('("a" . "b")', new BinaryNode('~', new ConstantNode('a'), new ConstantNode('b'))),
array('in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('a'), $array)),
array('in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('in', new ConstantNode('c'), $array)),
array('!in_array("c", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('c'), $array)),
array('!in_array("a", array(0 => "a", 1 => "b"))', new BinaryNode('not in', new ConstantNode('a'), $array)),
array('range(1, 3)', new BinaryNode('..', new ConstantNode(1), new ConstantNode(3))),
);
}
}

View File

@ -0,0 +1,34 @@
<?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 array(
array(1, new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))),
array(2, new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))),
);
}
public function getCompileData()
{
return array(
array('((true) ? (1) : (2))', new ConditionalNode(new ConstantNode(true), new ConstantNode(1), new ConstantNode(2))),
array('((false) ? (1) : (2))', new ConditionalNode(new ConstantNode(false), new ConstantNode(1), new ConstantNode(2))),
);
}
}

View File

@ -0,0 +1,43 @@
<?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 array(
array(false, new ConstantNode(false)),
array(true, new ConstantNode(true)),
array(null, new ConstantNode(null)),
array(3, new ConstantNode(3)),
array(3.3, new ConstantNode(3.3)),
array('foo', new ConstantNode('foo')),
array(array(1, 'b' => 'a'), new ConstantNode(array(1, 'b' => 'a'))),
);
}
public function getCompileData()
{
return array(
array('false', new ConstantNode(false)),
array('true', new ConstantNode(true)),
array('null', new ConstantNode(null)),
array('3', new ConstantNode(3)),
array('3.3', new ConstantNode(3.3)),
array('"foo"', new ConstantNode('foo')),
array('array(0 => 1, "b" => "a")', new ConstantNode(array(1, 'b' => 'a'))),
);
}
}

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

View File

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\ArrayNode;
use Symfony\Component\ExpressionLanguage\Node\NameNode;
use Symfony\Component\ExpressionLanguage\Node\GetAttrNode;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class GetAttrNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return array(
array('b', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))),
array('a', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL), array('foo' => array('b' => 'a', 'b'))),
array('bar', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())),
array('baz', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())),
);
}
public function getCompileData()
{
return array(
array('$foo[0]', new GetAttrNode(new NameNode('foo'), new ConstantNode(0), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)),
array('$foo["b"]', new GetAttrNode(new NameNode('foo'), new ConstantNode('b'), $this->getArrayNode(), GetAttrNode::ARRAY_CALL)),
array('$foo->foo', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::PROPERTY_CALL), array('foo' => new Obj())),
array('$foo->foo(array("b" => "a", 0 => "b"))', new GetAttrNode(new NameNode('foo'), new ConstantNode('foo'), $this->getArrayNode(), GetAttrNode::METHOD_CALL), array('foo' => new Obj())),
);
}
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,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage\Tests\Node;
use Symfony\Component\ExpressionLanguage\Node\NameNode;
class NameNodeTest extends AbstractNodeTest
{
public function getEvaluateData()
{
return array(
array('bar', new NameNode('foo'), array('foo' => 'bar')),
);
}
public function getCompileData()
{
return array(
array('$foo', new NameNode('foo')),
);
}
}

View File

@ -0,0 +1,30 @@
<?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\Node;
use Symfony\Component\ExpressionLanguage\Node\ConstantNode;
class NodeTest extends \PHPUnit_Framework_TestCase
{
public function testToString()
{
$node = new Node(array(new ConstantNode('foo')));
$this->assertEquals(<<<EOF
Node(
ConstantNode(value: 'foo')
)
EOF
, (string) $node);
}
}

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

View File

@ -0,0 +1,143 @@
<?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\Parser;
use Symfony\Component\ExpressionLanguage\Lexer;
use Symfony\Component\ExpressionLanguage\Node;
class ParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\ExpressionLanguage\SyntaxError
* @expectedExceptionMessage Variable "foo" is not valid around position 1.
*/
public function testParseWithInvalidName()
{
$lexer = new Lexer();
$parser = new Parser(array());
$parser->parse($lexer->tokenize('foo'));
}
/**
* @dataProvider getParseData
*/
public function testParse($node, $expression, $names = array())
{
$lexer = new Lexer();
$parser = new Parser(array());
$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));
return array(
array(
new Node\NameNode('a'),
'a',
array('a'),
),
array(
new Node\ConstantNode('a'),
'"a"',
),
array(
new Node\ConstantNode(3),
'3',
),
array(
new Node\ConstantNode(false),
'false',
),
array(
new Node\ConstantNode(true),
'true',
),
array(
new Node\ConstantNode(null),
'null',
),
array(
new Node\UnaryNode('-', new Node\ConstantNode(3)),
'-3',
),
array(
new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)),
'3 - 3',
),
array(
new Node\BinaryNode('*',
new Node\BinaryNode('-', new Node\ConstantNode(3), new Node\ConstantNode(3)),
new Node\ConstantNode(2)
),
'(3 - 3) * 2',
),
array(
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::PROPERTY_CALL),
'foo.bar',
array('foo'),
),
array(
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('bar'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL),
'foo.bar()',
array('foo'),
),
array(
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode('not'), new Node\ArgumentsNode(), Node\GetAttrNode::METHOD_CALL),
'foo.not()',
array('foo'),
),
array(
new Node\GetAttrNode(
new Node\NameNode('foo'),
new Node\ConstantNode('bar'),
$arguments,
Node\GetAttrNode::METHOD_CALL
),
'foo.bar("arg1", 2, true)',
array('foo'),
),
array(
new Node\GetAttrNode(new Node\NameNode('foo'), new Node\ConstantNode(3), new Node\ArgumentsNode(), Node\GetAttrNode::ARRAY_CALL),
'foo[3]',
array('foo'),
),
array(
new Node\ConditionalNode(new Node\ConstantNode(true), new Node\ConstantNode(true), new Node\ConstantNode(false)),
'true ? true : false',
),
// chained calls
array(
$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]',
array('foo'),
),
);
}
private function createGetAttrNode($node, $item, $type)
{
return new Node\GetAttrNode($node, new Node\ConstantNode($item), new Node\ArgumentsNode(), $type);
}
}

View File

@ -0,0 +1,68 @@
<?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';
/**
* Constructor.
*
* @param integer $type The type of the token
* @param string $value The token value
* @param integer $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 array|integer $type The type to test
* @param string|null $value The token value
*
* @return Boolean
*/
public function test($type, $value = null)
{
return $this->type === $type && (null === $value || $this->value == $value);
}
}

View File

@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\ExpressionLanguage;
/**
* Represents a token stream.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TokenStream
{
public $current;
private $tokens;
private $position;
/**
* Constructor.
*
* @param array $tokens An array of tokens
*/
public function __construct(array $tokens)
{
$this->tokens = $tokens;
$this->position = 0;
$this->current = $tokens[0];
}
/**
* 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()
{
if (!isset($this->tokens[$this->position])) {
throw new SyntaxError('Unexpected end of expression', $this->current->cursor);
}
++$this->position;
$this->current = $this->tokens[$this->position];
}
/**
* Tests a token.
*/
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->next();
}
/**
* Checks if end of stream was reached
*
* @return bool
*/
public function isEOF()
{
return $this->current->type === Token::EOF_TYPE;
}
}

View File

@ -0,0 +1,31 @@
{
"name": "symfony/expression-language",
"type": "library",
"description": "Symfony ExpressionLanguage Component",
"keywords": [],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3"
},
"autoload": {
"psr-0": { "Symfony\\Component\\ExpressionLanguage\\": "" }
},
"target-dir": "Symfony/Component/ExpressionLanguage",
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.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>