+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2009 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Autoloads Twig Extensions classes.
- *
- * @package twig
- * @author Fabien Potencier <fabien.potencier@symfony-project.com>
- */
-class Twig_Extensions_Autoloader
-{
- /**
- * Registers Twig_Extensions_Autoloader as an SPL autoloader.
- */
- static public function register()
- {
- spl_autoload_register(array(new self, 'autoload'));
- }
-
- /**
- * Handles autoloading of classes.
- *
- * @param string $class A class name.
- *
- * @return boolean Returns true if the class has been loaded
- */
- static public function autoload($class)
- {
- if (0 !== strpos($class, 'Twig_Extensions')) {
- return;
- }
-
- if (file_exists($file = dirname(__FILE__).'/../../'.str_replace('_', '/', $class).'.php')) {
- require $file;
- }
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Extension_Debug extends Twig_Extension
-{
- /**
- * Returns the token parser instance to add to the existing list.
- *
- * @return array An array of Twig_TokenParser instances
- */
- public function getTokenParsers()
- {
- return array(
- new Twig_Extensions_TokenParser_Debug(),
- );
- }
-
- /**
- * Returns the name of the extension.
- *
- * @return string The extension name
- */
- public function getName()
- {
- return 'debug';
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Extension_I18n extends Twig_Extension
-{
- /**
- * Returns the token parser instances to add to the existing list.
- *
- * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
- */
- public function getTokenParsers()
- {
- return array(new Twig_Extensions_TokenParser_Trans());
- }
-
- /**
- * Returns a list of filters to add to the existing list.
- *
- * @return array An array of filters
- */
- public function getFilters()
- {
- return array(
- 'trans' => new Twig_Filter_Function('gettext'),
- );
- }
-
- /**
- * Returns the name of the extension.
- *
- * @return string The extension name
- */
- public function getName()
- {
- return 'i18n';
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-class Twig_Extensions_Extension_Intl extends Twig_Extension
-{
- public function __construct()
- {
- if (!class_exists('IntlDateFormatter')) {
- throw new RuntimeException('The intl extension is needed to use intl-based filters.');
- }
- }
-
- /**
- * Returns a list of filters to add to the existing list.
- *
- * @return array An array of filters
- */
- public function getFilters()
- {
- return array(
- 'localizeddate' => new Twig_Filter_Function('twig_localized_date_filter', array('needs_environment' => true)),
- );
- }
-
- /**
- * Returns the name of the extension.
- *
- * @return string The extension name
- */
- public function getName()
- {
- return 'intl';
- }
-}
-
-function twig_localized_date_filter(Twig_Environment $env, $date, $dateFormat = 'medium', $timeFormat = 'medium', $locale = null, $timezone = null, $format = null)
-{
- $date = twig_date_converter($env, $date, $timezone);
-
- $formatValues = array(
- 'none' => IntlDateFormatter::NONE,
- 'short' => IntlDateFormatter::SHORT,
- 'medium' => IntlDateFormatter::MEDIUM,
- 'long' => IntlDateFormatter::LONG,
- 'full' => IntlDateFormatter::FULL,
- );
-
- $formatter = IntlDateFormatter::create(
- $locale !== null ? $locale : Locale::getDefault(),
- $formatValues[$dateFormat],
- $formatValues[$timeFormat],
- $date->getTimezone()->getName(),
- IntlDateFormatter::GREGORIAN,
- $format
- );
-
- return $formatter->format($date->getTimestamp());
-}
+++ /dev/null
-<?php
-
-/**
- * This file is part of Twig.
- *
- * (c) 2009 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- *
- * @author Henrik Bjornskov <hb@peytz.dk>
- * @package Twig
- * @subpackage Twig-extensions
- */
-class Twig_Extensions_Extension_Text extends Twig_Extension
-{
- /**
- * Returns a list of filters.
- *
- * @return array
- */
- public function getFilters()
- {
- $filters = array(
- 'truncate' => new Twig_Filter_Function('twig_truncate_filter', array('needs_environment' => true)),
- 'wordwrap' => new Twig_Filter_Function('twig_wordwrap_filter', array('needs_environment' => true)),
- );
-
- if (version_compare(Twig_Environment::VERSION, '1.5.0-DEV', '<')) {
- $filters['nl2br'] = new Twig_Filter_Function('twig_nl2br_filter', array('pre_escape' => 'html', 'is_safe' => array('html')));
- }
-
- return $filters;
- }
-
- /**
- * Name of this extension
- *
- * @return string
- */
- public function getName()
- {
- return 'Text';
- }
-}
-
-function twig_nl2br_filter($value, $sep = '<br />')
-{
- return str_replace("\n", $sep."\n", $value);
-}
-
-if (function_exists('mb_get_info')) {
- function twig_truncate_filter(Twig_Environment $env, $value, $length = 30, $preserve = false, $separator = '...')
- {
- if (mb_strlen($value, $env->getCharset()) > $length) {
- if ($preserve) {
- if (false !== ($breakpoint = mb_strpos($value, ' ', $length, $env->getCharset()))) {
- $length = $breakpoint;
- }
- }
-
- return rtrim(mb_substr($value, 0, $length, $env->getCharset())) . $separator;
- }
-
- return $value;
- }
-
- function twig_wordwrap_filter(Twig_Environment $env, $value, $length = 80, $separator = "\n", $preserve = false)
- {
- $sentences = array();
-
- $previous = mb_regex_encoding();
- mb_regex_encoding($env->getCharset());
-
- $pieces = mb_split($separator, $value);
- mb_regex_encoding($previous);
-
- foreach ($pieces as $piece) {
- while(!$preserve && mb_strlen($piece, $env->getCharset()) > $length) {
- $sentences[] = mb_substr($piece, 0, $length, $env->getCharset());
- $piece = mb_substr($piece, $length, 2048, $env->getCharset());
- }
-
- $sentences[] = $piece;
- }
-
- return implode($separator, $sentences);
- }
-} else {
- function twig_truncate_filter(Twig_Environment $env, $value, $length = 30, $preserve = false, $separator = '...')
- {
- if (strlen($value) > $length) {
- if ($preserve) {
- if (false !== ($breakpoint = strpos($value, ' ', $length))) {
- $length = $breakpoint;
- }
- }
-
- return rtrim(substr($value, 0, $length)) . $separator;
- }
-
- return $value;
- }
-
- function twig_wordwrap_filter(Twig_Environment $env, $value, $length = 80, $separator = "\n", $preserve = false)
- {
- return wordwrap($value, $length, $separator, !$preserve);
- }
-}
\ No newline at end of file
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-abstract class Twig_Extensions_Grammar implements Twig_Extensions_GrammarInterface
-{
- protected $name;
- protected $parser;
-
- public function __construct($name)
- {
- $this->name = $name;
- }
-
- public function setParser(Twig_ParserInterface $parser)
- {
- $this->parser = $parser;
- }
-
- public function getName()
- {
- return $this->name;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Arguments extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:arguments>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- return $this->parser->getExpressionParser()->parseArguments();
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Array extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:array>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- return $this->parser->getExpressionParser()->parseArrayExpression();
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Body extends Twig_Extensions_Grammar
-{
- protected $end;
-
- public function __construct($name, $end = null)
- {
- parent::__construct($name);
-
- $this->end = null === $end ? 'end'.$name : $end;
- }
-
- public function __toString()
- {
- return sprintf('<%s:body>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- $stream = $this->parser->getStream();
- $stream->expect(Twig_Token::BLOCK_END_TYPE);
-
- return $this->parser->subparse(array($this, 'decideBlockEnd'), true);
- }
-
- public function decideBlockEnd(Twig_Token $token)
- {
- return $token->test($this->end);
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Boolean extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:boolean>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, array('true', 'false'));
-
- return new Twig_Node_Expression_Constant('true' === $token->getValue() ? true : false, $token->getLine());
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Constant extends Twig_Extensions_Grammar
-{
- protected $type;
-
- public function __construct($name, $type = null)
- {
- $this->name = $name;
- $this->type = null === $type ? Twig_Token::NAME_TYPE : $type;
- }
-
- public function __toString()
- {
- return $this->name;
- }
-
- public function parse(Twig_Token $token)
- {
- $this->parser->getStream()->expect($this->type, $this->name);
-
- return $this->name;
- }
-
- public function getType()
- {
- return $this->type;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Expression extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- return $this->parser->getExpressionParser()->parseExpression();
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Hash extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:hash>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- return $this->parser->getExpressionParser()->parseHashExpression();
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Number extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:number>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- $this->parser->getStream()->expect(Twig_Token::NUMBER_TYPE);
-
- return new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Optional extends Twig_Extensions_Grammar
-{
- protected $grammar;
-
- public function __construct()
- {
- $this->grammar = array();
- foreach (func_get_args() as $grammar) {
- $this->addGrammar($grammar);
- }
- }
-
- public function __toString()
- {
- $repr = array();
- foreach ($this->grammar as $grammar) {
- $repr[] = (string) $grammar;
- }
-
- return sprintf('[%s]', implode(' ', $repr));
- }
-
- public function addGrammar(Twig_Extensions_GrammarInterface $grammar)
- {
- $this->grammar[] = $grammar;
- }
-
- public function parse(Twig_Token $token)
- {
- // test if we have the optional element before consuming it
- if ($this->grammar[0] instanceof Twig_Extensions_Grammar_Constant) {
- if (!$this->parser->getStream()->test($this->grammar[0]->getType(), $this->grammar[0]->getName())) {
- return array();
- }
- } elseif ($this->grammar[0] instanceof Twig_Extensions_Grammar_Name) {
- if (!$this->parser->getStream()->test(Twig_Token::NAME_TYPE)) {
- return array();
- }
- } elseif ($this->parser->getStream()->test(Twig_Token::BLOCK_END_TYPE)) {
- // if this is not a Constant or a Name, it must be the last element of the tag
-
- return array();
- }
-
- $elements = array();
- foreach ($this->grammar as $grammar) {
- $grammar->setParser($this->parser);
-
- $element = $grammar->parse($token);
- if (is_array($element)) {
- $elements = array_merge($elements, $element);
- } else {
- $elements[$grammar->getName()] = $element;
- }
- }
-
- return $elements;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Switch extends Twig_Extensions_Grammar
-{
- public function __toString()
- {
- return sprintf('<%s:switch>', $this->name);
- }
-
- public function parse(Twig_Token $token)
- {
- $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, $this->name);
-
- return new Twig_Node_Expression_Constant(true, $token->getLine());
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_Grammar_Tag extends Twig_Extensions_Grammar
-{
- protected $grammar;
-
- public function __construct()
- {
- $this->grammar = array();
- foreach (func_get_args() as $grammar) {
- $this->addGrammar($grammar);
- }
- }
-
- public function __toString()
- {
- $repr = array();
- foreach ($this->grammar as $grammar) {
- $repr[] = (string) $grammar;
- }
-
- return implode(' ', $repr);
- }
-
- public function addGrammar(Twig_Extensions_GrammarInterface $grammar)
- {
- $this->grammar[] = $grammar;
- }
-
- public function parse(Twig_Token $token)
- {
- $elements = array();
- foreach ($this->grammar as $grammar) {
- $grammar->setParser($this->parser);
-
- $element = $grammar->parse($token);
- if (is_array($element)) {
- $elements = array_merge($elements, $element);
- } else {
- $elements[$grammar->getName()] = $element;
- }
- }
-
- $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
-
- return $elements;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-interface Twig_Extensions_GrammarInterface
-{
- function setParser(Twig_ParserInterface $parser);
-
- function parse(Twig_Token $token);
-
- function getName();
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Represents a debug node.
- *
- * @package twig
- * @subpackage Twig-extensions
- * @author Fabien Potencier <fabien.potencier@symfony-project.com>
- * @version SVN: $Id$
- */
-class Twig_Extensions_Node_Debug extends Twig_Node
-{
- public function __construct(Twig_Node_Expression $expr = null, $lineno, $tag = null)
- {
- parent::__construct(array('expr' => $expr), array(), $lineno, $tag);
- }
-
- /**
- * Compiles the node to PHP.
- *
- * @param Twig_Compiler A Twig_Compiler instance
- */
- public function compile(Twig_Compiler $compiler)
- {
- $compiler->addDebugInfo($this);
-
- $compiler
- ->write("if (\$this->env->isDebug()) {\n")
- ->indent()
- ;
-
- if (null === $this->getNode('expr')) {
- // remove embedded templates (macros) from the context
- $compiler
- ->write("\$vars = array();\n")
- ->write("foreach (\$context as \$key => \$value) {\n")
- ->indent()
- ->write("if (!\$value instanceof Twig_Template) {\n")
- ->indent()
- ->write("\$vars[\$key] = \$value;\n")
- ->outdent()
- ->write("}\n")
- ->outdent()
- ->write("}\n")
- ->write("var_dump(\$vars);\n")
- ;
- } else {
- $compiler
- ->write("var_dump(")
- ->subcompile($this->getNode('expr'))
- ->raw(");\n")
- ;
- }
-
- $compiler
- ->outdent()
- ->write("}\n")
- ;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Represents a trans node.
- *
- * @package twig
- * @author Fabien Potencier <fabien.potencier@symfony-project.com>
- */
-class Twig_Extensions_Node_Trans extends Twig_Node
-{
- public function __construct(Twig_NodeInterface $body, Twig_NodeInterface $plural = null, Twig_Node_Expression $count = null, $lineno, $tag = null)
- {
- parent::__construct(array('count' => $count, 'body' => $body, 'plural' => $plural), array(), $lineno, $tag);
- }
-
- /**
- * Compiles the node to PHP.
- *
- * @param Twig_Compiler A Twig_Compiler instance
- */
- public function compile(Twig_Compiler $compiler)
- {
- $compiler->addDebugInfo($this);
-
- list($msg, $vars) = $this->compileString($this->getNode('body'));
-
- if (null !== $this->getNode('plural')) {
- list($msg1, $vars1) = $this->compileString($this->getNode('plural'));
-
- $vars = array_merge($vars, $vars1);
- }
-
- $function = null === $this->getNode('plural') ? 'gettext' : 'ngettext';
-
- if ($vars) {
- $compiler
- ->write('echo strtr('.$function.'(')
- ->subcompile($msg)
- ;
-
- if (null !== $this->getNode('plural')) {
- $compiler
- ->raw(', ')
- ->subcompile($msg1)
- ->raw(', abs(')
- ->subcompile($this->getNode('count'))
- ->raw(')')
- ;
- }
-
- $compiler->raw('), array(');
-
- foreach ($vars as $var) {
- if ('count' === $var->getAttribute('name')) {
- $compiler
- ->string('%count%')
- ->raw(' => abs(')
- ->subcompile($this->getNode('count'))
- ->raw('), ')
- ;
- } else {
- $compiler
- ->string('%'.$var->getAttribute('name').'%')
- ->raw(' => ')
- ->subcompile($var)
- ->raw(', ')
- ;
- }
- }
-
- $compiler->raw("));\n");
- } else {
- $compiler
- ->write('echo '.$function.'(')
- ->subcompile($msg)
- ;
-
- if (null !== $this->getNode('plural')) {
- $compiler
- ->raw(', ')
- ->subcompile($msg1)
- ->raw(', abs(')
- ->subcompile($this->getNode('count'))
- ->raw(')')
- ;
- }
-
- $compiler->raw(");\n");
- }
- }
-
- protected function compileString(Twig_NodeInterface $body)
- {
- if ($body instanceof Twig_Node_Expression_Name || $body instanceof Twig_Node_Expression_Constant || $body instanceof Twig_Node_Expression_TempName) {
- return array($body, array());
- }
-
- $vars = array();
- if (count($body)) {
- $msg = '';
-
- foreach ($body as $node) {
- if (get_class($node) === 'Twig_Node' && $node->getNode(0) instanceof Twig_Node_SetTemp) {
- $node = $node->getNode(1);
- }
-
- if ($node instanceof Twig_Node_Print) {
- $n = $node->getNode('expr');
- while ($n instanceof Twig_Node_Expression_Filter) {
- $n = $n->getNode('node');
- }
- $msg .= sprintf('%%%s%%', $n->getAttribute('name'));
- $vars[] = new Twig_Node_Expression_Name($n->getAttribute('name'), $n->getLine());
- } else {
- $msg .= $node->getAttribute('data');
- }
- }
- } else {
- $msg = $body->getAttribute('data');
- }
-
- return array(new Twig_Node(array(new Twig_Node_Expression_Constant(trim($msg), $body->getLine()))), $vars);
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-abstract class Twig_Extensions_SimpleTokenParser extends Twig_TokenParser
-{
- /**
- * Parses a token and returns a node.
- *
- * @param Twig_Token $token A Twig_Token instance
- *
- * @return Twig_NodeInterface A Twig_NodeInterface instance
- */
- public function parse(Twig_Token $token)
- {
- $grammar = $this->getGrammar();
- if (!is_object($grammar)) {
- $grammar = self::parseGrammar($grammar);
- }
-
- $grammar->setParser($this->parser);
- $values = $grammar->parse($token);
-
- return $this->getNode($values, $token->getLine());
- }
-
- /**
- * Gets the grammar as an object or as a string.
- *
- * @return string|Twig_Extensions_Grammar A Twig_Extensions_Grammar instance or a string
- */
- abstract protected function getGrammar();
-
- /**
- * Gets the nodes based on the parsed values.
- *
- * @param array $values An array of values
- * @param integer $line The parser line
- */
- abstract protected function getNode(array $values, $line);
-
- protected function getAttribute($node, $attribute, $arguments = array(), $type = Twig_Node_Expression_GetAttr::TYPE_ANY, $line = -1)
- {
- return new Twig_Node_Expression_GetAttr(
- $node instanceof Twig_NodeInterface ? $node : new Twig_Node_Expression_Name($node, $line),
- $attribute instanceof Twig_NodeInterface ? $attribute : new Twig_Node_Expression_Constant($attribute, $line),
- $arguments instanceof Twig_NodeInterface ? $arguments : new Twig_Node($arguments),
- $type,
- $line
- );
- }
-
- protected function call($node, $attribute, $arguments = array(), $line = -1)
- {
- return $this->getAttribute($node, $attribute, $arguments, Twig_Node_Expression_GetAttr::TYPE_METHOD, $line);
- }
-
- protected function markAsSafe(Twig_NodeInterface $node, $line = -1)
- {
- return new Twig_Node_Expression_Filter(
- $node,
- new Twig_Node_Expression_Constant('raw', $line),
- new Twig_Node(),
- $line
- );
- }
-
- protected function output(Twig_NodeInterface $node, $line = -1)
- {
- return new Twig_Node_Print($node, $line);
- }
-
- protected function getNodeValues(array $values)
- {
- $nodes = array();
- foreach ($values as $value) {
- if ($value instanceof Twig_NodeInterface) {
- $nodes[] = $value;
- }
- }
-
- return $nodes;
- }
-
- static public function parseGrammar($str, $main = true)
- {
- static $cursor;
-
- if (true === $main) {
- $cursor = 0;
- $grammar = new Twig_Extensions_Grammar_Tag();
- } else {
- $grammar = new Twig_Extensions_Grammar_Optional();
- }
-
- while ($cursor < strlen($str)) {
- if (preg_match('/\s+/A', $str, $match, null, $cursor)) {
- $cursor += strlen($match[0]);
- } elseif (preg_match('/<(\w+)(?:\:(\w+))?>/A', $str, $match, null, $cursor)) {
- $class = sprintf('Twig_Extensions_Grammar_%s', ucfirst(isset($match[2]) ? $match[2] : 'Expression'));
- if (!class_exists($class)) {
- throw new Twig_Error_Runtime(sprintf('Unable to understand "%s" in grammar (%s class does not exist)', $match[0], $class));
- }
- $grammar->addGrammar(new $class($match[1]));
- $cursor += strlen($match[0]);
- } elseif (preg_match('/\w+/A', $str, $match, null, $cursor)) {
- $grammar->addGrammar(new Twig_Extensions_Grammar_Constant($match[0]));
- $cursor += strlen($match[0]);
- } elseif (preg_match('/,/A', $str, $match, null, $cursor)) {
- $grammar->addGrammar(new Twig_Extensions_Grammar_Constant($match[0], Twig_Token::PUNCTUATION_TYPE));
- $cursor += strlen($match[0]);
- } elseif (preg_match('/\[/A', $str, $match, null, $cursor)) {
- $cursor += strlen($match[0]);
- $grammar->addGrammar(self::parseGrammar($str, false));
- } elseif (true !== $main && preg_match('/\]/A', $str, $match, null, $cursor)) {
- $cursor += strlen($match[0]);
-
- return $grammar;
- } else {
- throw new Twig_Error_Runtime(sprintf('Unable to parse grammar "%s" near "...%s..."', $str, substr($str, $cursor, 10)));
- }
- }
-
- return $grammar;
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2009-2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_TokenParser_Debug extends Twig_TokenParser
-{
- /**
- * Parses a token and returns a node.
- *
- * @param Twig_Token $token A Twig_Token instance
- *
- * @return Twig_NodeInterface A Twig_NodeInterface instance
- */
- public function parse(Twig_Token $token)
- {
- $lineno = $token->getLine();
-
- $expr = null;
- if (!$this->parser->getStream()->test(Twig_Token::BLOCK_END_TYPE)) {
- $expr = $this->parser->getExpressionParser()->parseExpression();
- }
- $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
-
- return new Twig_Extensions_Node_Debug($expr, $lineno, $this->getTag());
- }
-
- /**
- * Gets the tag name associated with this token parser.
- *
- * @param string The tag name
- */
- public function getTag()
- {
- return 'debug';
- }
-}
+++ /dev/null
-<?php
-
-/*
- * This file is part of Twig.
- *
- * (c) 2010 Fabien Potencier
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-class Twig_Extensions_TokenParser_Trans extends Twig_TokenParser
-{
- /**
- * Parses a token and returns a node.
- *
- * @param Twig_Token $token A Twig_Token instance
- *
- * @return Twig_NodeInterface A Twig_NodeInterface instance
- */
- public function parse(Twig_Token $token)
- {
- $lineno = $token->getLine();
- $stream = $this->parser->getStream();
- $count = null;
- $plural = null;
-
- if (!$stream->test(Twig_Token::BLOCK_END_TYPE)) {
- $body = $this->parser->getExpressionParser()->parseExpression();
- } else {
- $stream->expect(Twig_Token::BLOCK_END_TYPE);
- $body = $this->parser->subparse(array($this, 'decideForFork'));
- if ('plural' === $stream->next()->getValue()) {
- $count = $this->parser->getExpressionParser()->parseExpression();
- $stream->expect(Twig_Token::BLOCK_END_TYPE);
- $plural = $this->parser->subparse(array($this, 'decideForEnd'), true);
- }
- }
-
- $stream->expect(Twig_Token::BLOCK_END_TYPE);
-
- $this->checkTransString($body, $lineno);
-
- return new Twig_Extensions_Node_Trans($body, $plural, $count, $lineno, $this->getTag());
- }
-
- public function decideForFork(Twig_Token $token)
- {
- return $token->test(array('plural', 'endtrans'));
- }
-
- public function decideForEnd(Twig_Token $token)
- {
- return $token->test('endtrans');
- }
-
- /**
- * Gets the tag name associated with this token parser.
- *
- * @param string The tag name
- */
- public function getTag()
- {
- return 'trans';
- }
-
- protected function checkTransString(Twig_NodeInterface $body, $lineno)
- {
- foreach ($body as $i => $node) {
- if (
- $node instanceof Twig_Node_Text
- ||
- ($node instanceof Twig_Node_Print && $node->getNode('expr') instanceof Twig_Node_Expression_Name)
- ) {
- continue;
- }
-
- throw new Twig_Error_Syntax(sprintf('The text to be translated with "trans" can only contain references to simple variables'), $lineno);
- }
- }
-}
+++ /dev/null
-<?php
-
-/**
- * This file is part of the Twig Gettext utility.
- *
- * (c) Саша Стаменковић <umpirsky@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Twig\Gettext;
-
-use Symfony\Component\Filesystem\Filesystem;
-
-/**
- * Extracts translations from twig templates.
- *
- * @author Саша Стаменковић <umpirsky@gmail.com>
- */
-class Extractor
-{
- /**
- * @var \Twig_Environment
- */
- protected $environment;
-
- /**
- * Template cached file names.
- *
- * @var string[]
- */
- protected $templates;
-
- /**
- * Gettext parameters.
- *
- * @var string[]
- */
- protected $parameters;
-
- public function __construct(\Twig_Environment $environment)
- {
- $this->environment = $environment;
- $this->reset();
- }
-
- protected function reset()
- {
- $this->templates = array();
- $this->parameters = array();
- }
-
- public function addTemplate($path)
- {
- $this->environment->loadTemplate($path);
- $this->templates[] = $this->environment->getCacheFilename($path);
- }
-
- public function addGettextParameter($parameter)
- {
- $this->parameters[] = $parameter;
- }
-
- public function setGettextParameters(array $parameters)
- {
- $this->parameters = $parameters;
- }
-
- public function extract()
- {
- $command = 'xgettext';
- $command .= ' '.join(' ', $this->parameters);
- $command .= ' '.join(' ', $this->templates);
-
- $error = 0;
- $output = system($command, $error);
- if (0 !== $error) {
- throw new \RuntimeException(sprintf(
- 'Gettext command "%s" failed with error code %s and output: %s',
- $command,
- $error,
- $output
- ));
- }
-
- $this->reset();
- }
-
- public function __destruct()
- {
- $filesystem = new Filesystem();
- $filesystem->remove($this->environment->getCache());
- }
-}
+++ /dev/null
-<?php
-
-/**
- * This file is part of the Twig Gettext utility.
- *
- * (c) Саша Стаменковић <umpirsky@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Twig\Gettext\Loader;
-
-/**
- * Loads template from the filesystem.
- *
- * @author Саша Стаменковић <umpirsky@gmail.com>
- */
-class Filesystem extends \Twig_Loader_Filesystem
-{
- /**
- * Hacked find template to allow loading templates by absolute path.
- *
- * @param string $name template name or absolute path
- */
- protected function findTemplate($name)
- {
- // normalize name
- $name = preg_replace('#/{2,}#', '/', strtr($name, '\\', '/'));
-
- if (isset($this->cache[$name])) {
- return $this->cache[$name];
- }
-
- $this->validateName($name);
-
- $namespace = '__main__';
- if (isset($name[0]) && '@' == $name[0]) {
- if (false === $pos = strpos($name, '/')) {
- throw new \InvalidArgumentException(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
- }
-
- $namespace = substr($name, 1, $pos - 1);
-
- $name = substr($name, $pos + 1);
- }
-
- if (!isset($this->paths[$namespace])) {
- throw new \Twig_Error_Loader(sprintf('There are no registered paths for namespace "%s".', $namespace));
- }
-
- if (is_file($name)) {
- return $this->cache[$name] = $name;
- }
-
- return __DIR__.'/../Test/Fixtures/twig/empty.twig';
- }
-}
+++ /dev/null
-<?php
-
-/**
- * This file is part of the Twig Gettext utility.
- *
- * (c) Саша Стаменковић <umpirsky@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Twig\Gettext\Routing\Generator;
-
-use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
-use Symfony\Component\Routing\RequestContext;
-
-/**
- * Dummy url generator.
- *
- * @author Саша Стаменковић <umpirsky@gmail.com>
- */
-class UrlGenerator implements UrlGeneratorInterface
-{
- protected $context;
-
- public function generate($name, $parameters = array(), $absolute = false)
- {
- }
-
- public function getContext()
- {
- return $this->context;
- }
-
- public function setContext(RequestContext $context)
- {
- $this->context = $context;
- }
-}
+++ /dev/null
-<?php
-
-/**
- * This file is part of the Twig Gettext utility.
- *
- * (c) Саша Стаменковић <umpirsky@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Twig\Gettext\Test;
-
-use Twig\Gettext\Extractor;
-use Twig\Gettext\Loader\Filesystem;
-use Symfony\Component\Translation\Loader\PoFileLoader;
-
-/**
- * @author Саша Стаменковић <umpirsky@gmail.com>
- */
-class ExtractorTest extends \PHPUnit_Framework_TestCase
-{
- /**
- * @var \Twig_Environment
- */
- protected $twig;
-
- /**
- * @var PoFileLoader
- */
- protected $loader;
-
- protected function setUp()
- {
- $this->twig = new \Twig_Environment(new Filesystem('/'), array(
- 'cache' => '/tmp/cache/'.uniqid(),
- 'auto_reload' => true
- ));
- $this->twig->addExtension(new \Twig_Extensions_Extension_I18n());
-
- $this->loader = new PoFileLoader();
- }
-
- /**
- * @dataProvider testExtractDataProvider
- */
- public function testExtract(array $templates, array $parameters, array $messages)
- {
- $extractor = new Extractor($this->twig);
-
- foreach ($templates as $template) {
- $extractor->addTemplate($template);
- }
- foreach ($parameters as $parameter) {
- $extractor->addGettextParameter($parameter);
- }
-
- $extractor->extract();
-
- $catalog = $this->loader->load($this->getPotFile(), null);
-
- foreach ($messages as $message) {
- $this->assertTrue(
- $catalog->has($message),
- sprintf('Message "%s" not found in catalog.', $message)
- );
- }
- }
-
- public function testExtractDataProvider()
- {
- return array(
- array(
- array(
- __DIR__.'/Fixtures/twig/singular.twig',
- __DIR__.'/Fixtures/twig/plural.twig',
- ),
- $this->getGettextParameters(),
- array(
- 'Hello %name%!',
- 'Hello World!',
- 'Hey %name%, I have one apple.',
- 'Hey %name%, I have %count% apples.',
- ),
- ),
- );
- }
-
- public function testExtractNoTranslations()
- {
- $extractor = new Extractor($this->twig);
-
- $extractor->addTemplate(__DIR__.'/Fixtures/twig/empty.twig');
- $extractor->setGettextParameters($this->getGettextParameters());
-
- $extractor->extract();
-
- $catalog = $this->loader->load($this->getPotFile(), null);
-
- $this->assertEmpty($catalog->all('messages'));
- }
-
- private function getPotFile()
- {
- return __DIR__.'/Fixtures/messages.pot';
- }
-
- private function getGettextParameters()
- {
- return array(
- '--force-po',
- '-o',
- $this->getPotFile(),
- );
- }
-
- protected function tearDown()
- {
- if (file_exists($this->getPotFile())) {
- unlink($this->getPotFile());
- }
- }
-}
+++ /dev/null
-Nothing to translate here.
+++ /dev/null
-{% trans %}
- Hey {{ name }}, I have one apple.
-{% plural apple_count %}
- Hey {{ name }}, I have {{ count }} apples.
-{% endtrans %}
+++ /dev/null
-{% trans "Hello World!" %}
-
-{% trans %}
- Hello World!
-{% endtrans %}
-
-{% trans %}
- Hello {{ name }}!
-{% endtrans %}
$storage_type = 'sqlite'; # sqlite, file
# /!\ Be careful if you change the lines below /!\
-
require_once 'poche/pocheTools.class.php';
require_once 'poche/pocheCore.php';
require_once '3rdparty/Readability.php';
require_once '3rdparty/Encoding.php';
require_once '3rdparty/Session.class.php';
-require_once '3rdparty/Twig/Autoloader.php';
require_once 'store/store.class.php';
require_once 'store/' . $storage_type . '.class.php';
+require_once './vendor/autoload.php';
if (DOWNLOAD_PICTURES) {
require_once 'poche/pochePicture.php';
textdomain(LANG);
# template engine
-Twig_Autoloader::register();
+// Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem(TPL);
$twig = new Twig_Environment($loader, array(
'cache' => CACHE,
include dirname(__FILE__).'/inc/config.php';
-$errors = array();
+$notices = array();
# XSRF protection with token
-if (!empty($_POST)) {
- if (!Session::isToken($_POST['token'])) {
- #die(_('Wrong token'));
- // TODO CORRIGER ICI !!!
- }
- unset($_SESSION['tokens']);
-}
+// if (!empty($_POST)) {
+// if (!Session::isToken($_POST['token'])) {
+// die(_('Wrong token'));
+// // TODO remettre le test
+// }
+// unset($_SESSION['tokens']);
+// }
$referer = empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER'];
if (isset($_GET['login'])) {
+ # hello you
if (!empty($_POST['login']) && !empty($_POST['password'])) {
if (Session::login($_SESSION['login'], $_SESSION['pass'], $_POST['login'], encode_string($_POST['password'] . $_POST['login']))) {
pocheTools::logm('login successful');
- $errors[]['value'] = _('login successful');
+ $pocheTools[]['value'] = _('login successful');
if (!empty($_POST['longlastingsession'])) {
$_SESSION['longlastingsession'] = 31536000;
$_SESSION['expires_on'] = time() + $_SESSION['longlastingsession'];
session_set_cookie_params($_SESSION['longlastingsession']);
} else {
- session_set_cookie_params(0); // when browser closes
+ session_set_cookie_params(0);
}
session_regenerate_id(true);
pocheTools::redirect($referer);
}
pocheTools::logm('login failed');
- $errors[]['value'] = _('Login failed !');
+ $notices[]['value'] = _('Login failed !');
+ pocheTools::redirect();
} else {
pocheTools::logm('login failed');
+ pocheTools::redirect();
}
}
elseif (isset($_GET['logout'])) {
+ # see you soon !
pocheTools::logm('logout');
Session::logout();
pocheTools::redirect();
}
elseif (isset($_GET['config'])) {
+ # Update password
if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
- pocheTools::logm('password updated');
if (!MODE_DEMO) {
+ pocheTools::logm('password updated');
$store->updatePassword(encode_string($_POST['password'] . $_SESSION['login']));
- #your password has been updated
+ Session::logout();
+ pocheTools::redirect();
}
else {
- #in demo mode, you can\'t update password
+ pocheTools::logm('in demo mode, you can\'t do this');
}
}
- #else
- #your password can\'t be empty and you have to repeat it in the second field
}
}
-# Traitement des paramètres et déclenchement des actions
-$view = (isset ($_REQUEST['view'])) ? htmlentities($_REQUEST['view']) : 'home';
-$full_head = (isset ($_REQUEST['full_head'])) ? htmlentities($_REQUEST['full_head']) : 'yes';
-$action = (isset ($_REQUEST['action'])) ? htmlentities($_REQUEST['action']) : '';
-$_SESSION['sort'] = (isset ($_REQUEST['sort'])) ? htmlentities($_REQUEST['sort']) : 'id';
-$id = (isset ($_REQUEST['id'])) ? htmlspecialchars($_REQUEST['id']) : '';
-$url = (isset ($_GET['url'])) ? $_GET['url'] : '';
+# Aaaaaaand action !
+$view = (isset ($_REQUEST['view'])) ? htmlentities($_REQUEST['view']) : 'home';
+$full_head = (isset ($_REQUEST['full_head'])) ? htmlentities($_REQUEST['full_head']) : 'yes';
+$action = (isset ($_REQUEST['action'])) ? htmlentities($_REQUEST['action']) : '';
+$_SESSION['sort'] = (isset ($_REQUEST['sort'])) ? htmlentities($_REQUEST['sort']) : 'id';
+$id = (isset ($_REQUEST['id'])) ? htmlspecialchars($_REQUEST['id']) : '';
+$url = (isset ($_GET['url'])) ? $_GET['url'] : '';
$tpl_vars = array(
'referer' => $referer,
'demo' => MODE_DEMO,
'title' => _('poche, a read it later open source system'),
'token' => Session::getToken(),
- 'errors' => $errors,
+ 'notices' => $notices,
);
if (Session::isLogged()) {
- <link rel="shortcut icon" type="image/x-icon" href="./img/favicon.ico" />
- <link rel="apple-touch-icon-precomposed" sizes="144x144" href="./img/apple-touch-icon-144x144-precomposed.png">
- <link rel="apple-touch-icon-precomposed" sizes="72x72" href="./img/apple-touch-icon-72x72-precomposed.png">
- <link rel="apple-touch-icon-precomposed" href="./img/apple-touch-icon-precomposed.png">
- <link rel="stylesheet" href="./css/knacss.css" media="all">
- <link rel="stylesheet" href="./css/style.css" media="all">
+ <link rel="shortcut icon" type="image/x-icon" href="./tpl/img/favicon.ico" />
+ <link rel="apple-touch-icon-precomposed" sizes="144x144" href="./tpl/img/apple-touch-icon-144x144-precomposed.png">
+ <link rel="apple-touch-icon-precomposed" sizes="72x72" href="./tpl/img/apple-touch-icon-72x72-precomposed.png">
+ <link rel="apple-touch-icon-precomposed" href="./tpl/img/apple-touch-icon-precomposed.png">
+ <link rel="stylesheet" href="./tpl/css/knacss.css" media="all">
+ <link rel="stylesheet" href="./tpl/css/style.css" media="all">
<!-- Light Theme -->
- <link rel="stylesheet" href="./css/style-light.css" media="all" title="light-style">
+ <link rel="stylesheet" href="./tpl/css/style-light.css" media="all" title="light-style">
<!-- Dark Theme -->
- <link rel="alternate stylesheet" href="./css/style-dark.css" media="all" title="dark-style">
\ No newline at end of file
+ <link rel="alternate stylesheet" href="./tpl/css/style-dark.css" media="all" title="dark-style">
\ No newline at end of file
<header>
- <h1><a href="./"><img src="./img/logo.png" alt="logo poche" /></a>poche</h1>
+ <h1><a href="./"><img src="./tpl/img/logo.png" alt="logo poche" /></a>poche</h1>
</header>
\ No newline at end of file
{% endblock %}
{% block content %}
<div id="content">
- <h2>Bookmarklet</h2>
- <p>Thanks to the bookmarklet, you will be able to easily add a link to your poche. If you don't know how use a bookmarklet, <a href="http://support.mozilla.org/en-US/kb/bookmarklets-perform-common-web-page-tasks">have a look here</a>.</p>
- <p>Drag & drop this link to your bookmarks bar and have fun with poche.</p>
- <p><a ondragend="this.click();" style="cursor: move; border: 1px dashed grey; background: white;" title="i am a bookmarklet, use me !" href="javascript:if(top['bookmarklet-url@inthepoche.com']){top['bookmarklet-url@inthepoche.com'];}else{(function(){var%20url%20=%20location.href%20||%20url;window.open('{$poche_url}?action=add&url='%20+%20btoa(url),'_self');})();void(0);}">poche it !</a></p>
+ <h2>{% trans "Bookmarklet" %}</h2>
+ <p>{% trans "Thanks to the bookmarklet, you will be able to easily add a link to your poche." %} {% trans "Have a look to this documentation:" %} <a href="http://inthepoche.com/?pages/Documentation" target="_blank">http://inthepoche.com/?pages/Documentation</a>.</p>
+ <p>{% trans "Drag & drop this link to your bookmarks bar and have fun with poche." %}</p>
+ <p><a ondragend="this.click();" style="cursor: move; border: 1px dashed grey; background: white;" title="i am a bookmarklet, use me !" href="javascript:if(top['bookmarklet-url@inthepoche.com']){top['bookmarklet-url@inthepoche.com'];}else{(function(){var%20url%20=%20location.href%20||%20url;window.open('{$poche_url}?action=add&url='%20+%20btoa(url),'_self');})();void(0);}">{% trans "poche it!" %}</a></p>
- <h2>Password</h2>
+ <h2>{% trans "Change your password" %}</h2>
<form method="post" action="?config" name="loginform">
<fieldset class="w500p">
<div class="row">
- <label class="col w150p" for="password">New password</label>
- <input class="col" type="password" id="password" name="password" placeholder="Password" tabindex="2">
+ <label class="col w150p" for="password">{% trans "New password" %}</label>
+ <input class="col" type="password" id="password" name="password" placeholder="{% trans "Password" %}" tabindex="2">
</div>
<div class="row">
- <label class="col w150p" for="password_repeat">Repeat your new password</label>
- <input class="col" type="password" id="password_repeat" name="password_repeat" placeholder="Password" tabindex="3">
+ <label class="col w150p" for="password_repeat">{% trans "Repeat your new password" %}</label>
+ <input class="col" type="password" id="password_repeat" name="password_repeat" placeholder="{% trans "Password" %}" tabindex="3">
</div>
<div class="row mts txtcenter">
- <button class="bouton" type="submit" tabindex="4">Update</button>
+ <button class="bouton" type="submit" tabindex="4">{% trans "Update" %}</button>
</div>
</fieldset>
- <input type="hidden" name="returnurl" value="<?php echo htmlspecialchars($referer);?>">
- <input type="hidden" name="token" value="<?php echo Session::getToken(); ?>">
+ <input type="hidden" name="returnurl" value="{{ referer }}">
+ <input type="hidden" name="token" value="{{ token }}">
</form>
- <h2>Export</h2>
- <p><a href="?view=export" target="_blank">Click here</a> to export your poche datas.</p>
+ <h2>{% trans "Export your poche datas" %}</h2>
+ <p><a href="?view=export" target="_blank">{% trans "Click here" %}</a> {% trans "to export your poche datas." %}</p>
</div>
{% endblock %}
\ No newline at end of file
+++ /dev/null
- <div id="content">
- {loop="entries"}
- <div id="entry-{$value.id}" class="entrie mb2">
- <span class="content">
- <h2 class="h6-like">
- <a href="index.php?&view=view&id={$value.id}">{$value.title}</a>
- </h2>
- <div class="tools">
- <ul>
- <li>
- <a title="toggle mark as read" class="tool archive {if="$value.is_read == '0'"}archive-off{/if}" onclick="toggle_archive(this, {$value.id})"><span></span></a></li>
- <li><a title="toggle favorite" class="tool fav {if="$value.is_fav == '0'"}fav-off{/if}" onclick="toggle_favorite(this, {$value.id})"><span></span></a></li>
- <li><form method="post" onsubmit="return confirm('Are you sure?')" style="display: inline;"><input type="hidden" name="token" id="token" value="<?php echo Session::getToken(); ?>" /><input type="hidden" id="action" name="action" value="delete" /><input type="hidden" id="view" name="view" value="{$view}" /><input type="hidden" id="id" name="id" value="{$value.id}" /><input type="submit" class="delete" title="toggle delete" /></form>
- </li>
- </ul>
- </div>
- <div class="url">{$value.url}</div>
- </span>
- </div>
- {/loop}
- </div>
\ No newline at end of file
{% endblock %}
{% block precontent %}
<ul id="sort">
- <li><img src="img/up.png" onclick="sort_links('{{ view }}', 'ia');" title="{% trans "by date asc" %}" /> {% trans "by date" %} <img src="img/down.png" onclick="sort_links('{{ view }}', 'id');" title="{% trans "by date desc" %}" /></li>
- <li><img src="img/up.png" onclick="sort_links('{{ view }}', 'ta');" title="{% trans "by title asc" %}" /> {% trans "by title" %} <img src="img/down.png" onclick="sort_links('{{ view }}', 'td');" title="{% trans "by title desc" %}" /></li>
+ <li><img src="./tpl/img/up.png" onclick="sort_links('{{ view }}', 'ia');" title="{% trans "by date asc" %}" /> {% trans "by date" %} <img src="./tpl/img/down.png" onclick="sort_links('{{ view }}', 'id');" title="{% trans "by date desc" %}" /></li>
+ <li><img src="./tpl/img/up.png" onclick="sort_links('{{ view }}', 'ta');" title="{% trans "by title asc" %}" /> {% trans "by title" %} <img src="./tpl/img/down.png" onclick="sort_links('{{ view }}', 'td');" title="{% trans "by title desc" %}" /></li>
</ul>
{% endblock %}
+{% block notices %}
+ <div class="messages">
+ <ul>
+ {% for notice in notices %}
+ <li>{{ notice.value|e }}</li>
+ {% endfor %}
+ </ul>
+ </div>
+{% endblock %}
{% block content %}
<div id="content">
{% for entry in entries %}
<li>
<a title="{% trans "toggle mark as read" %}" class="tool archive {% if entry.is_read == 0 %}archive-off{% endif %}" onclick="toggle_archive(this, {{ entry.id|e }})"><span></span></a></li>
<li><a title="{% trans "toggle favorite" %}" class="tool fav {% if entry.is_fav == 0 %}fav-off{% endif %}" onclick="toggle_favorite(this, {{ entry.id|e }})"><span></span></a></li>
- <li><form method="post" onsubmit="return confirm('{% trans "are you sure?" %}')" style="display: inline;"><input type="hidden" name="token" id="token" value="{{ token }}" /><input type="hidden" id="action" name="action" value="delete" /><input type="hidden" id="view" name="view" value="{{ view }}" /><input type="hidden" id="id" name="id" value="{{ entry.id|e }}" /><input type="submit" class="delete" title="{% trans "toggle delete" %}" /></form>
+ <li><form method="post" style="display: inline;"><input type="hidden" name="token" id="token" value="{{ token }}" /><input type="hidden" id="action" name="action" value="delete" /><input type="hidden" id="view" name="view" value="{{ view }}" /><input type="hidden" id="id" name="id" value="{{ entry.id|e }}" /><input type="submit" class="delete" title="{% trans "toggle delete" %}" /></form>
</li>
</ul>
</div>
{% endblock %}
{% block js %}
- <script type="text/javascript" src="js/jquery-1.9.1.min.js"></script>
- <script type="text/javascript" src="js/poche.js"></script>
- <script type="text/javascript" src="js/jquery.masonry.min.js"></script>
+ <script type="text/javascript" src="./tpl/js/jquery-1.9.1.min.js"></script>
+ <script type="text/javascript" src="./tpl/js/poche.js"></script>
+ <script type="text/javascript" src="./tpl/js/jquery.masonry.min.js"></script>
<script type="text/javascript">
$( window ).load( function()
{
{% extends "layout.twig" %}
{% block title %}{% trans "login to your poche" %}{% endblock %}
-{% block messages %}
+{% block notices %}
<div class="messages">
<ul>
- {% for error in errors %}
- <li>{{ error.value|e }}</li>
+ {% for notice in notices %}
+ <li>{{ notice.value|e }}</li>
{% endfor %}
</ul>
</div>
<label class="col w150p">{% trans "Stay signed in" %}</label>
<div class="col">
<input type="checkbox" name="longlastingsession" tabindex="3">
- <small class="inbl">(Do not check on public computers)</small>
+ <small class="inbl">{% trans "(Do not check on public computers)" %}</small>
</div>
</div>
<div class="row mts txtcenter">
--- /dev/null
+<?php
+
+// autoload.php generated by Composer
+
+require_once __DIR__ . '/composer' . '/autoload_real.php';
+
+return ComposerAutoloaderInit1c7743925d207055d2ad189b1f10a029::getLoader();
--- /dev/null
+../umpirsky/twig-gettext-extractor/twig-gettext-extractor
\ No newline at end of file
--- /dev/null
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0 class loader
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class ClassLoader
+{
+ private $prefixes = array();
+ private $fallbackDirs = array();
+ private $useIncludePath = false;
+ private $classMap = array();
+
+ public function getPrefixes()
+ {
+ return call_user_func_array('array_merge', $this->prefixes);
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirs;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of classes, merging with any others previously set.
+ *
+ * @param string $prefix The classes prefix
+ * @param array|string $paths The location(s) of the classes
+ * @param bool $prepend Prepend the location(s)
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirs = array_merge(
+ (array) $paths,
+ $this->fallbackDirs
+ );
+ } else {
+ $this->fallbackDirs = array_merge(
+ $this->fallbackDirs,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixes[$first][$prefix])) {
+ $this->prefixes[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixes[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixes[$first][$prefix]
+ );
+ } else {
+ $this->prefixes[$first][$prefix] = array_merge(
+ $this->prefixes[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of classes, replacing any others previously set.
+ *
+ * @param string $prefix The classes prefix
+ * @param array|string $paths The location(s) of the classes
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirs = (array) $paths;
+
+ return;
+ }
+ $this->prefixes[substr($prefix, 0, 1)][$prefix] = (array) $paths;
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ include $file;
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
+ if ('\\' == $class[0]) {
+ $class = substr($class, 1);
+ }
+
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $classPath = strtr(substr($class, 0, $pos), '\\', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ $className = substr($class, $pos + 1);
+ } else {
+ // PEAR-like class name
+ $classPath = null;
+ $className = $class;
+ }
+
+ $classPath .= strtr($className, '_', DIRECTORY_SEPARATOR) . '.php';
+
+ $first = $class[0];
+ if (isset($this->prefixes[$first])) {
+ foreach ($this->prefixes[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
+ return $dir . DIRECTORY_SEPARATOR . $classPath;
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($this->fallbackDirs as $dir) {
+ if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
+ return $dir . DIRECTORY_SEPARATOR . $classPath;
+ }
+ }
+
+ if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) {
+ return $file;
+ }
+
+ return $this->classMap[$class] = false;
+ }
+}
--- /dev/null
+<?php
+
+// autoload_classmap.php generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ 'Collator' => $vendorDir . '/symfony/intl/Symfony/Component/Intl/Resources/stubs/Collator.php',
+ 'IntlDateFormatter' => $vendorDir . '/symfony/intl/Symfony/Component/Intl/Resources/stubs/IntlDateFormatter.php',
+ 'Locale' => $vendorDir . '/symfony/intl/Symfony/Component/Intl/Resources/stubs/Locale.php',
+ 'NumberFormatter' => $vendorDir . '/symfony/intl/Symfony/Component/Intl/Resources/stubs/NumberFormatter.php',
+);
--- /dev/null
+<?php
+
+// autoload_files.php generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ $vendorDir . '/symfony/intl/Symfony/Component/Intl/Resources/stubs/functions.php',
+);
\ No newline at end of file
--- /dev/null
+<?php
+
+// autoload_namespaces.php generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ 'Twig_Extensions_' => array($vendorDir . '/twig/extensions/lib'),
+ 'Twig_' => array($vendorDir . '/twig/twig/lib'),
+ 'Twig\\Gettext' => array($vendorDir . '/umpirsky/twig-gettext-extractor'),
+ 'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
+ 'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'),
+ 'Symfony\\Component\\PropertyAccess\\' => array($vendorDir . '/symfony/property-access'),
+ 'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'),
+ 'Symfony\\Component\\Intl\\' => array($vendorDir . '/symfony/intl'),
+ 'Symfony\\Component\\Icu\\' => array($vendorDir . '/symfony/icu'),
+ 'Symfony\\Component\\Form\\' => array($vendorDir . '/symfony/form'),
+ 'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
+ 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
+ 'Symfony\\Bridge\\Twig\\' => array($vendorDir . '/symfony/twig-bridge'),
+);
--- /dev/null
+<?php
+
+// autoload_real.php generated by Composer
+
+class ComposerAutoloaderInit1c7743925d207055d2ad189b1f10a029
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInit1c7743925d207055d2ad189b1f10a029', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+ spl_autoload_unregister(array('ComposerAutoloaderInit1c7743925d207055d2ad189b1f10a029', 'loadClassLoader'));
+
+ $vendorDir = dirname(__DIR__);
+ $baseDir = dirname($vendorDir);
+
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+
+ $loader->register(true);
+
+ foreach (require __DIR__ . '/autoload_files.php' as $file) {
+ require $file;
+ }
+
+ return $loader;
+ }
+}
--- /dev/null
+[
+ {
+ "name": "twig/twig",
+ "version": "v1.13.2",
+ "version_normalized": "1.13.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fabpot/Twig.git",
+ "reference": "v1.13.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fabpot/Twig/zipball/v1.13.2",
+ "reference": "v1.13.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2.4"
+ },
+ "time": "2013-08-03 15:35:31",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.13-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Twig_": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "http://twig.sensiolabs.org",
+ "keywords": [
+ "templating"
+ ]
+ },
+ {
+ "name": "twig/extensions",
+ "version": "dev-master",
+ "version_normalized": "9999999-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fabpot/Twig-extensions.git",
+ "reference": "f5b0c84f3699e494c84ee627d7d583e115d2c4a2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fabpot/Twig-extensions/zipball/f5b0c84f3699e494c84ee627d7d583e115d2c4a2",
+ "reference": "f5b0c84f3699e494c84ee627d7d583e115d2c4a2",
+ "shasum": ""
+ },
+ "require": {
+ "twig/twig": "~1.0"
+ },
+ "time": "2013-07-02 11:21:55",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "installation-source": "source",
+ "autoload": {
+ "psr-0": {
+ "Twig_Extensions_": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ }
+ ],
+ "description": "Common additional features for Twig that do not directly belong in core",
+ "homepage": "https://github.com/fabpot/Twig-extensions",
+ "keywords": [
+ "debug",
+ "i18n",
+ "text"
+ ]
+ },
+ {
+ "name": "symfony/icu",
+ "version": "v1.0.0",
+ "version_normalized": "1.0.0.0",
+ "target-dir": "Symfony/Component/Icu",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Icu.git",
+ "reference": "v1.0.0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Icu/zipball/v1.0.0",
+ "reference": "v1.0.0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/intl": ">=2.3,<3.0"
+ },
+ "time": "2013-06-03 18:32:07",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Icu\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Contains an excerpt of the ICU data and classes to load it.",
+ "homepage": "http://symfony.com",
+ "keywords": [
+ "icu",
+ "intl"
+ ]
+ },
+ {
+ "name": "symfony/intl",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/Intl",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Intl.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Intl/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/icu": "~1.0-RC"
+ },
+ "require-dev": {
+ "symfony/filesystem": ">=2.1"
+ },
+ "suggest": {
+ "ext-intl": "to use the component with locales other than \"en\""
+ },
+ "time": "2013-07-08 13:00:35",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Intl\\": ""
+ },
+ "classmap": [
+ "Symfony/Component/Intl/Resources/stubs"
+ ],
+ "files": [
+ "Symfony/Component/Intl/Resources/stubs/functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch",
+ "homepage": "http://wiedler.ch/igor/"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ },
+ {
+ "name": "Eriksen Costa",
+ "email": "eriksen.costa@infranology.com.br"
+ }
+ ],
+ "description": "A PHP replacement layer for the C intl extension that includes additional data from the ICU library.",
+ "homepage": "http://symfony.com",
+ "keywords": [
+ "i18n",
+ "icu",
+ "internationalization",
+ "intl",
+ "l10n",
+ "localization"
+ ]
+ },
+ {
+ "name": "symfony/property-access",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/PropertyAccess",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/PropertyAccess.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/PropertyAccess/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "time": "2013-07-01 12:24:43",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\PropertyAccess\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony PropertyAccess Component",
+ "homepage": "http://symfony.com",
+ "keywords": [
+ "access",
+ "array",
+ "extraction",
+ "index",
+ "injection",
+ "object",
+ "property",
+ "property path",
+ "reflection"
+ ]
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/OptionsResolver",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/OptionsResolver.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/OptionsResolver/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "time": "2013-04-11 06:50:46",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony OptionsResolver Component",
+ "homepage": "http://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ]
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/EventDispatcher",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/EventDispatcher.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "symfony/dependency-injection": "~2.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "time": "2013-05-13 14:36:40",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony EventDispatcher Component",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "symfony/form",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/Form",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Form.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Form/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/event-dispatcher": "~2.1",
+ "symfony/intl": "~2.3",
+ "symfony/options-resolver": "~2.1",
+ "symfony/property-access": "~2.2"
+ },
+ "require-dev": {
+ "symfony/http-foundation": "~2.2",
+ "symfony/validator": "~2.2"
+ },
+ "suggest": {
+ "symfony/http-foundation": "",
+ "symfony/validator": ""
+ },
+ "time": "2013-07-01 12:24:43",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Form\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Form Component",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/Translation",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Translation.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Translation/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "symfony/config": "~2.0",
+ "symfony/yaml": "~2.2"
+ },
+ "suggest": {
+ "symfony/config": "",
+ "symfony/yaml": ""
+ },
+ "time": "2013-05-13 14:36:40",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Translation\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Translation Component",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/Filesystem",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Filesystem.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Filesystem/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "time": "2013-06-04 15:02:05",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Filesystem\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Filesystem Component",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "symfony/routing",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Component/Routing",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/Routing.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/Routing/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "doctrine/common": "~2.2",
+ "psr/log": "~1.0",
+ "symfony/config": "~2.2",
+ "symfony/yaml": "~2.0"
+ },
+ "suggest": {
+ "doctrine/common": "",
+ "symfony/config": "",
+ "symfony/yaml": ""
+ },
+ "time": "2013-06-23 08:16:02",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Component\\Routing\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Routing Component",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v2.3.2",
+ "version_normalized": "2.3.2.0",
+ "target-dir": "Symfony/Bridge/Twig",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/TwigBridge.git",
+ "reference": "v2.3.2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/TwigBridge/zipball/v2.3.2",
+ "reference": "v2.3.2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "twig/twig": "~1.11"
+ },
+ "require-dev": {
+ "symfony/form": "2.2.*",
+ "symfony/http-kernel": "~2.2",
+ "symfony/routing": "~2.2",
+ "symfony/security": "~2.0",
+ "symfony/templating": "~2.1",
+ "symfony/translation": "~2.2",
+ "symfony/yaml": "~2.0"
+ },
+ "suggest": {
+ "symfony/form": "",
+ "symfony/http-kernel": "",
+ "symfony/routing": "",
+ "symfony/security": "",
+ "symfony/templating": "",
+ "symfony/translation": "",
+ "symfony/yaml": ""
+ },
+ "time": "2013-05-16 10:19:58",
+ "type": "symfony-bridge",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Symfony\\Bridge\\Twig\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Twig Bridge",
+ "homepage": "http://symfony.com"
+ },
+ {
+ "name": "umpirsky/twig-gettext-extractor",
+ "version": "1.1.3",
+ "version_normalized": "1.1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/umpirsky/Twig-Gettext-Extractor.git",
+ "reference": "1.1.3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/umpirsky/Twig-Gettext-Extractor/zipball/1.1.3",
+ "reference": "1.1.3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/filesystem": ">=2.0,<3.0",
+ "symfony/form": ">=2.0,<3.0",
+ "symfony/routing": ">=2.0,<3.0",
+ "symfony/translation": ">=2.0,<3.0",
+ "symfony/twig-bridge": ">=2.0,<3.0",
+ "twig/extensions": "1.0.*",
+ "twig/twig": ">=1.2.0,<2.0-dev"
+ },
+ "require-dev": {
+ "symfony/config": "2.1.*"
+ },
+ "time": "2013-02-14 16:41:48",
+ "bin": [
+ "twig-gettext-extractor"
+ ],
+ "type": "application",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Twig\\Gettext": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Саша Стаменковић",
+ "email": "umpirsky@gmail.com",
+ "homepage": "http://umpirsky.com"
+ }
+ ],
+ "description": "The Twig Gettext Extractor is Poedit friendly tool which extracts translations from twig templates."
+ }
+]
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+CHANGELOG
+=========
+
+2.1.0
+-----
+
+ * added TraceableEventDispatcherInterface
+ * added ContainerAwareEventDispatcher
+ * added a reference to the EventDispatcher on the Event
+ * added a reference to the Event name on the event
+ * added fluid interface to the dispatch() method which now returns the Event
+ object
+ * added GenericEvent event class
+ * added the possibility for subscribers to subscribe several times for the
+ same event
+ * added ImmutableEventDispatcher
--- /dev/null
+<?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\EventDispatcher;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Lazily loads listeners and subscribers from the dependency injection
+ * container
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Jordan Alliot <jordan.alliot@gmail.com>
+ */
+class ContainerAwareEventDispatcher extends EventDispatcher
+{
+ /**
+ * The container from where services are loaded
+ * @var ContainerInterface
+ */
+ private $container;
+
+ /**
+ * The service IDs of the event listeners and subscribers
+ * @var array
+ */
+ private $listenerIds = array();
+
+ /**
+ * The services registered as listeners
+ * @var array
+ */
+ private $listeners = array();
+
+ /**
+ * Constructor.
+ *
+ * @param ContainerInterface $container A ContainerInterface instance
+ */
+ public function __construct(ContainerInterface $container)
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * Adds a service as event listener
+ *
+ * @param string $eventName Event for which the listener is added
+ * @param array $callback The service ID of the listener service & the method
+ * name that has to be called
+ * @param integer $priority The higher this value, the earlier an event listener
+ * will be triggered in the chain.
+ * Defaults to 0.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addListenerService($eventName, $callback, $priority = 0)
+ {
+ if (!is_array($callback) || 2 !== count($callback)) {
+ throw new \InvalidArgumentException('Expected an array("service", "method") argument');
+ }
+
+ $this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority);
+ }
+
+ public function removeListener($eventName, $listener)
+ {
+ $this->lazyLoad($eventName);
+
+ if (isset($this->listeners[$eventName])) {
+ foreach ($this->listeners[$eventName] as $key => $l) {
+ foreach ($this->listenerIds[$eventName] as $i => $args) {
+ list($serviceId, $method, $priority) = $args;
+ if ($key === $serviceId.'.'.$method) {
+ if ($listener === array($l, $method)) {
+ unset($this->listeners[$eventName][$key]);
+ if (empty($this->listeners[$eventName])) {
+ unset($this->listeners[$eventName]);
+ }
+ unset($this->listenerIds[$eventName][$i]);
+ if (empty($this->listenerIds[$eventName])) {
+ unset($this->listenerIds[$eventName]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ parent::removeListener($eventName, $listener);
+ }
+
+ /**
+ * @see EventDispatcherInterface::hasListeners
+ */
+ public function hasListeners($eventName = null)
+ {
+ if (null === $eventName) {
+ return (Boolean) count($this->listenerIds) || (Boolean) count($this->listeners);
+ }
+
+ if (isset($this->listenerIds[$eventName])) {
+ return true;
+ }
+
+ return parent::hasListeners($eventName);
+ }
+
+ /**
+ * @see EventDispatcherInterface::getListeners
+ */
+ public function getListeners($eventName = null)
+ {
+ if (null === $eventName) {
+ foreach (array_keys($this->listenerIds) as $serviceEventName) {
+ $this->lazyLoad($serviceEventName);
+ }
+ } else {
+ $this->lazyLoad($eventName);
+ }
+
+ return parent::getListeners($eventName);
+ }
+
+ /**
+ * Adds a service as event subscriber
+ *
+ * @param string $serviceId The service ID of the subscriber service
+ * @param string $class The service's class name (which must implement EventSubscriberInterface)
+ */
+ public function addSubscriberService($serviceId, $class)
+ {
+ foreach ($class::getSubscribedEvents() as $eventName => $params) {
+ if (is_string($params)) {
+ $this->listenerIds[$eventName][] = array($serviceId, $params, 0);
+ } elseif (is_string($params[0])) {
+ $this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0);
+ } else {
+ foreach ($params as $listener) {
+ $this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Lazily loads listeners for this event from the dependency injection
+ * container.
+ *
+ * @throws \InvalidArgumentException if the service is not defined
+ */
+ public function dispatch($eventName, Event $event = null)
+ {
+ $this->lazyLoad($eventName);
+
+ return parent::dispatch($eventName, $event);
+ }
+
+ public function getContainer()
+ {
+ return $this->container;
+ }
+
+ /**
+ * Lazily loads listeners for this event from the dependency injection
+ * container.
+ *
+ * @param string $eventName The name of the event to dispatch. The name of
+ * the event is the name of the method that is
+ * invoked on listeners.
+ */
+ protected function lazyLoad($eventName)
+ {
+ if (isset($this->listenerIds[$eventName])) {
+ foreach ($this->listenerIds[$eventName] as $args) {
+ list($serviceId, $method, $priority) = $args;
+ $listener = $this->container->get($serviceId);
+
+ $key = $serviceId.'.'.$method;
+ if (!isset($this->listeners[$eventName][$key])) {
+ $this->addListener($eventName, array($listener, $method), $priority);
+ } elseif ($listener !== $this->listeners[$eventName][$key]) {
+ parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method));
+ $this->addListener($eventName, array($listener, $method), $priority);
+ }
+
+ $this->listeners[$eventName][$key] = $listener;
+ }
+ }
+ }
+}
--- /dev/null
+<?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\EventDispatcher\Debug;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface TraceableEventDispatcherInterface
+{
+ /**
+ * Gets the called listeners.
+ *
+ * @return array An array of called listeners
+ */
+ public function getCalledListeners();
+
+ /**
+ * Gets the not called listeners.
+ *
+ * @return array An array of not called listeners
+ */
+ public function getNotCalledListeners();
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * Event is the base class for classes containing event data.
+ *
+ * This class contains no event data. It is used by events that do not pass
+ * state information to an event handler when an event is raised.
+ *
+ * You can call the method stopPropagation() to abort the execution of
+ * further listeners in your event listener.
+ *
+ * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
+ * @author Jonathan Wage <jonwage@gmail.com>
+ * @author Roman Borschel <roman@code-factory.org>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @api
+ */
+class Event
+{
+ /**
+ * @var Boolean Whether no further event listeners should be triggered
+ */
+ private $propagationStopped = false;
+
+ /**
+ * @var EventDispatcher Dispatcher that dispatched this event
+ */
+ private $dispatcher;
+
+ /**
+ * @var string This event's name
+ */
+ private $name;
+
+ /**
+ * Returns whether further event listeners should be triggered.
+ *
+ * @see Event::stopPropagation
+ * @return Boolean Whether propagation was already stopped for this event.
+ *
+ * @api
+ */
+ public function isPropagationStopped()
+ {
+ return $this->propagationStopped;
+ }
+
+ /**
+ * Stops the propagation of the event to further event listeners.
+ *
+ * If multiple event listeners are connected to the same event, no
+ * further event listener will be triggered once any trigger calls
+ * stopPropagation().
+ *
+ * @api
+ */
+ public function stopPropagation()
+ {
+ $this->propagationStopped = true;
+ }
+
+ /**
+ * Stores the EventDispatcher that dispatches this Event
+ *
+ * @param EventDispatcherInterface $dispatcher
+ *
+ * @api
+ */
+ public function setDispatcher(EventDispatcherInterface $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * Returns the EventDispatcher that dispatches this Event
+ *
+ * @return EventDispatcherInterface
+ *
+ * @api
+ */
+ public function getDispatcher()
+ {
+ return $this->dispatcher;
+ }
+
+ /**
+ * Gets the event's name.
+ *
+ * @return string
+ *
+ * @api
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets the event's name property.
+ *
+ * @param string $name The event name.
+ *
+ * @api
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * The EventDispatcherInterface is the central point of Symfony's event listener system.
+ *
+ * Listeners are registered on the manager and events are dispatched through the
+ * manager.
+ *
+ * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
+ * @author Jonathan Wage <jonwage@gmail.com>
+ * @author Roman Borschel <roman@code-factory.org>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Jordan Alliot <jordan.alliot@gmail.com>
+ *
+ * @api
+ */
+class EventDispatcher implements EventDispatcherInterface
+{
+ private $listeners = array();
+ private $sorted = array();
+
+ /**
+ * @see EventDispatcherInterface::dispatch
+ *
+ * @api
+ */
+ public function dispatch($eventName, Event $event = null)
+ {
+ if (null === $event) {
+ $event = new Event();
+ }
+
+ $event->setDispatcher($this);
+ $event->setName($eventName);
+
+ if (!isset($this->listeners[$eventName])) {
+ return $event;
+ }
+
+ $this->doDispatch($this->getListeners($eventName), $eventName, $event);
+
+ return $event;
+ }
+
+ /**
+ * @see EventDispatcherInterface::getListeners
+ */
+ public function getListeners($eventName = null)
+ {
+ if (null !== $eventName) {
+ if (!isset($this->sorted[$eventName])) {
+ $this->sortListeners($eventName);
+ }
+
+ return $this->sorted[$eventName];
+ }
+
+ foreach (array_keys($this->listeners) as $eventName) {
+ if (!isset($this->sorted[$eventName])) {
+ $this->sortListeners($eventName);
+ }
+ }
+
+ return $this->sorted;
+ }
+
+ /**
+ * @see EventDispatcherInterface::hasListeners
+ */
+ public function hasListeners($eventName = null)
+ {
+ return (Boolean) count($this->getListeners($eventName));
+ }
+
+ /**
+ * @see EventDispatcherInterface::addListener
+ *
+ * @api
+ */
+ public function addListener($eventName, $listener, $priority = 0)
+ {
+ $this->listeners[$eventName][$priority][] = $listener;
+ unset($this->sorted[$eventName]);
+ }
+
+ /**
+ * @see EventDispatcherInterface::removeListener
+ */
+ public function removeListener($eventName, $listener)
+ {
+ if (!isset($this->listeners[$eventName])) {
+ return;
+ }
+
+ foreach ($this->listeners[$eventName] as $priority => $listeners) {
+ if (false !== ($key = array_search($listener, $listeners, true))) {
+ unset($this->listeners[$eventName][$priority][$key], $this->sorted[$eventName]);
+ }
+ }
+ }
+
+ /**
+ * @see EventDispatcherInterface::addSubscriber
+ *
+ * @api
+ */
+ public function addSubscriber(EventSubscriberInterface $subscriber)
+ {
+ foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
+ if (is_string($params)) {
+ $this->addListener($eventName, array($subscriber, $params));
+ } elseif (is_string($params[0])) {
+ $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
+ } else {
+ foreach ($params as $listener) {
+ $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * @see EventDispatcherInterface::removeSubscriber
+ */
+ public function removeSubscriber(EventSubscriberInterface $subscriber)
+ {
+ foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
+ if (is_array($params) && is_array($params[0])) {
+ foreach ($params as $listener) {
+ $this->removeListener($eventName, array($subscriber, $listener[0]));
+ }
+ } else {
+ $this->removeListener($eventName, array($subscriber, is_string($params) ? $params : $params[0]));
+ }
+ }
+ }
+
+ /**
+ * Triggers the listeners of an event.
+ *
+ * This method can be overridden to add functionality that is executed
+ * for each listener.
+ *
+ * @param array[callback] $listeners The event listeners.
+ * @param string $eventName The name of the event to dispatch.
+ * @param Event $event The event object to pass to the event handlers/listeners.
+ */
+ protected function doDispatch($listeners, $eventName, Event $event)
+ {
+ foreach ($listeners as $listener) {
+ call_user_func($listener, $event);
+ if ($event->isPropagationStopped()) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Sorts the internal list of listeners for the given event by priority.
+ *
+ * @param string $eventName The name of the event.
+ */
+ private function sortListeners($eventName)
+ {
+ $this->sorted[$eventName] = array();
+
+ if (isset($this->listeners[$eventName])) {
+ krsort($this->listeners[$eventName]);
+ $this->sorted[$eventName] = call_user_func_array('array_merge', $this->listeners[$eventName]);
+ }
+ }
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * The EventDispatcherInterface is the central point of Symfony's event listener system.
+ * Listeners are registered on the manager and events are dispatched through the
+ * manager.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @api
+ */
+interface EventDispatcherInterface
+{
+ /**
+ * Dispatches an event to all registered listeners.
+ *
+ * @param string $eventName The name of the event to dispatch. The name of
+ * the event is the name of the method that is
+ * invoked on listeners.
+ * @param Event $event The event to pass to the event handlers/listeners.
+ * If not supplied, an empty Event instance is created.
+ *
+ * @return Event
+ *
+ * @api
+ */
+ public function dispatch($eventName, Event $event = null);
+
+ /**
+ * Adds an event listener that listens on the specified events.
+ *
+ * @param string $eventName The event to listen on
+ * @param callable $listener The listener
+ * @param integer $priority The higher this value, the earlier an event
+ * listener will be triggered in the chain (defaults to 0)
+ *
+ * @api
+ */
+ public function addListener($eventName, $listener, $priority = 0);
+
+ /**
+ * Adds an event subscriber.
+ *
+ * The subscriber is asked for all the events he is
+ * interested in and added as a listener for these events.
+ *
+ * @param EventSubscriberInterface $subscriber The subscriber.
+ *
+ * @api
+ */
+ public function addSubscriber(EventSubscriberInterface $subscriber);
+
+ /**
+ * Removes an event listener from the specified events.
+ *
+ * @param string|array $eventName The event(s) to remove a listener from
+ * @param callable $listener The listener to remove
+ */
+ public function removeListener($eventName, $listener);
+
+ /**
+ * Removes an event subscriber.
+ *
+ * @param EventSubscriberInterface $subscriber The subscriber
+ */
+ public function removeSubscriber(EventSubscriberInterface $subscriber);
+
+ /**
+ * Gets the listeners of a specific event or all listeners.
+ *
+ * @param string $eventName The name of the event
+ *
+ * @return array The event listeners for the specified event, or all event listeners by event name
+ */
+ public function getListeners($eventName = null);
+
+ /**
+ * Checks whether an event has any registered listeners.
+ *
+ * @param string $eventName The name of the event
+ *
+ * @return Boolean true if the specified event has any listeners, false otherwise
+ */
+ public function hasListeners($eventName = null);
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * An EventSubscriber knows himself what events he is interested in.
+ * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes
+ * {@link getSubscribedEvents} and registers the subscriber as a listener for all
+ * returned events.
+ *
+ * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
+ * @author Jonathan Wage <jonwage@gmail.com>
+ * @author Roman Borschel <roman@code-factory.org>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @api
+ */
+interface EventSubscriberInterface
+{
+ /**
+ * Returns an array of event names this subscriber wants to listen to.
+ *
+ * The array keys are event names and the value can be:
+ *
+ * * The method name to call (priority defaults to 0)
+ * * An array composed of the method name to call and the priority
+ * * An array of arrays composed of the method names to call and respective
+ * priorities, or 0 if unset
+ *
+ * For instance:
+ *
+ * * array('eventName' => 'methodName')
+ * * array('eventName' => array('methodName', $priority))
+ * * array('eventName' => array(array('methodName1', $priority), array('methodName2'))
+ *
+ * @return array The event names to listen to
+ *
+ * @api
+ */
+ public static function getSubscribedEvents();
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * Event encapsulation class.
+ *
+ * Encapsulates events thus decoupling the observer from the subject they encapsulate.
+ *
+ * @author Drak <drak@zikula.org>
+ */
+class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate
+{
+ /**
+ * Observer pattern subject.
+ *
+ * @var mixed usually object or callable
+ */
+ protected $subject;
+
+ /**
+ * Array of arguments.
+ *
+ * @var array
+ */
+ protected $arguments;
+
+ /**
+ * Encapsulate an event with $subject and $args.
+ *
+ * @param mixed $subject The subject of the event, usually an object.
+ * @param array $arguments Arguments to store in the event.
+ */
+ public function __construct($subject = null, array $arguments = array())
+ {
+ $this->subject = $subject;
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * Getter for subject property.
+ *
+ * @return mixed $subject The observer subject.
+ */
+ public function getSubject()
+ {
+ return $this->subject;
+ }
+
+ /**
+ * Get argument by key.
+ *
+ * @param string $key Key.
+ *
+ * @throws \InvalidArgumentException If key is not found.
+ *
+ * @return mixed Contents of array key.
+ */
+ public function getArgument($key)
+ {
+ if ($this->hasArgument($key)) {
+ return $this->arguments[$key];
+ }
+
+ throw new \InvalidArgumentException(sprintf('%s not found in %s', $key, $this->getName()));
+ }
+
+ /**
+ * Add argument to event.
+ *
+ * @param string $key Argument name.
+ * @param mixed $value Value.
+ *
+ * @return GenericEvent
+ */
+ public function setArgument($key, $value)
+ {
+ $this->arguments[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Getter for all arguments.
+ *
+ * @return array
+ */
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * Set args property.
+ *
+ * @param array $args Arguments.
+ *
+ * @return GenericEvent
+ */
+ public function setArguments(array $args = array())
+ {
+ $this->arguments = $args;
+
+ return $this;
+ }
+
+ /**
+ * Has argument.
+ *
+ * @param string $key Key of arguments array.
+ *
+ * @return boolean
+ */
+ public function hasArgument($key)
+ {
+ return array_key_exists($key, $this->arguments);
+ }
+
+ /**
+ * ArrayAccess for argument getter.
+ *
+ * @param string $key Array key.
+ *
+ * @throws \InvalidArgumentException If key does not exist in $this->args.
+ *
+ * @return mixed
+ */
+ public function offsetGet($key)
+ {
+ return $this->getArgument($key);
+ }
+
+ /**
+ * ArrayAccess for argument setter.
+ *
+ * @param string $key Array key to set.
+ * @param mixed $value Value.
+ */
+ public function offsetSet($key, $value)
+ {
+ $this->setArgument($key, $value);
+ }
+
+ /**
+ * ArrayAccess for unset argument.
+ *
+ * @param string $key Array key.
+ */
+ public function offsetUnset($key)
+ {
+ if ($this->hasArgument($key)) {
+ unset($this->arguments[$key]);
+ }
+ }
+
+ /**
+ * ArrayAccess has argument.
+ *
+ * @param string $key Array key.
+ *
+ * @return boolean
+ */
+ public function offsetExists($key)
+ {
+ return $this->hasArgument($key);
+ }
+
+ /**
+ * IteratorAggregate for iterating over the object like an array
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->arguments);
+ }
+}
--- /dev/null
+<?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\EventDispatcher;
+
+/**
+ * A read-only proxy for an event dispatcher.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ImmutableEventDispatcher implements EventDispatcherInterface
+{
+ /**
+ * The proxied dispatcher.
+ * @var EventDispatcherInterface
+ */
+ private $dispatcher;
+
+ /**
+ * Creates an unmodifiable proxy for an event dispatcher.
+ *
+ * @param EventDispatcherInterface $dispatcher The proxied event dispatcher.
+ */
+ public function __construct(EventDispatcherInterface $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dispatch($eventName, Event $event = null)
+ {
+ return $this->dispatcher->dispatch($eventName, $event);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addListener($eventName, $listener, $priority = 0)
+ {
+ throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubscriber(EventSubscriberInterface $subscriber)
+ {
+ throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeListener($eventName, $listener)
+ {
+ throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeSubscriber(EventSubscriberInterface $subscriber)
+ {
+ throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getListeners($eventName = null)
+ {
+ return $this->dispatcher->getListeners($eventName);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasListeners($eventName = null)
+ {
+ return $this->dispatcher->hasListeners($eventName);
+ }
+}
--- /dev/null
+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.
--- /dev/null
+EventDispatcher Component
+=========================
+
+EventDispatcher implements a lightweight version of the Observer design
+pattern.
+
+ use Symfony\Component\EventDispatcher\EventDispatcher;
+ use Symfony\Component\EventDispatcher\Event;
+
+ $dispatcher = new EventDispatcher();
+
+ $dispatcher->addListener('event_name', function (Event $event) {
+ // ...
+ });
+
+ $dispatcher->dispatch('event_name');
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/EventDispatcher/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\EventDispatcher\Tests;
+
+use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\Scope;
+use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class ContainerAwareEventDispatcherTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\DependencyInjection\Container')) {
+ $this->markTestSkipped('The "DependencyInjection" component is not available');
+ }
+ }
+
+ public function testAddAListenerService()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $service
+ ->expects($this->once())
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $dispatcher->dispatch('onEvent', $event);
+ }
+
+ public function testAddASubscriberService()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\SubscriberService');
+
+ $service
+ ->expects($this->once())
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $container = new Container();
+ $container->set('service.subscriber', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addSubscriberService('service.subscriber', 'Symfony\Component\EventDispatcher\Tests\SubscriberService');
+
+ $dispatcher->dispatch('onEvent', $event);
+ }
+
+ public function testPreventDuplicateListenerService()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $service
+ ->expects($this->once())
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 5);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'), 10);
+
+ $dispatcher->dispatch('onEvent', $event);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testTriggerAListenerServiceOutOfScope()
+ {
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $scope = new Scope('scope');
+ $container = new Container();
+ $container->addScope($scope);
+ $container->enterScope('scope');
+
+ $container->set('service.listener', $service, 'scope');
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $container->leaveScope('scope');
+ $dispatcher->dispatch('onEvent');
+ }
+
+ public function testReEnteringAScope()
+ {
+ $event = new Event();
+
+ $service1 = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $service1
+ ->expects($this->exactly(2))
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $scope = new Scope('scope');
+ $container = new Container();
+ $container->addScope($scope);
+ $container->enterScope('scope');
+
+ $container->set('service.listener', $service1, 'scope');
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+ $dispatcher->dispatch('onEvent', $event);
+
+ $service2 = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $service2
+ ->expects($this->once())
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $container->enterScope('scope');
+ $container->set('service.listener', $service2, 'scope');
+
+ $dispatcher->dispatch('onEvent', $event);
+
+ $container->leaveScope('scope');
+
+ $dispatcher->dispatch('onEvent');
+ }
+
+ public function testHasListenersOnLazyLoad()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $event->setDispatcher($dispatcher);
+ $event->setName('onEvent');
+
+ $service
+ ->expects($this->once())
+ ->method('onEvent')
+ ->with($event)
+ ;
+
+ $this->assertTrue($dispatcher->hasListeners());
+
+ if ($dispatcher->hasListeners('onEvent')) {
+ $dispatcher->dispatch('onEvent');
+ }
+ }
+
+ public function testGetListenersOnLazyLoad()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $listeners = $dispatcher->getListeners();
+
+ $this->assertTrue(isset($listeners['onEvent']));
+
+ $this->assertCount(1, $dispatcher->getListeners('onEvent'));
+ }
+
+ public function testRemoveAfterDispatch()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $dispatcher->dispatch('onEvent', new Event());
+ $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent'));
+ $this->assertFalse($dispatcher->hasListeners('onEvent'));
+ }
+
+ public function testRemoveBeforeDispatch()
+ {
+ $event = new Event();
+
+ $service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service');
+
+ $container = new Container();
+ $container->set('service.listener', $service);
+
+ $dispatcher = new ContainerAwareEventDispatcher($container);
+ $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent'));
+
+ $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent'));
+ $this->assertFalse($dispatcher->hasListeners('onEvent'));
+ }
+}
+
+class Service
+{
+ public function onEvent(Event $e)
+ {
+ }
+}
+
+class SubscriberService implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ 'onEvent' => 'onEvent',
+ 'onEvent' => array('onEvent', 10),
+ 'onEvent' => array('onEvent'),
+ );
+ }
+
+ public function onEvent(Event $e)
+ {
+ }
+}
--- /dev/null
+<?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\EventDispatcher\Tests;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class EventDispatcherTest extends \PHPUnit_Framework_TestCase
+{
+ /* Some pseudo events */
+ const preFoo = 'pre.foo';
+ const postFoo = 'post.foo';
+ const preBar = 'pre.bar';
+ const postBar = 'post.bar';
+
+ private $dispatcher;
+
+ private $listener;
+
+ protected function setUp()
+ {
+ $this->dispatcher = new EventDispatcher();
+ $this->listener = new TestEventListener();
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->listener = null;
+ }
+
+ public function testInitialState()
+ {
+ $this->assertEquals(array(), $this->dispatcher->getListeners());
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertFalse($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testAddListener()
+ {
+ $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo'));
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'));
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ $this->assertCount(1, $this->dispatcher->getListeners(self::preFoo));
+ $this->assertCount(1, $this->dispatcher->getListeners(self::postFoo));
+ $this->assertCount(2, $this->dispatcher->getListeners());
+ }
+
+ public function testGetListenersSortsByPriority()
+ {
+ $listener1 = new TestEventListener();
+ $listener2 = new TestEventListener();
+ $listener3 = new TestEventListener();
+ $listener1->name = '1';
+ $listener2->name = '2';
+ $listener3->name = '3';
+
+ $this->dispatcher->addListener('pre.foo', array($listener1, 'preFoo'), -10);
+ $this->dispatcher->addListener('pre.foo', array($listener2, 'preFoo'), 10);
+ $this->dispatcher->addListener('pre.foo', array($listener3, 'preFoo'));
+
+ $expected = array(
+ array($listener2, 'preFoo'),
+ array($listener3, 'preFoo'),
+ array($listener1, 'preFoo'),
+ );
+
+ $this->assertSame($expected, $this->dispatcher->getListeners('pre.foo'));
+ }
+
+ public function testGetAllListenersSortsByPriority()
+ {
+ $listener1 = new TestEventListener();
+ $listener2 = new TestEventListener();
+ $listener3 = new TestEventListener();
+ $listener4 = new TestEventListener();
+ $listener5 = new TestEventListener();
+ $listener6 = new TestEventListener();
+
+ $this->dispatcher->addListener('pre.foo', $listener1, -10);
+ $this->dispatcher->addListener('pre.foo', $listener2);
+ $this->dispatcher->addListener('pre.foo', $listener3, 10);
+ $this->dispatcher->addListener('post.foo', $listener4, -10);
+ $this->dispatcher->addListener('post.foo', $listener5);
+ $this->dispatcher->addListener('post.foo', $listener6, 10);
+
+ $expected = array(
+ 'pre.foo' => array($listener3, $listener2, $listener1),
+ 'post.foo' => array($listener6, $listener5, $listener4),
+ );
+
+ $this->assertSame($expected, $this->dispatcher->getListeners());
+ }
+
+ public function testDispatch()
+ {
+ $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo'));
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'));
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertTrue($this->listener->preFooInvoked);
+ $this->assertFalse($this->listener->postFooInvoked);
+ $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch('noevent'));
+ $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch(self::preFoo));
+ $event = new Event();
+ $return = $this->dispatcher->dispatch(self::preFoo, $event);
+ $this->assertEquals('pre.foo', $event->getName());
+ $this->assertSame($event, $return);
+ }
+
+ public function testDispatchForClosure()
+ {
+ $invoked = 0;
+ $listener = function () use (&$invoked) {
+ $invoked++;
+ };
+ $this->dispatcher->addListener('pre.foo', $listener);
+ $this->dispatcher->addListener('post.foo', $listener);
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertEquals(1, $invoked);
+ }
+
+ public function testStopEventPropagation()
+ {
+ $otherListener = new TestEventListener();
+
+ // postFoo() stops the propagation, so only one listener should
+ // be executed
+ // Manually set priority to enforce $this->listener to be called first
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'), 10);
+ $this->dispatcher->addListener('post.foo', array($otherListener, 'preFoo'));
+ $this->dispatcher->dispatch(self::postFoo);
+ $this->assertTrue($this->listener->postFooInvoked);
+ $this->assertFalse($otherListener->postFooInvoked);
+ }
+
+ public function testDispatchByPriority()
+ {
+ $invoked = array();
+ $listener1 = function () use (&$invoked) {
+ $invoked[] = '1';
+ };
+ $listener2 = function () use (&$invoked) {
+ $invoked[] = '2';
+ };
+ $listener3 = function () use (&$invoked) {
+ $invoked[] = '3';
+ };
+ $this->dispatcher->addListener('pre.foo', $listener1, -10);
+ $this->dispatcher->addListener('pre.foo', $listener2);
+ $this->dispatcher->addListener('pre.foo', $listener3, 10);
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertEquals(array('3', '2', '1'), $invoked);
+ }
+
+ public function testRemoveListener()
+ {
+ $this->dispatcher->addListener('pre.bar', $this->listener);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preBar));
+ $this->dispatcher->removeListener('pre.bar', $this->listener);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preBar));
+ $this->dispatcher->removeListener('notExists', $this->listener);
+ }
+
+ public function testAddSubscriber()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testAddSubscriberWithPriorities()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $eventSubscriber = new TestEventSubscriberWithPriorities();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $listeners = $this->dispatcher->getListeners('pre.foo');
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $listeners);
+ $this->assertInstanceOf('Symfony\Component\EventDispatcher\Tests\TestEventSubscriberWithPriorities', $listeners[0][0]);
+ }
+
+ public function testAddSubscriberWithMultipleListeners()
+ {
+ $eventSubscriber = new TestEventSubscriberWithMultipleListeners();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $listeners = $this->dispatcher->getListeners('pre.foo');
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $listeners);
+ $this->assertEquals('preFoo2', $listeners[0][1]);
+ }
+
+ public function testRemoveSubscriber()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertFalse($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testRemoveSubscriberWithPriorities()
+ {
+ $eventSubscriber = new TestEventSubscriberWithPriorities();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ }
+
+ public function testRemoveSubscriberWithMultipleListeners()
+ {
+ $eventSubscriber = new TestEventSubscriberWithMultipleListeners();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $this->dispatcher->getListeners(self::preFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ }
+
+ public function testEventReceivesTheDispatcherInstance()
+ {
+ $test = $this;
+ $this->dispatcher->addListener('test', function ($event) use (&$dispatcher) {
+ $dispatcher = $event->getDispatcher();
+ });
+ $this->dispatcher->dispatch('test');
+ $this->assertSame($this->dispatcher, $dispatcher);
+ }
+
+ /**
+ * @see https://bugs.php.net/bug.php?id=62976
+ *
+ * This bug affects:
+ * - The PHP 5.3 branch for versions < 5.3.18
+ * - The PHP 5.4 branch for versions < 5.4.8
+ * - The PHP 5.5 branch is not affected
+ */
+ public function testWorkaroundForPhpBug62976()
+ {
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addListener('bug.62976', new CallableClass());
+ $dispatcher->removeListener('bug.62976', function() {});
+ $this->assertTrue($dispatcher->hasListeners('bug.62976'));
+ }
+}
+
+class CallableClass
+{
+ public function __invoke()
+ {
+ }
+}
+
+class TestEventListener
+{
+ public $preFooInvoked = false;
+ public $postFooInvoked = false;
+
+ /* Listener methods */
+
+ public function preFoo(Event $e)
+ {
+ $this->preFooInvoked = true;
+ }
+
+ public function postFoo(Event $e)
+ {
+ $this->postFooInvoked = true;
+
+ $e->stopPropagation();
+ }
+}
+
+class TestEventSubscriber implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array('pre.foo' => 'preFoo', 'post.foo' => 'postFoo');
+ }
+}
+
+class TestEventSubscriberWithPriorities implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ 'pre.foo' => array('preFoo', 10),
+ 'post.foo' => array('postFoo'),
+ );
+ }
+}
+
+class TestEventSubscriberWithMultipleListeners implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array('pre.foo' => array(
+ array('preFoo1'),
+ array('preFoo2', 10)
+ ));
+ }
+}
--- /dev/null
+<?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\EventDispatcher\Tests;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+/**
+ * Test class for Event.
+ */
+class EventTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \Symfony\Component\EventDispatcher\Event
+ */
+ protected $event;
+
+ /**
+ * @var \Symfony\Component\EventDispatcher\EventDispatcher
+ */
+ protected $dispatcher;
+
+ /**
+ * Sets up the fixture, for example, opens a network connection.
+ * This method is called before a test is executed.
+ */
+ protected function setUp()
+ {
+ $this->event = new Event;
+ $this->dispatcher = new EventDispatcher();
+ }
+
+ /**
+ * Tears down the fixture, for example, closes a network connection.
+ * This method is called after a test is executed.
+ */
+ protected function tearDown()
+ {
+ $this->event = null;
+ $this->eventDispatcher = null;
+ }
+
+ public function testIsPropagationStopped()
+ {
+ $this->assertFalse($this->event->isPropagationStopped());
+ }
+
+ public function testStopPropagationAndIsPropagationStopped()
+ {
+ $this->event->stopPropagation();
+ $this->assertTrue($this->event->isPropagationStopped());
+ }
+
+ public function testSetDispatcher()
+ {
+ $this->event->setDispatcher($this->dispatcher);
+ $this->assertSame($this->dispatcher, $this->event->getDispatcher());
+ }
+
+ public function testGetDispatcher()
+ {
+ $this->assertNull($this->event->getDispatcher());
+ }
+
+ public function testGetName()
+ {
+ $this->assertNull($this->event->getName());
+ }
+
+ public function testSetName()
+ {
+ $this->event->setName('foo');
+ $this->assertEquals('foo', $this->event->getName());
+ }
+}
--- /dev/null
+<?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\EventDispatcher\Tests;
+
+use Symfony\Component\EventDispatcher\GenericEvent;
+
+/**
+ * Test class for Event.
+ */
+class GenericEventTest extends \PHPUnit_Framework_TestCase
+{
+
+ /**
+ * @var GenericEvent
+ */
+ private $event;
+
+ private $subject;
+
+ /**
+ * Prepares the environment before running a test.
+ */
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->subject = new \StdClass();
+ $this->event = new GenericEvent($this->subject, array('name' => 'Event'), 'foo');
+ }
+
+ /**
+ * Cleans up the environment after running a test.
+ */
+ protected function tearDown()
+ {
+ $this->subject = null;
+ $this->event = null;
+
+ parent::tearDown();
+ }
+
+ public function testConstruct()
+ {
+ $this->assertEquals($this->event, new GenericEvent($this->subject, array('name' => 'Event')));
+ }
+
+ /**
+ * Tests Event->getArgs()
+ */
+ public function testGetArguments()
+ {
+ // test getting all
+ $this->assertSame(array('name' => 'Event'), $this->event->getArguments());
+ }
+
+ public function testSetArguments()
+ {
+ $result = $this->event->setArguments(array('foo' => 'bar'));
+ $this->assertAttributeSame(array('foo' => 'bar'), 'arguments', $this->event);
+ $this->assertSame($this->event, $result);
+ }
+
+ public function testSetArgument()
+ {
+ $result = $this->event->setArgument('foo2', 'bar2');
+ $this->assertAttributeSame(array('name' => 'Event', 'foo2' => 'bar2'), 'arguments', $this->event);
+ $this->assertEquals($this->event, $result);
+ }
+
+ public function testGetArgument()
+ {
+ // test getting key
+ $this->assertEquals('Event', $this->event->getArgument('name'));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGetArgException()
+ {
+ $this->event->getArgument('nameNotExist');
+ }
+
+ public function testOffsetGet()
+ {
+ // test getting key
+ $this->assertEquals('Event', $this->event['name']);
+
+ // test getting invalid arg
+ $this->setExpectedException('InvalidArgumentException');
+ $this->assertFalse($this->event['nameNotExist']);
+ }
+
+ public function testOffsetSet()
+ {
+ $this->event['foo2'] = 'bar2';
+ $this->assertAttributeSame(array('name' => 'Event', 'foo2' => 'bar2'), 'arguments', $this->event);
+ }
+
+ public function testOffsetUnset()
+ {
+ unset($this->event['name']);
+ $this->assertAttributeSame(array(), 'arguments', $this->event);
+ }
+
+ public function testOffsetIsset()
+ {
+ $this->assertTrue(isset($this->event['name']));
+ $this->assertFalse(isset($this->event['nameNotExist']));
+ }
+
+ public function testHasArgument()
+ {
+ $this->assertTrue($this->event->hasArgument('name'));
+ $this->assertFalse($this->event->hasArgument('nameNotExist'));
+ }
+
+ public function testGetSubject()
+ {
+ $this->assertSame($this->subject, $this->event->getSubject());
+ }
+
+ public function testHasIterator()
+ {
+ $data = array();
+ foreach ($this->event as $key => $value) {
+ $data[$key] = $value;
+ }
+ $this->assertEquals(array('name' => 'Event'), $data);
+ }
+}
--- /dev/null
+<?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\EventDispatcher\Tests;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ImmutableEventDispatcherTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $innerDispatcher;
+
+ /**
+ * @var ImmutableEventDispatcher
+ */
+ private $dispatcher;
+
+ protected function setUp()
+ {
+ $this->innerDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->dispatcher = new ImmutableEventDispatcher($this->innerDispatcher);
+ }
+
+ public function testDispatchDelegates()
+ {
+ $event = new Event();
+
+ $this->innerDispatcher->expects($this->once())
+ ->method('dispatch')
+ ->with('event', $event)
+ ->will($this->returnValue('result'));
+
+ $this->assertSame('result', $this->dispatcher->dispatch('event', $event));
+ }
+
+ public function testGetListenersDelegates()
+ {
+ $this->innerDispatcher->expects($this->once())
+ ->method('getListeners')
+ ->with('event')
+ ->will($this->returnValue('result'));
+
+ $this->assertSame('result', $this->dispatcher->getListeners('event'));
+ }
+
+ public function testHasListenersDelegates()
+ {
+ $this->innerDispatcher->expects($this->once())
+ ->method('hasListeners')
+ ->with('event')
+ ->will($this->returnValue('result'));
+
+ $this->assertSame('result', $this->dispatcher->hasListeners('event'));
+ }
+
+ /**
+ * @expectedException \BadMethodCallException
+ */
+ public function testAddListenerDisallowed()
+ {
+ $this->dispatcher->addListener('event', function () { return 'foo'; });
+ }
+
+ /**
+ * @expectedException \BadMethodCallException
+ */
+ public function testAddSubscriberDisallowed()
+ {
+ $subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface');
+
+ $this->dispatcher->addSubscriber($subscriber);
+ }
+
+ /**
+ * @expectedException \BadMethodCallException
+ */
+ public function testRemoveListenerDisallowed()
+ {
+ $this->dispatcher->removeListener('event', function () { return 'foo'; });
+ }
+
+ /**
+ * @expectedException \BadMethodCallException
+ */
+ public function testRemoveSubscriberDisallowed()
+ {
+ $subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface');
+
+ $this->dispatcher->removeSubscriber($subscriber);
+ }
+}
--- /dev/null
+{
+ "name": "symfony/event-dispatcher",
+ "type": "library",
+ "description": "Symfony EventDispatcher 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"
+ },
+ "require-dev": {
+ "symfony/dependency-injection": "~2.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\EventDispatcher\\": "" }
+ },
+ "target-dir": "Symfony/Component/EventDispatcher",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 EventDispatcher Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Resources</directory>
+ <directory>./Tests</directory>
+ <directory>./vendor</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+CHANGELOG
+=========
+
+2.3.0
+-----
+
+ * added the dumpFile() method to atomically write files
+
+2.2.0
+-----
+
+ * added a delete option for the mirror() method
+
+2.1.0
+-----
+
+ * 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value
+ * created the component
--- /dev/null
+<?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\Filesystem\Exception;
+
+/**
+ * Exception interface for all exceptions thrown by the component.
+ *
+ * @author Romain Neutron <imprec@gmail.com>
+ *
+ * @api
+ */
+interface ExceptionInterface
+{
+
+}
--- /dev/null
+<?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\Filesystem\Exception;
+
+/**
+ * Exception class thrown when a filesystem operation failure happens
+ *
+ * @author Romain Neutron <imprec@gmail.com>
+ *
+ * @api
+ */
+class IOException extends \RuntimeException implements ExceptionInterface
+{
+
+}
--- /dev/null
+<?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\Filesystem;
+
+use Symfony\Component\Filesystem\Exception\IOException;
+
+/**
+ * Provides basic utility to manipulate the file system.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Filesystem
+{
+ /**
+ * Copies a file.
+ *
+ * This method only copies the file if the origin file is newer than the target file.
+ *
+ * By default, if the target already exists, it is not overridden.
+ *
+ * @param string $originFile The original filename
+ * @param string $targetFile The target filename
+ * @param boolean $override Whether to override an existing file or not
+ *
+ * @throws IOException When copy fails
+ */
+ public function copy($originFile, $targetFile, $override = false)
+ {
+ if (stream_is_local($originFile) && !is_file($originFile)) {
+ throw new IOException(sprintf('Failed to copy %s because file not exists', $originFile));
+ }
+
+ $this->mkdir(dirname($targetFile));
+
+ if (!$override && is_file($targetFile)) {
+ $doCopy = filemtime($originFile) > filemtime($targetFile);
+ } else {
+ $doCopy = true;
+ }
+
+ if ($doCopy) {
+ // https://bugs.php.net/bug.php?id=64634
+ $source = fopen($originFile, 'r');
+ $target = fopen($targetFile, 'w+');
+ stream_copy_to_stream($source, $target);
+ fclose($source);
+ fclose($target);
+ unset($source, $target);
+
+ if (!is_file($targetFile)) {
+ throw new IOException(sprintf('Failed to copy %s to %s', $originFile, $targetFile));
+ }
+ }
+ }
+
+ /**
+ * Creates a directory recursively.
+ *
+ * @param string|array|\Traversable $dirs The directory path
+ * @param integer $mode The directory mode
+ *
+ * @throws IOException On any directory creation failure
+ */
+ public function mkdir($dirs, $mode = 0777)
+ {
+ foreach ($this->toIterator($dirs) as $dir) {
+ if (is_dir($dir)) {
+ continue;
+ }
+
+ if (true !== @mkdir($dir, $mode, true)) {
+ throw new IOException(sprintf('Failed to create %s', $dir));
+ }
+ }
+ }
+
+ /**
+ * Checks the existence of files or directories.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check
+ *
+ * @return Boolean true if the file exists, false otherwise
+ */
+ public function exists($files)
+ {
+ foreach ($this->toIterator($files) as $file) {
+ if (!file_exists($file)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Sets access and modification time of file.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create
+ * @param integer $time The touch time as a unix timestamp
+ * @param integer $atime The access time as a unix timestamp
+ *
+ * @throws IOException When touch fails
+ */
+ public function touch($files, $time = null, $atime = null)
+ {
+ foreach ($this->toIterator($files) as $file) {
+ $touch = $time ? @touch($file, $time, $atime) : @touch($file);
+ if (true !== $touch) {
+ throw new IOException(sprintf('Failed to touch %s', $file));
+ }
+ }
+ }
+
+ /**
+ * Removes files or directories.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove
+ *
+ * @throws IOException When removal fails
+ */
+ public function remove($files)
+ {
+ $files = iterator_to_array($this->toIterator($files));
+ $files = array_reverse($files);
+ foreach ($files as $file) {
+ if (!file_exists($file) && !is_link($file)) {
+ continue;
+ }
+
+ if (is_dir($file) && !is_link($file)) {
+ $this->remove(new \FilesystemIterator($file));
+
+ if (true !== @rmdir($file)) {
+ throw new IOException(sprintf('Failed to remove directory %s', $file));
+ }
+ } else {
+ // https://bugs.php.net/bug.php?id=52176
+ if (defined('PHP_WINDOWS_VERSION_MAJOR') && is_dir($file)) {
+ if (true !== @rmdir($file)) {
+ throw new IOException(sprintf('Failed to remove file %s', $file));
+ }
+ } else {
+ if (true !== @unlink($file)) {
+ throw new IOException(sprintf('Failed to remove file %s', $file));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Change mode for an array of files or directories.
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode
+ * @param integer $mode The new mode (octal)
+ * @param integer $umask The mode mask (octal)
+ * @param Boolean $recursive Whether change the mod recursively or not
+ *
+ * @throws IOException When the change fail
+ */
+ public function chmod($files, $mode, $umask = 0000, $recursive = false)
+ {
+ foreach ($this->toIterator($files) as $file) {
+ if ($recursive && is_dir($file) && !is_link($file)) {
+ $this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
+ }
+ if (true !== @chmod($file, $mode & ~$umask)) {
+ throw new IOException(sprintf('Failed to chmod file %s', $file));
+ }
+ }
+ }
+
+ /**
+ * Change the owner of an array of files or directories
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner
+ * @param string $user The new owner user name
+ * @param Boolean $recursive Whether change the owner recursively or not
+ *
+ * @throws IOException When the change fail
+ */
+ public function chown($files, $user, $recursive = false)
+ {
+ foreach ($this->toIterator($files) as $file) {
+ if ($recursive && is_dir($file) && !is_link($file)) {
+ $this->chown(new \FilesystemIterator($file), $user, true);
+ }
+ if (is_link($file) && function_exists('lchown')) {
+ if (true !== @lchown($file, $user)) {
+ throw new IOException(sprintf('Failed to chown file %s', $file));
+ }
+ } else {
+ if (true !== @chown($file, $user)) {
+ throw new IOException(sprintf('Failed to chown file %s', $file));
+ }
+ }
+ }
+ }
+
+ /**
+ * Change the group of an array of files or directories
+ *
+ * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group
+ * @param string $group The group name
+ * @param Boolean $recursive Whether change the group recursively or not
+ *
+ * @throws IOException When the change fail
+ */
+ public function chgrp($files, $group, $recursive = false)
+ {
+ foreach ($this->toIterator($files) as $file) {
+ if ($recursive && is_dir($file) && !is_link($file)) {
+ $this->chgrp(new \FilesystemIterator($file), $group, true);
+ }
+ if (is_link($file) && function_exists('lchgrp')) {
+ if (true !== @lchgrp($file, $group)) {
+ throw new IOException(sprintf('Failed to chgrp file %s', $file));
+ }
+ } else {
+ if (true !== @chgrp($file, $group)) {
+ throw new IOException(sprintf('Failed to chgrp file %s', $file));
+ }
+ }
+ }
+ }
+
+ /**
+ * Renames a file or a directory.
+ *
+ * @param string $origin The origin filename or directory
+ * @param string $target The new filename or directory
+ * @param Boolean $overwrite Whether to overwrite the target if it already exists
+ *
+ * @throws IOException When target file or directory already exists
+ * @throws IOException When origin cannot be renamed
+ */
+ public function rename($origin, $target, $overwrite = false)
+ {
+ // we check that target does not exist
+ if (!$overwrite && is_readable($target)) {
+ throw new IOException(sprintf('Cannot rename because the target "%s" already exist.', $target));
+ }
+
+ if (true !== @rename($origin, $target)) {
+ throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target));
+ }
+ }
+
+ /**
+ * Creates a symbolic link or copy a directory.
+ *
+ * @param string $originDir The origin directory path
+ * @param string $targetDir The symbolic link name
+ * @param Boolean $copyOnWindows Whether to copy files if on Windows
+ *
+ * @throws IOException When symlink fails
+ */
+ public function symlink($originDir, $targetDir, $copyOnWindows = false)
+ {
+ if (!function_exists('symlink') && $copyOnWindows) {
+ $this->mirror($originDir, $targetDir);
+
+ return;
+ }
+
+ $this->mkdir(dirname($targetDir));
+
+ $ok = false;
+ if (is_link($targetDir)) {
+ if (readlink($targetDir) != $originDir) {
+ $this->remove($targetDir);
+ } else {
+ $ok = true;
+ }
+ }
+
+ if (!$ok) {
+ if (true !== @symlink($originDir, $targetDir)) {
+ $report = error_get_last();
+ if (is_array($report)) {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR') && false !== strpos($report['message'], 'error code(1314)')) {
+ throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?');
+ }
+ }
+ throw new IOException(sprintf('Failed to create symbolic link from %s to %s', $originDir, $targetDir));
+ }
+ }
+ }
+
+ /**
+ * Given an existing path, convert it to a path relative to a given starting path
+ *
+ * @param string $endPath Absolute path of target
+ * @param string $startPath Absolute path where traversal begins
+ *
+ * @return string Path of target relative to starting path
+ */
+ public function makePathRelative($endPath, $startPath)
+ {
+ // Normalize separators on windows
+ if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ $endPath = strtr($endPath, '\\', '/');
+ $startPath = strtr($startPath, '\\', '/');
+ }
+
+ // Split the paths into arrays
+ $startPathArr = explode('/', trim($startPath, '/'));
+ $endPathArr = explode('/', trim($endPath, '/'));
+
+ // Find for which directory the common path stops
+ $index = 0;
+ while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
+ $index++;
+ }
+
+ // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
+ $depth = count($startPathArr) - $index;
+
+ // Repeated "../" for each level need to reach the common path
+ $traverser = str_repeat('../', $depth);
+
+ $endPathRemainder = implode('/', array_slice($endPathArr, $index));
+
+ // Construct $endPath from traversing to the common path, then to the remaining $endPath
+ $relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : '');
+
+ return (strlen($relativePath) === 0) ? './' : $relativePath;
+ }
+
+ /**
+ * Mirrors a directory to another.
+ *
+ * @param string $originDir The origin directory
+ * @param string $targetDir The target directory
+ * @param \Traversable $iterator A Traversable instance
+ * @param array $options An array of boolean options
+ * Valid options are:
+ * - $options['override'] Whether to override an existing file on copy or not (see copy())
+ * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink())
+ * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
+ *
+ * @throws IOException When file type is unknown
+ */
+ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array())
+ {
+ $targetDir = rtrim($targetDir, '/\\');
+ $originDir = rtrim($originDir, '/\\');
+
+ // Iterate in destination folder to remove obsolete entries
+ if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
+ $deleteIterator = $iterator;
+ if (null === $deleteIterator) {
+ $flags = \FilesystemIterator::SKIP_DOTS;
+ $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
+ }
+ foreach ($deleteIterator as $file) {
+ $origin = str_replace($targetDir, $originDir, $file->getPathname());
+ if (!$this->exists($origin)) {
+ $this->remove($file);
+ }
+ }
+ }
+
+ $copyOnWindows = false;
+ if (isset($options['copy_on_windows']) && !function_exists('symlink')) {
+ $copyOnWindows = $options['copy_on_windows'];
+ }
+
+ if (null === $iterator) {
+ $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
+ $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
+ }
+
+ foreach ($iterator as $file) {
+ $target = str_replace($originDir, $targetDir, $file->getPathname());
+
+ if ($copyOnWindows) {
+ if (is_link($file) || is_file($file)) {
+ $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
+ } elseif (is_dir($file)) {
+ $this->mkdir($target);
+ } else {
+ throw new IOException(sprintf('Unable to guess "%s" file type.', $file));
+ }
+ } else {
+ if (is_link($file)) {
+ $this->symlink($file, $target);
+ } elseif (is_dir($file)) {
+ $this->mkdir($target);
+ } elseif (is_file($file)) {
+ $this->copy($file, $target, isset($options['override']) ? $options['override'] : false);
+ } else {
+ throw new IOException(sprintf('Unable to guess "%s" file type.', $file));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether the file path is an absolute path.
+ *
+ * @param string $file A file path
+ *
+ * @return Boolean
+ */
+ public function isAbsolutePath($file)
+ {
+ if (strspn($file, '/\\', 0, 1)
+ || (strlen($file) > 3 && ctype_alpha($file[0])
+ && substr($file, 1, 1) === ':'
+ && (strspn($file, '/\\', 2, 1))
+ )
+ || null !== parse_url($file, PHP_URL_SCHEME)
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param mixed $files
+ *
+ * @return \Traversable
+ */
+ private function toIterator($files)
+ {
+ if (!$files instanceof \Traversable) {
+ $files = new \ArrayObject(is_array($files) ? $files : array($files));
+ }
+
+ return $files;
+ }
+
+ /**
+ * Atomically dumps content into a file.
+ *
+ * @param string $filename The file to be written to.
+ * @param string $content The data to write into the file.
+ * @param integer $mode The file mode (octal).
+ * @throws IOException If the file cannot be written to.
+ */
+ public function dumpFile($filename, $content, $mode = 0666)
+ {
+ $dir = dirname($filename);
+
+ if (!is_dir($dir)) {
+ $this->mkdir($dir);
+ } elseif (!is_writable($dir)) {
+ throw new IOException(sprintf('Unable to write in the %s directory\n', $dir));
+ }
+
+ $tmpFile = tempnam($dir, basename($filename));
+
+ if (false === @file_put_contents($tmpFile, $content)) {
+ throw new IOException(sprintf('Failed to write file "%s".', $filename));
+ }
+
+ $this->rename($tmpFile, $filename, true);
+ $this->chmod($filename, $mode);
+ }
+}
--- /dev/null
+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.
--- /dev/null
+Filesystem Component
+====================
+
+Filesystem provides basic utility to manipulate the file system:
+
+```php
+<?php
+
+use Symfony\Component\Filesystem\Filesystem;
+
+$filesystem = new Filesystem();
+
+$filesystem->copy($originFile, $targetFile, $override = false);
+
+$filesystem->mkdir($dirs, $mode = 0777);
+
+$filesystem->touch($files, $time = null, $atime = null);
+
+$filesystem->remove($files);
+
+$filesystem->chmod($files, $mode, $umask = 0000, $recursive = false);
+
+$filesystem->chown($files, $user, $recursive = false);
+
+$filesystem->chgrp($files, $group, $recursive = false);
+
+$filesystem->rename($origin, $target);
+
+$filesystem->symlink($originDir, $targetDir, $copyOnWindows = false);
+
+$filesystem->makePathRelative($endPath, $startPath);
+
+$filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array());
+
+$filesystem->isAbsolutePath($file);
+```
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Filesystem/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\Filesystem\Tests;
+
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * Test class for Filesystem.
+ */
+class FilesystemTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var string $workspace
+ */
+ private $workspace = null;
+
+ /**
+ * @var \Symfony\Component\Filesystem\Filesystem $filesystem
+ */
+ private $filesystem = null;
+
+ private static $symlinkOnWindows = null;
+
+ public static function setUpBeforeClass()
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ self::$symlinkOnWindows = true;
+ $originDir = tempnam(sys_get_temp_dir(), 'sl');
+ $targetDir = tempnam(sys_get_temp_dir(), 'sl');
+ if (true !== @symlink($originDir, $targetDir)) {
+ $report = error_get_last();
+ if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) {
+ self::$symlinkOnWindows = false;
+ }
+ }
+ }
+ }
+
+ public function setUp()
+ {
+ $this->filesystem = new Filesystem();
+ $this->workspace = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.time().rand(0, 1000);
+ mkdir($this->workspace, 0777, true);
+ $this->workspace = realpath($this->workspace);
+ }
+
+ public function tearDown()
+ {
+ $this->clean($this->workspace);
+ }
+
+ /**
+ * @param string $file
+ */
+ private function clean($file)
+ {
+ if (is_dir($file) && !is_link($file)) {
+ $dir = new \FilesystemIterator($file);
+ foreach ($dir as $childFile) {
+ $this->clean($childFile);
+ }
+
+ rmdir($file);
+ } else {
+ unlink($file);
+ }
+ }
+
+ public function testCopyCreatesNewFile()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ file_put_contents($sourceFilePath, 'SOURCE FILE');
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath);
+
+ $this->assertFileExists($targetFilePath);
+ $this->assertEquals('SOURCE FILE', file_get_contents($targetFilePath));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testCopyFails()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath);
+ }
+
+ public function testCopyOverridesExistingFileIfModified()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ file_put_contents($sourceFilePath, 'SOURCE FILE');
+ file_put_contents($targetFilePath, 'TARGET FILE');
+ touch($targetFilePath, time() - 1000);
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath);
+
+ $this->assertFileExists($targetFilePath);
+ $this->assertEquals('SOURCE FILE', file_get_contents($targetFilePath));
+ }
+
+ public function testCopyDoesNotOverrideExistingFileByDefault()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ file_put_contents($sourceFilePath, 'SOURCE FILE');
+ file_put_contents($targetFilePath, 'TARGET FILE');
+
+ // make sure both files have the same modification time
+ $modificationTime = time() - 1000;
+ touch($sourceFilePath, $modificationTime);
+ touch($targetFilePath, $modificationTime);
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath);
+
+ $this->assertFileExists($targetFilePath);
+ $this->assertEquals('TARGET FILE', file_get_contents($targetFilePath));
+ }
+
+ public function testCopyOverridesExistingFileIfForced()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ file_put_contents($sourceFilePath, 'SOURCE FILE');
+ file_put_contents($targetFilePath, 'TARGET FILE');
+
+ // make sure both files have the same modification time
+ $modificationTime = time() - 1000;
+ touch($sourceFilePath, $modificationTime);
+ touch($targetFilePath, $modificationTime);
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath, true);
+
+ $this->assertFileExists($targetFilePath);
+ $this->assertEquals('SOURCE FILE', file_get_contents($targetFilePath));
+ }
+
+ public function testCopyCreatesTargetDirectoryIfItDoesNotExist()
+ {
+ $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file';
+ $targetFileDirectory = $this->workspace.DIRECTORY_SEPARATOR.'directory';
+ $targetFilePath = $targetFileDirectory.DIRECTORY_SEPARATOR.'copy_target_file';
+
+ file_put_contents($sourceFilePath, 'SOURCE FILE');
+
+ $this->filesystem->copy($sourceFilePath, $targetFilePath);
+
+ $this->assertTrue(is_dir($targetFileDirectory));
+ $this->assertFileExists($targetFilePath);
+ $this->assertEquals('SOURCE FILE', file_get_contents($targetFilePath));
+ }
+
+ public function testMkdirCreatesDirectoriesRecursively()
+ {
+ $directory = $this->workspace
+ .DIRECTORY_SEPARATOR.'directory'
+ .DIRECTORY_SEPARATOR.'sub_directory';
+
+ $this->filesystem->mkdir($directory);
+
+ $this->assertTrue(is_dir($directory));
+ }
+
+ public function testMkdirCreatesDirectoriesFromArray()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+ $directories = array(
+ $basePath.'1', $basePath.'2', $basePath.'3'
+ );
+
+ $this->filesystem->mkdir($directories);
+
+ $this->assertTrue(is_dir($basePath.'1'));
+ $this->assertTrue(is_dir($basePath.'2'));
+ $this->assertTrue(is_dir($basePath.'3'));
+ }
+
+ public function testMkdirCreatesDirectoriesFromTraversableObject()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+ $directories = new \ArrayObject(array(
+ $basePath.'1', $basePath.'2', $basePath.'3'
+ ));
+
+ $this->filesystem->mkdir($directories);
+
+ $this->assertTrue(is_dir($basePath.'1'));
+ $this->assertTrue(is_dir($basePath.'2'));
+ $this->assertTrue(is_dir($basePath.'3'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testMkdirCreatesDirectoriesFails()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+ $dir = $basePath.'2';
+
+ file_put_contents($dir, '');
+
+ $this->filesystem->mkdir($dir);
+ }
+
+ public function testTouchCreatesEmptyFile()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'1';
+
+ $this->filesystem->touch($file);
+
+ $this->assertFileExists($file);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testTouchFails()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR.'2';
+
+ $this->filesystem->touch($file);
+ }
+
+ public function testTouchCreatesEmptyFilesFromArray()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+ $files = array(
+ $basePath.'1', $basePath.'2', $basePath.'3'
+ );
+
+ $this->filesystem->touch($files);
+
+ $this->assertFileExists($basePath.'1');
+ $this->assertFileExists($basePath.'2');
+ $this->assertFileExists($basePath.'3');
+ }
+
+ public function testTouchCreatesEmptyFilesFromTraversableObject()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+ $files = new \ArrayObject(array(
+ $basePath.'1', $basePath.'2', $basePath.'3'
+ ));
+
+ $this->filesystem->touch($files);
+
+ $this->assertFileExists($basePath.'1');
+ $this->assertFileExists($basePath.'2');
+ $this->assertFileExists($basePath.'3');
+ }
+
+ public function testRemoveCleansFilesAndDirectoriesIteratively()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR.'directory'.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath);
+ mkdir($basePath.'dir');
+ touch($basePath.'file');
+
+ $this->filesystem->remove($basePath);
+
+ $this->assertTrue(!is_dir($basePath));
+ }
+
+ public function testRemoveCleansArrayOfFilesAndDirectories()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath.'dir');
+ touch($basePath.'file');
+
+ $files = array(
+ $basePath.'dir', $basePath.'file'
+ );
+
+ $this->filesystem->remove($files);
+
+ $this->assertTrue(!is_dir($basePath.'dir'));
+ $this->assertTrue(!is_file($basePath.'file'));
+ }
+
+ public function testRemoveCleansTraversableObjectOfFilesAndDirectories()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath.'dir');
+ touch($basePath.'file');
+
+ $files = new \ArrayObject(array(
+ $basePath.'dir', $basePath.'file'
+ ));
+
+ $this->filesystem->remove($files);
+
+ $this->assertTrue(!is_dir($basePath.'dir'));
+ $this->assertTrue(!is_file($basePath.'file'));
+ }
+
+ public function testRemoveIgnoresNonExistingFiles()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath.'dir');
+
+ $files = array(
+ $basePath.'dir', $basePath.'file'
+ );
+
+ $this->filesystem->remove($files);
+
+ $this->assertTrue(!is_dir($basePath.'dir'));
+ }
+
+ public function testRemoveCleansInvalidLinks()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR.'directory'.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath);
+ mkdir($basePath.'dir');
+ // create symlink to unexisting file
+ @symlink($basePath.'file', $basePath.'link');
+
+ $this->filesystem->remove($basePath);
+
+ $this->assertTrue(!is_dir($basePath));
+ }
+
+ public function testFilesExists()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR.'directory'.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath);
+ touch($basePath.'file1');
+ mkdir($basePath.'folder');
+
+ $this->assertTrue($this->filesystem->exists($basePath.'file1'));
+ $this->assertTrue($this->filesystem->exists($basePath.'folder'));
+ }
+
+ public function testFilesExistsTraversableObjectOfFilesAndDirectories()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath.'dir');
+ touch($basePath.'file');
+
+ $files = new \ArrayObject(array(
+ $basePath.'dir', $basePath.'file'
+ ));
+
+ $this->assertTrue($this->filesystem->exists($files));
+ }
+
+ public function testFilesNotExistsTraversableObjectOfFilesAndDirectories()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR;
+
+ mkdir($basePath.'dir');
+ touch($basePath.'file');
+ touch($basePath.'file2');
+
+ $files = new \ArrayObject(array(
+ $basePath.'dir', $basePath.'file', $basePath.'file2'
+ ));
+
+ unlink($basePath.'file');
+
+ $this->assertFalse($this->filesystem->exists($files));
+ }
+
+ public function testInvalidFileNotExists()
+ {
+ $basePath = $this->workspace.DIRECTORY_SEPARATOR.'directory'.DIRECTORY_SEPARATOR;
+
+ $this->assertFalse($this->filesystem->exists($basePath.time()));
+ }
+
+ public function testChmodChangesFileMode()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+ $file = $dir.DIRECTORY_SEPARATOR.'file';
+ touch($file);
+
+ $this->filesystem->chmod($file, 0400);
+ $this->filesystem->chmod($dir, 0753);
+
+ $this->assertEquals(753, $this->getFilePermissions($dir));
+ $this->assertEquals(400, $this->getFilePermissions($file));
+ }
+
+ public function testChmodWrongMod()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ touch($dir);
+
+ $this->filesystem->chmod($dir, 'Wrongmode');
+ }
+
+ public function testChmodRecursive()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+ $file = $dir.DIRECTORY_SEPARATOR.'file';
+ touch($file);
+
+ $this->filesystem->chmod($file, 0400, 0000, true);
+ $this->filesystem->chmod($dir, 0753, 0000, true);
+
+ $this->assertEquals(753, $this->getFilePermissions($dir));
+ $this->assertEquals(753, $this->getFilePermissions($file));
+ }
+
+ public function testChmodAppliesUmask()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ touch($file);
+
+ $this->filesystem->chmod($file, 0770, 0022);
+ $this->assertEquals(750, $this->getFilePermissions($file));
+ }
+
+ public function testChmodChangesModeOfArrayOfFiles()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.DIRECTORY_SEPARATOR.'directory';
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $files = array($directory, $file);
+
+ mkdir($directory);
+ touch($file);
+
+ $this->filesystem->chmod($files, 0753);
+
+ $this->assertEquals(753, $this->getFilePermissions($file));
+ $this->assertEquals(753, $this->getFilePermissions($directory));
+ }
+
+ public function testChmodChangesModeOfTraversableFileObject()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.DIRECTORY_SEPARATOR.'directory';
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $files = new \ArrayObject(array($directory, $file));
+
+ mkdir($directory);
+ touch($file);
+
+ $this->filesystem->chmod($files, 0753);
+
+ $this->assertEquals(753, $this->getFilePermissions($file));
+ $this->assertEquals(753, $this->getFilePermissions($directory));
+ }
+
+ public function testChown()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+
+ $this->filesystem->chown($dir, $this->getFileOwner($dir));
+ }
+
+ public function testChownRecursive()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+ $file = $dir.DIRECTORY_SEPARATOR.'file';
+ touch($file);
+
+ $this->filesystem->chown($dir, $this->getFileOwner($dir), true);
+ }
+
+ public function testChownSymlink()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->filesystem->chown($link, $this->getFileOwner($link));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testChownSymlinkFails()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testChownFail()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+
+ $this->filesystem->chown($dir, 'user'.time().mt_rand(1000, 9999));
+ }
+
+ public function testChgrp()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+
+ $this->filesystem->chgrp($dir, $this->getFileGroup($dir));
+ }
+
+ public function testChgrpRecursive()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+ $file = $dir.DIRECTORY_SEPARATOR.'file';
+ touch($file);
+
+ $this->filesystem->chgrp($dir, $this->getFileGroup($dir), true);
+ }
+
+ public function testChgrpSymlink()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->filesystem->chgrp($link, $this->getFileGroup($link));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testChgrpSymlinkFails()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testChgrpFail()
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir';
+ mkdir($dir);
+
+ $this->filesystem->chgrp($dir, 'user'.time().mt_rand(1000, 9999));
+ }
+
+ public function testRename()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $newPath = $this->workspace.DIRECTORY_SEPARATOR.'new_file';
+ touch($file);
+
+ $this->filesystem->rename($file, $newPath);
+
+ $this->assertFileNotExists($file);
+ $this->assertFileExists($newPath);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testRenameThrowsExceptionIfTargetAlreadyExists()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $newPath = $this->workspace.DIRECTORY_SEPARATOR.'new_file';
+
+ touch($file);
+ touch($newPath);
+
+ $this->filesystem->rename($file, $newPath);
+ }
+
+ public function testRenameOverwritesTheTargetIfItAlreadyExists()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $newPath = $this->workspace.DIRECTORY_SEPARATOR.'new_file';
+
+ touch($file);
+ touch($newPath);
+
+ $this->filesystem->rename($file, $newPath, true);
+
+ $this->assertFileNotExists($file);
+ $this->assertFileExists($newPath);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Filesystem\Exception\IOException
+ */
+ public function testRenameThrowsExceptionOnError()
+ {
+ $file = $this->workspace.DIRECTORY_SEPARATOR.uniqid();
+ $newPath = $this->workspace.DIRECTORY_SEPARATOR.'new_file';
+
+ $this->filesystem->rename($file, $newPath);
+ }
+
+ public function testSymlink()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->assertTrue(is_link($link));
+ $this->assertEquals($file, readlink($link));
+ }
+
+ /**
+ * @depends testSymlink
+ */
+ public function testRemoveSymlink()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ $this->filesystem->remove($link);
+
+ $this->assertTrue(!is_link($link));
+ }
+
+ public function testSymlinkIsOverwrittenIfPointsToDifferentTarget()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+ symlink($this->workspace, $link);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->assertTrue(is_link($link));
+ $this->assertEquals($file, readlink($link));
+ }
+
+ public function testSymlinkIsNotOverwrittenIfAlreadyCreated()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link = $this->workspace.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+ symlink($file, $link);
+
+ $this->filesystem->symlink($file, $link);
+
+ $this->assertTrue(is_link($link));
+ $this->assertEquals($file, readlink($link));
+ }
+
+ public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $file = $this->workspace.DIRECTORY_SEPARATOR.'file';
+ $link1 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'link';
+ $link2 = $this->workspace.DIRECTORY_SEPARATOR.'dir'.DIRECTORY_SEPARATOR.'subdir'.DIRECTORY_SEPARATOR.'link';
+
+ touch($file);
+
+ $this->filesystem->symlink($file, $link1);
+ $this->filesystem->symlink($file, $link2);
+
+ $this->assertTrue(is_link($link1));
+ $this->assertEquals($file, readlink($link1));
+ $this->assertTrue(is_link($link2));
+ $this->assertEquals($file, readlink($link2));
+ }
+
+ /**
+ * @dataProvider providePathsForMakePathRelative
+ */
+ public function testMakePathRelative($endPath, $startPath, $expectedPath)
+ {
+ $path = $this->filesystem->makePathRelative($endPath, $startPath);
+
+ $this->assertEquals($expectedPath, $path);
+ }
+
+ /**
+ * @return array
+ */
+ public function providePathsForMakePathRelative()
+ {
+ $paths = array(
+ array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component', '../'),
+ array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/src/Symfony/Component/', '../'),
+ array('/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component', '../'),
+ array('/var/lib/symfony/src/Symfony', '/var/lib/symfony/src/Symfony/Component/', '../'),
+ array('var/lib/symfony/', 'var/lib/symfony/src/Symfony/Component', '../../../'),
+ array('/usr/lib/symfony/', '/var/lib/symfony/src/Symfony/Component', '../../../../../../usr/lib/symfony/'),
+ array('/var/lib/symfony/src/Symfony/', '/var/lib/symfony/', 'src/Symfony/'),
+ array('/aa/bb', '/aa/bb', './'),
+ array('/aa/bb', '/aa/bb/', './'),
+ array('/aa/bb/', '/aa/bb', './'),
+ array('/aa/bb/', '/aa/bb/', './'),
+ array('/aa/bb/cc', '/aa/bb/cc/dd', '../'),
+ array('/aa/bb/cc', '/aa/bb/cc/dd/', '../'),
+ array('/aa/bb/cc/', '/aa/bb/cc/dd', '../'),
+ array('/aa/bb/cc/', '/aa/bb/cc/dd/', '../'),
+ array('/aa/bb/cc', '/aa', 'bb/cc/'),
+ array('/aa/bb/cc', '/aa/', 'bb/cc/'),
+ array('/aa/bb/cc/', '/aa', 'bb/cc/'),
+ array('/aa/bb/cc/', '/aa/', 'bb/cc/'),
+ array('/a/aab/bb', '/a/aa', '../aab/bb/'),
+ array('/a/aab/bb', '/a/aa/', '../aab/bb/'),
+ array('/a/aab/bb/', '/a/aa', '../aab/bb/'),
+ array('/a/aab/bb/', '/a/aa/', '../aab/bb/'),
+ );
+
+ if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ $paths[] = array('c:\var\lib/symfony/src/Symfony/', 'c:/var/lib/symfony/', 'src/Symfony/');
+ }
+
+ return $paths;
+ }
+
+ public function testMirrorCopiesFilesAndDirectoriesRecursively()
+ {
+ $sourcePath = $this->workspace.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR;
+ $directory = $sourcePath.'directory'.DIRECTORY_SEPARATOR;
+ $file1 = $directory.'file1';
+ $file2 = $sourcePath.'file2';
+
+ mkdir($sourcePath);
+ mkdir($directory);
+ file_put_contents($file1, 'FILE1');
+ file_put_contents($file2, 'FILE2');
+
+ $targetPath = $this->workspace.DIRECTORY_SEPARATOR.'target'.DIRECTORY_SEPARATOR;
+
+ $this->filesystem->mirror($sourcePath, $targetPath);
+
+ $this->assertTrue(is_dir($targetPath));
+ $this->assertTrue(is_dir($targetPath.'directory'));
+ $this->assertFileEquals($file1, $targetPath.'directory'.DIRECTORY_SEPARATOR.'file1');
+ $this->assertFileEquals($file2, $targetPath.'file2');
+
+ $this->filesystem->remove($file1);
+
+ $this->filesystem->mirror($sourcePath, $targetPath, null, array('delete' => false));
+ $this->assertTrue($this->filesystem->exists($targetPath.'directory'.DIRECTORY_SEPARATOR.'file1'));
+
+ $this->filesystem->mirror($sourcePath, $targetPath, null, array('delete' => true));
+ $this->assertFalse($this->filesystem->exists($targetPath.'directory'.DIRECTORY_SEPARATOR.'file1'));
+
+ file_put_contents($file1, 'FILE1');
+
+ $this->filesystem->mirror($sourcePath, $targetPath, null, array('delete' => true));
+ $this->assertTrue($this->filesystem->exists($targetPath.'directory'.DIRECTORY_SEPARATOR.'file1'));
+
+ $this->filesystem->remove($directory);
+ $this->filesystem->mirror($sourcePath, $targetPath, null, array('delete' => true));
+ $this->assertFalse($this->filesystem->exists($targetPath.'directory'));
+ $this->assertFalse($this->filesystem->exists($targetPath.'directory'.DIRECTORY_SEPARATOR.'file1'));
+ }
+
+ public function testMirrorCopiesLinks()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $sourcePath = $this->workspace.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR;
+
+ mkdir($sourcePath);
+ file_put_contents($sourcePath.'file1', 'FILE1');
+ symlink($sourcePath.'file1', $sourcePath.'link1');
+
+ $targetPath = $this->workspace.DIRECTORY_SEPARATOR.'target'.DIRECTORY_SEPARATOR;
+
+ $this->filesystem->mirror($sourcePath, $targetPath);
+
+ $this->assertTrue(is_dir($targetPath));
+ $this->assertFileEquals($sourcePath.'file1', $targetPath.DIRECTORY_SEPARATOR.'link1');
+ $this->assertTrue(is_link($targetPath.DIRECTORY_SEPARATOR.'link1'));
+ }
+
+ public function testMirrorCopiesLinkedDirectoryContents()
+ {
+ $this->markAsSkippedIfSymlinkIsMissing();
+
+ $sourcePath = $this->workspace.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR;
+
+ mkdir($sourcePath.'nested/', 0777, true);
+ file_put_contents($sourcePath.'/nested/file1.txt', 'FILE1');
+ // Note: We symlink directory, not file
+ symlink($sourcePath.'nested', $sourcePath.'link1');
+
+ $targetPath = $this->workspace.DIRECTORY_SEPARATOR.'target'.DIRECTORY_SEPARATOR;
+
+ $this->filesystem->mirror($sourcePath, $targetPath);
+
+ $this->assertTrue(is_dir($targetPath));
+ $this->assertFileEquals($sourcePath.'/nested/file1.txt', $targetPath.DIRECTORY_SEPARATOR.'link1/file1.txt');
+ $this->assertTrue(is_link($targetPath.DIRECTORY_SEPARATOR.'link1'));
+ }
+
+ /**
+ * @dataProvider providePathsForIsAbsolutePath
+ */
+ public function testIsAbsolutePath($path, $expectedResult)
+ {
+ $result = $this->filesystem->isAbsolutePath($path);
+
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ /**
+ * @return array
+ */
+ public function providePathsForIsAbsolutePath()
+ {
+ return array(
+ array('/var/lib', true),
+ array('c:\\\\var\\lib', true),
+ array('\\var\\lib', true),
+ array('var/lib', false),
+ array('../var/lib', false),
+ array('', false),
+ array(null, false)
+ );
+ }
+
+ public function testDumpFile()
+ {
+ $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo'.DIRECTORY_SEPARATOR.'baz.txt';
+
+ $this->filesystem->dumpFile($filename, 'bar', 0753);
+
+ $this->assertFileExists($filename);
+ $this->assertSame('bar', file_get_contents($filename));
+
+ // skip mode check on windows
+ if (!defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ $this->assertEquals(753, $this->getFilePermissions($filename));
+ }
+ }
+
+ public function testDumpFileOverwritesAnExistingFile()
+ {
+ $filename = $this->workspace.DIRECTORY_SEPARATOR.'foo.txt';
+ file_put_contents($filename, 'FOO BAR');
+
+ $this->filesystem->dumpFile($filename, 'bar');
+
+ $this->assertFileExists($filename);
+ $this->assertSame('bar', file_get_contents($filename));
+ }
+
+ /**
+ * Returns file permissions as three digits (i.e. 755)
+ *
+ * @param string $filePath
+ *
+ * @return integer
+ */
+ private function getFilePermissions($filePath)
+ {
+ return (int) substr(sprintf('%o', fileperms($filePath)), -3);
+ }
+
+ private function getFileOwner($filepath)
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $infos = stat($filepath);
+ if ($datas = posix_getpwuid($infos['uid'])) {
+ return $datas['name'];
+ }
+ }
+
+ private function getFileGroup($filepath)
+ {
+ $this->markAsSkippedIfPosixIsMissing();
+
+ $infos = stat($filepath);
+ if ($datas = posix_getgrgid($infos['gid'])) {
+ return $datas['name'];
+ }
+ }
+
+ private function markAsSkippedIfSymlinkIsMissing()
+ {
+ if (!function_exists('symlink')) {
+ $this->markTestSkipped('symlink is not supported');
+ }
+
+ if (defined('PHP_WINDOWS_VERSION_MAJOR') && false === self::$symlinkOnWindows) {
+ $this->markTestSkipped('symlink requires "Create symbolic links" privilege on windows');
+ }
+ }
+
+ private function markAsSkippedIfChmodIsMissing()
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
+ $this->markTestSkipped('chmod is not supported on windows');
+ }
+ }
+
+ private function markAsSkippedIfPosixIsMissing()
+ {
+ if (defined('PHP_WINDOWS_VERSION_MAJOR') || !function_exists('posix_isatty')) {
+ $this->markTestSkipped('Posix is not supported');
+ }
+ }
+}
--- /dev/null
+{
+ "name": "symfony/filesystem",
+ "type": "library",
+ "description": "Symfony Filesystem 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\\Filesystem\\": "" }
+ },
+ "target-dir": "Symfony/Component/Filesystem",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Filesystem Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Tests</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractExtension implements FormExtensionInterface
+{
+ /**
+ * The types provided by this extension
+ * @var FormTypeInterface[] An array of FormTypeInterface
+ */
+ private $types;
+
+ /**
+ * The type extensions provided by this extension
+ * @var FormTypeExtensionInterface[] An array of FormTypeExtensionInterface
+ */
+ private $typeExtensions;
+
+ /**
+ * The type guesser provided by this extension
+ * @var FormTypeGuesserInterface
+ */
+ private $typeGuesser;
+
+ /**
+ * Whether the type guesser has been loaded
+ * @var Boolean
+ */
+ private $typeGuesserLoaded = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType($name)
+ {
+ if (null === $this->types) {
+ $this->initTypes();
+ }
+
+ if (!isset($this->types[$name])) {
+ throw new InvalidArgumentException(sprintf('The type "%s" can not be loaded by this extension', $name));
+ }
+
+ return $this->types[$name];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasType($name)
+ {
+ if (null === $this->types) {
+ $this->initTypes();
+ }
+
+ return isset($this->types[$name]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeExtensions($name)
+ {
+ if (null === $this->typeExtensions) {
+ $this->initTypeExtensions();
+ }
+
+ return isset($this->typeExtensions[$name])
+ ? $this->typeExtensions[$name]
+ : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTypeExtensions($name)
+ {
+ if (null === $this->typeExtensions) {
+ $this->initTypeExtensions();
+ }
+
+ return isset($this->typeExtensions[$name]) && count($this->typeExtensions[$name]) > 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeGuesser()
+ {
+ if (!$this->typeGuesserLoaded) {
+ $this->initTypeGuesser();
+ }
+
+ return $this->typeGuesser;
+ }
+
+ /**
+ * Registers the types.
+ *
+ * @return FormTypeInterface[] An array of FormTypeInterface instances
+ */
+ protected function loadTypes()
+ {
+ return array();
+ }
+
+ /**
+ * Registers the type extensions.
+ *
+ * @return FormTypeExtensionInterface[] An array of FormTypeExtensionInterface instances
+ */
+ protected function loadTypeExtensions()
+ {
+ return array();
+ }
+
+ /**
+ * Registers the type guesser.
+ *
+ * @return FormTypeGuesserInterface|null A type guesser
+ */
+ protected function loadTypeGuesser()
+ {
+ return null;
+ }
+
+ /**
+ * Initializes the types.
+ *
+ * @throws UnexpectedTypeException if any registered type is not an instance of FormTypeInterface
+ */
+ private function initTypes()
+ {
+ $this->types = array();
+
+ foreach ($this->loadTypes() as $type) {
+ if (!$type instanceof FormTypeInterface) {
+ throw new UnexpectedTypeException($type, 'Symfony\Component\Form\FormTypeInterface');
+ }
+
+ $this->types[$type->getName()] = $type;
+ }
+ }
+
+ /**
+ * Initializes the type extensions.
+ *
+ * @throws UnexpectedTypeException if any registered type extension is not
+ * an instance of FormTypeExtensionInterface
+ */
+ private function initTypeExtensions()
+ {
+ $this->typeExtensions = array();
+
+ foreach ($this->loadTypeExtensions() as $extension) {
+ if (!$extension instanceof FormTypeExtensionInterface) {
+ throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
+ }
+
+ $type = $extension->getExtendedType();
+
+ $this->typeExtensions[$type][] = $extension;
+ }
+ }
+
+ /**
+ * Initializes the type guesser.
+ *
+ * @throws UnexpectedTypeException if the type guesser is not an instance of FormTypeGuesserInterface
+ */
+ private function initTypeGuesser()
+ {
+ $this->typeGuesserLoaded = true;
+
+ $this->typeGuesser = $this->loadTypeGuesser();
+ if (null !== $this->typeGuesser && !$this->typeGuesser instanceof FormTypeGuesserInterface) {
+ throw new UnexpectedTypeException($this->typeGuesser, 'Symfony\Component\Form\FormTypeGuesserInterface');
+ }
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Default implementation of {@link FormRendererEngineInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractRendererEngine implements FormRendererEngineInterface
+{
+ /**
+ * The variable in {@link FormView} used as cache key.
+ */
+ const CACHE_KEY_VAR = 'cache_key';
+
+ /**
+ * @var array
+ */
+ protected $defaultThemes;
+
+ /**
+ * @var array
+ */
+ protected $themes = array();
+
+ /**
+ * @var array
+ */
+ protected $resources = array();
+
+ /**
+ * @var array
+ */
+ private $resourceHierarchyLevels = array();
+
+ /**
+ * Creates a new renderer engine.
+ *
+ * @param array $defaultThemes The default themes. The type of these
+ * themes is open to the implementation.
+ */
+ public function __construct(array $defaultThemes = array())
+ {
+ $this->defaultThemes = $defaultThemes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTheme(FormView $view, $themes)
+ {
+ $cacheKey = $view->vars[self::CACHE_KEY_VAR];
+
+ // Do not cast, as casting turns objects into arrays of properties
+ $this->themes[$cacheKey] = is_array($themes) ? $themes : array($themes);
+
+ // Unset instead of resetting to an empty array, in order to allow
+ // implementations (like TwigRendererEngine) to check whether $cacheKey
+ // is set at all.
+ unset($this->resources[$cacheKey]);
+ unset($this->resourceHierarchyLevels[$cacheKey]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResourceForBlockName(FormView $view, $blockName)
+ {
+ $cacheKey = $view->vars[self::CACHE_KEY_VAR];
+
+ if (!isset($this->resources[$cacheKey][$blockName])) {
+ $this->loadResourceForBlockName($cacheKey, $view, $blockName);
+ }
+
+ return $this->resources[$cacheKey][$blockName];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, $hierarchyLevel)
+ {
+ $cacheKey = $view->vars[self::CACHE_KEY_VAR];
+ $blockName = $blockNameHierarchy[$hierarchyLevel];
+
+ if (!isset($this->resources[$cacheKey][$blockName])) {
+ $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
+ }
+
+ return $this->resources[$cacheKey][$blockName];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, $hierarchyLevel)
+ {
+ $cacheKey = $view->vars[self::CACHE_KEY_VAR];
+ $blockName = $blockNameHierarchy[$hierarchyLevel];
+
+ if (!isset($this->resources[$cacheKey][$blockName])) {
+ $this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $hierarchyLevel);
+ }
+
+ // If $block was previously rendered loaded with loadTemplateForBlock(), the template
+ // is cached but the hierarchy level is not. In this case, we know that the block
+ // exists at this very hierarchy level, so we can just set it.
+ if (!isset($this->resourceHierarchyLevels[$cacheKey][$blockName])) {
+ $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
+ }
+
+ return $this->resourceHierarchyLevels[$cacheKey][$blockName];
+ }
+
+ /**
+ * Loads the cache with the resource for a given block name.
+ *
+ * @see getResourceForBlock()
+ *
+ * @param string $cacheKey The cache key of the form view.
+ * @param FormView $view The form view for finding the applying themes.
+ * @param string $blockName The name of the block to load.
+ *
+ * @return Boolean True if the resource could be loaded, false otherwise.
+ */
+ abstract protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName);
+
+ /**
+ * Loads the cache with the resource for a specific level of a block hierarchy.
+ *
+ * @see getResourceForBlockHierarchy()
+ *
+ * @param string $cacheKey The cache key used for storing the
+ * resource.
+ * @param FormView $view The form view for finding the applying
+ * themes.
+ * @param array $blockNameHierarchy The block hierarchy, with the most
+ * specific block name at the end.
+ * @param integer $hierarchyLevel The level in the block hierarchy that
+ * should be loaded.
+ *
+ * @return Boolean True if the resource could be loaded, false otherwise.
+ */
+ private function loadResourceForBlockNameHierarchy($cacheKey, FormView $view, array $blockNameHierarchy, $hierarchyLevel)
+ {
+ $blockName = $blockNameHierarchy[$hierarchyLevel];
+
+ // Try to find a template for that block
+ if ($this->loadResourceForBlockName($cacheKey, $view, $blockName)) {
+ // If loadTemplateForBlock() returns true, it was able to populate the
+ // cache. The only missing thing is to set the hierarchy level at which
+ // the template was found.
+ $this->resourceHierarchyLevels[$cacheKey][$blockName] = $hierarchyLevel;
+
+ return true;
+ }
+
+ if ($hierarchyLevel > 0) {
+ $parentLevel = $hierarchyLevel - 1;
+ $parentBlockName = $blockNameHierarchy[$parentLevel];
+
+ // The next two if statements contain slightly duplicated code. This is by intention
+ // and tries to avoid execution of unnecessary checks in order to increase performance.
+
+ if (isset($this->resources[$cacheKey][$parentBlockName])) {
+ // It may happen that the parent block is already loaded, but its level is not.
+ // In this case, the parent block must have been loaded by loadResourceForBlock(),
+ // which does not check the hierarchy of the block. Subsequently the block must have
+ // been found directly on the parent level.
+ if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlockName])) {
+ $this->resourceHierarchyLevels[$cacheKey][$parentBlockName] = $parentLevel;
+ }
+
+ // Cache the shortcuts for further accesses
+ $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
+ $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
+
+ return true;
+ }
+
+ if ($this->loadResourceForBlockNameHierarchy($cacheKey, $view, $blockNameHierarchy, $parentLevel)) {
+ // Cache the shortcuts for further accesses
+ $this->resources[$cacheKey][$blockName] = $this->resources[$cacheKey][$parentBlockName];
+ $this->resourceHierarchyLevels[$cacheKey][$blockName] = $this->resourceHierarchyLevels[$cacheKey][$parentBlockName];
+
+ return true;
+ }
+ }
+
+ // Cache the result for further accesses
+ $this->resources[$cacheKey][$blockName] = false;
+ $this->resourceHierarchyLevels[$cacheKey][$blockName] = false;
+
+ return false;
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractType implements FormTypeInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractTypeExtension implements FormTypeExtensionInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\AlreadySubmittedException;
+use Symfony\Component\Form\Exception\BadMethodCallException;
+
+/**
+ * A form button.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Button implements \IteratorAggregate, FormInterface
+{
+ /**
+ * @var FormInterface
+ */
+ private $parent;
+
+ /**
+ * @var FormConfigInterface
+ */
+ private $config;
+
+ /**
+ * @var Boolean
+ */
+ private $submitted = false;
+
+ /**
+ * Creates a new button from a form configuration.
+ *
+ * @param FormConfigInterface $config The button's configuration.
+ */
+ public function __construct(FormConfigInterface $config)
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param mixed $offset
+ *
+ * @return Boolean Always returns false.
+ */
+ public function offsetExists($offset)
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ *
+ * @throws BadMethodCallException
+ */
+ public function offsetGet($offset)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ * @param mixed $value
+ *
+ * @throws BadMethodCallException
+ */
+ public function offsetSet($offset, $value)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $offset
+ *
+ * @throws BadMethodCallException
+ */
+ public function offsetUnset($offset)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setParent(FormInterface $parent = null)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param int|string|FormInterface $child
+ * @param null $type
+ * @param array $options
+ *
+ * @throws BadMethodCallException
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws BadMethodCallException
+ */
+ public function get($name)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $name
+ *
+ * @return Boolean Always returns false.
+ */
+ public function has($name)
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws BadMethodCallException
+ */
+ public function remove($name)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ return array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrors()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $modelData
+ *
+ * @throws BadMethodCallException
+ */
+ public function setData($modelData)
+ {
+ throw new BadMethodCallException('Buttons cannot have data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getNormData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getViewData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getExtraData()
+ {
+ return array();
+ }
+
+ /**
+ * Returns the button's configuration.
+ *
+ * @return FormConfigInterface The configuration.
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Returns whether the button is submitted.
+ *
+ * @return Boolean true if the button was submitted.
+ */
+ public function isSubmitted()
+ {
+ return $this->submitted;
+ }
+
+ /**
+ * Returns the name by which the button is identified in forms.
+ *
+ * @return string The name of the button.
+ */
+ public function getName()
+ {
+ return $this->config->getName();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getPropertyPath()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param FormError $error
+ *
+ * @throws BadMethodCallException
+ */
+ public function addError(FormError $error)
+ {
+ throw new BadMethodCallException('Buttons cannot have errors.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isValid()
+ {
+ return true;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function isRequired()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDisabled()
+ {
+ return $this->config->getDisabled();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isEmpty()
+ {
+ return true;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns true.
+ */
+ public function isSynchronized()
+ {
+ return true;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @throws BadMethodCallException
+ */
+ public function initialize()
+ {
+ throw new BadMethodCallException('Buttons cannot be initialized. Call initialize() on the root form instead.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param mixed $request
+ *
+ * @throws BadMethodCallException
+ */
+ public function handleRequest($request = null)
+ {
+ throw new BadMethodCallException('Buttons cannot handle requests. Call handleRequest() on the root form instead.');
+ }
+
+ /**
+ * Submits data to the button.
+ *
+ * @param null|string $submittedData The data.
+ * @param Boolean $clearMissing Not used.
+ *
+ * @return Button The button instance
+ *
+ * @throws Exception\AlreadySubmittedException If the button has already been submitted.
+ */
+ public function submit($submittedData, $clearMissing = true)
+ {
+ if ($this->submitted) {
+ throw new AlreadySubmittedException('A form can only be submitted once');
+ }
+
+ $this->submitted = true;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoot()
+ {
+ return $this->parent ? $this->parent->getRoot() : $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRoot()
+ {
+ return null === $this->parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createView(FormView $parent = null)
+ {
+ if (null === $parent && $this->parent) {
+ $parent = $this->parent->createView();
+ }
+
+ return $this->config->getType()->createView($this, $parent);
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return integer Always returns 0.
+ */
+ public function count()
+ {
+ return 0;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return \EmptyIterator Always returns an empty iterator.
+ */
+ public function getIterator()
+ {
+ return new \EmptyIterator();
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Exception\BadMethodCallException;
+
+/**
+ * A builder for {@link Button} instances.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface
+{
+ /**
+ * @var Boolean
+ */
+ protected $locked = false;
+
+ /**
+ * @var Boolean
+ */
+ private $disabled;
+
+ /**
+ * @var ResolvedFormTypeInterface
+ */
+ private $type;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $attributes = array();
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Creates a new button builder.
+ *
+ * @param string $name The name of the button.
+ * @param array $options The button's options.
+ *
+ * @throws InvalidArgumentException If the name is empty.
+ */
+ public function __construct($name, array $options)
+ {
+ if (empty($name) && 0 != $name) {
+ throw new InvalidArgumentException('Buttons cannot have empty names.');
+ }
+
+ $this->name = (string) $name;
+ $this->options = $options;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string|integer|FormBuilderInterface $child
+ * @param string|FormTypeInterface $type
+ * @param array $options
+ *
+ * @throws BadMethodCallException
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ * @param string|FormTypeInterface $type
+ * @param array $options
+ *
+ * @throws BadMethodCallException
+ */
+ public function create($name, $type = null, array $options = array())
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws BadMethodCallException
+ */
+ public function get($name)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $name
+ *
+ * @throws BadMethodCallException
+ */
+ public function remove($name)
+ {
+ throw new BadMethodCallException('Buttons cannot have children.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $name
+ *
+ * @return Boolean Always returns false.
+ */
+ public function has($name)
+ {
+ return false;
+ }
+
+ /**
+ * Returns the children.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function all()
+ {
+ return array();
+ }
+
+ /**
+ * Creates the button.
+ *
+ * @return Button The button
+ */
+ public function getForm()
+ {
+ return new Button($this->getFormConfig());
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param string $eventName
+ * @param callable $listener
+ * @param integer $priority
+ *
+ * @throws BadMethodCallException
+ */
+ public function addEventListener($eventName, $listener, $priority = 0)
+ {
+ throw new BadMethodCallException('Buttons do not support event listeners.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param EventSubscriberInterface $subscriber
+ *
+ * @throws BadMethodCallException
+ */
+ public function addEventSubscriber(EventSubscriberInterface $subscriber)
+ {
+ throw new BadMethodCallException('Buttons do not support event subscribers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataTransformerInterface $viewTransformer
+ * @param Boolean $forcePrepend
+ *
+ * @throws BadMethodCallException
+ */
+ public function addViewTransformer(DataTransformerInterface $viewTransformer, $forcePrepend = false)
+ {
+ throw new BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @throws BadMethodCallException
+ */
+ public function resetViewTransformers()
+ {
+ throw new BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataTransformerInterface $modelTransformer
+ * @param Boolean $forceAppend
+ *
+ * @throws BadMethodCallException
+ */
+ public function addModelTransformer(DataTransformerInterface $modelTransformer, $forceAppend = false)
+ {
+ throw new BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @throws BadMethodCallException
+ */
+ public function resetModelTransformers()
+ {
+ throw new BadMethodCallException('Buttons do not support data transformers.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttribute($name, $value)
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttributes(array $attributes)
+ {
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param DataMapperInterface $dataMapper
+ *
+ * @throws BadMethodCallException
+ */
+ public function setDataMapper(DataMapperInterface $dataMapper = null)
+ {
+ throw new BadMethodCallException('Buttons do not support data mappers.');
+ }
+
+ /**
+ * Set whether the button is disabled.
+ *
+ * @param Boolean $disabled Whether the button is disabled
+ *
+ * @return ButtonBuilder The button builder.
+ */
+ public function setDisabled($disabled)
+ {
+ $this->disabled = $disabled;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param mixed $emptyData
+ *
+ * @throws BadMethodCallException
+ */
+ public function setEmptyData($emptyData)
+ {
+ throw new BadMethodCallException('Buttons do not support empty data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $errorBubbling
+ *
+ * @throws BadMethodCallException
+ */
+ public function setErrorBubbling($errorBubbling)
+ {
+ throw new BadMethodCallException('Buttons do not support error bubbling.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $required
+ *
+ * @throws BadMethodCallException
+ */
+ public function setRequired($required)
+ {
+ throw new BadMethodCallException('Buttons cannot be required.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param null $propertyPath
+ *
+ * @throws BadMethodCallException
+ */
+ public function setPropertyPath($propertyPath)
+ {
+ throw new BadMethodCallException('Buttons do not support property paths.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $mapped
+ *
+ * @throws BadMethodCallException
+ */
+ public function setMapped($mapped)
+ {
+ throw new BadMethodCallException('Buttons do not support data mapping.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $byReference
+ *
+ * @throws BadMethodCallException
+ */
+ public function setByReference($byReference)
+ {
+ throw new BadMethodCallException('Buttons do not support data mapping.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $virtual
+ *
+ * @throws BadMethodCallException
+ */
+ public function setVirtual($virtual)
+ {
+ throw new BadMethodCallException('Buttons cannot be virtual.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $compound
+ *
+ * @throws BadMethodCallException
+ */
+ public function setCompound($compound)
+ {
+ throw new BadMethodCallException('Buttons cannot be compound.');
+ }
+
+ /**
+ * Sets the type of the button.
+ *
+ * @param ResolvedFormTypeInterface $type The type of the button.
+ *
+ * @return ButtonBuilder The button builder.
+ */
+ public function setType(ResolvedFormTypeInterface $type)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param array $data
+ *
+ * @throws BadMethodCallException
+ */
+ public function setData($data)
+ {
+ throw new BadMethodCallException('Buttons do not support data.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param Boolean $locked
+ *
+ * @throws BadMethodCallException
+ */
+ public function setDataLocked($locked)
+ {
+ throw new BadMethodCallException('Buttons do not support data locking.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * This method should not be invoked.
+ *
+ * @param FormFactoryInterface $formFactory
+ *
+ * @return void
+ *
+ * @throws BadMethodCallException
+ */
+ public function setFormFactory(FormFactoryInterface $formFactory)
+ {
+ throw new BadMethodCallException('Buttons do not support form factories.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $action
+ *
+ * @throws BadMethodCallException
+ */
+ public function setAction($action)
+ {
+ throw new BadMethodCallException('Buttons do not support actions.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param string $method
+ *
+ * @throws BadMethodCallException
+ */
+ public function setMethod($method)
+ {
+ throw new BadMethodCallException('Buttons do not support methods.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param RequestHandlerInterface $requestHandler
+ *
+ * @throws BadMethodCallException
+ */
+ public function setRequestHandler(RequestHandlerInterface $requestHandler)
+ {
+ throw new BadMethodCallException('Buttons do not support form processors.');
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param Boolean $initialize
+ *
+ * @throws BadMethodCallException
+ */
+ public function setAutoInitialize($initialize)
+ {
+ if (true === $initialize) {
+ throw new BadMethodCallException('Buttons do not support automatic initialization.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @param Boolean $inheritData
+ *
+ * @throws BadMethodCallException
+ */
+ public function setInheritData($inheritData)
+ {
+ throw new BadMethodCallException('Buttons do not support data inheritance.');
+ }
+
+ /**
+ * Builds and returns the button configuration.
+ *
+ * @return FormConfigInterface
+ */
+ public function getFormConfig()
+ {
+ // This method should be idempotent, so clone the builder
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getEventDispatcher()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getPropertyPath()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getMapped()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getByReference()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getVirtual()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getCompound()
+ {
+ return false;
+ }
+
+ /**
+ * Returns the form type used to construct the button.
+ *
+ * @return ResolvedFormTypeInterface The button's type.
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getViewTransformers()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return array Always returns an empty array.
+ */
+ public function getModelTransformers()
+ {
+ return array();
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getDataMapper()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getRequired()
+ {
+ return false;
+ }
+
+ /**
+ * Returns whether the button is disabled.
+ *
+ * @return Boolean Whether the button is disabled.
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getErrorBubbling()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getEmptyData()
+ {
+ return null;
+ }
+
+ /**
+ * Returns additional attributes of the button.
+ *
+ * @return array An array of key-value combinations.
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Returns whether the attribute with the given name exists.
+ *
+ * @param string $name The attribute name.
+ *
+ * @return Boolean Whether the attribute exists.
+ */
+ public function hasAttribute($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ /**
+ * Returns the value of the given attribute.
+ *
+ * @param string $name The attribute name.
+ * @param mixed $default The value returned if the attribute does not exist.
+ *
+ * @return mixed The attribute value.
+ */
+ public function getAttribute($name, $default = null)
+ {
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getData()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getDataClass()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getDataLocked()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getFormFactory()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getAction()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getMethod()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return null Always returns null.
+ */
+ public function getRequestHandler()
+ {
+ return null;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getAutoInitialize()
+ {
+ return false;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return Boolean Always returns false.
+ */
+ public function getInheritData()
+ {
+ return false;
+ }
+
+ /**
+ * Returns all options passed during the construction of the button.
+ *
+ * @return array The passed options.
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Returns whether a specific option exists.
+ *
+ * @param string $name The option name,
+ *
+ * @return Boolean Whether the option exists.
+ */
+ public function hasOption($name)
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ /**
+ * Returns the value of a specific option.
+ *
+ * @param string $name The option name.
+ * @param mixed $default The value returned if the option does not exist.
+ *
+ * @return mixed The option value.
+ */
+ public function getOption($name, $default = null)
+ {
+ return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return integer Always returns 0.
+ */
+ public function count()
+ {
+ return 0;
+ }
+
+ /**
+ * Unsupported method.
+ *
+ * @return \EmptyIterator Always returns an empty iterator.
+ */
+ public function getIterator()
+ {
+ return new \EmptyIterator();
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A type that should be converted into a {@link Button} instance.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ButtonTypeInterface extends FormTypeInterface
+{
+}
--- /dev/null
+CHANGELOG
+=========
+
+
+2.3.0
+------
+
+ * deprecated FormPerformanceTestCase and FormIntegrationTestCase in the Symfony\Component\Form\Tests namespace and moved them to the Symfony\Component\Form\Test namespace
+ * deprecated TypeTestCase in the Symfony\Component\Form\Tests\Extension\Core\Type namespace and moved it to the Symfony\Component\Form\Test namespace
+ * changed FormRenderer::humanize() to humanize also camel cased field name
+ * added RequestHandlerInterface and FormInterface::handleRequest()
+ * deprecated passing a Request instance to FormInterface::bind()
+ * added options "method" and "action" to FormType
+ * deprecated option "virtual" in favor "inherit_data"
+ * deprecated VirtualFormAwareIterator in favor of InheritDataAwareIterator
+ * [BC BREAK] removed the "array" type hint from DataMapperInterface
+ * improved forms inheriting their parent data to actually return that data from getData(), getNormData() and getViewData()
+ * added component-level exceptions for various SPL exceptions
+ changed all uses of the deprecated Exception class to use more specialized exceptions instead
+ removed NotInitializedException, NotValidException, TypeDefinitionException, TypeLoaderException, CreationException
+ * added events PRE_SUBMIT, SUBMIT and POST_SUBMIT
+ * deprecated events PRE_BIND, BIND and POST_BIND
+ * [BC BREAK] renamed bind() and isBound() in FormInterface to submit() and isSubmitted()
+ * added methods submit() and isSubmitted() to Form
+ * deprecated bind() and isBound() in Form
+ * deprecated AlreadyBoundException in favor of AlreadySubmittedException
+ * added support for PATCH requests
+ * [BC BREAK] added initialize() to FormInterface
+ * [BC BREAK] added getAutoInitialize() to FormConfigInterface
+ * [BC BREAK] added setAutoInitialize() to FormConfigBuilderInterface
+ * [BC BREAK] initialization for Form instances added to a form tree must be manually disabled
+ * PRE_SET_DATA is now guaranteed to be called after children were added by the form builder,
+ unless FormInterface::setData() is called manually
+ * fixed CSRF error message to be translated
+ * custom CSRF error messages can now be set through the "csrf_message" option
+ * fixed: expanded single-choice fields now show a radio button for the empty value
+
+2.2.0
+-----
+
+ * TrimListener now removes unicode whitespaces
+ * deprecated getParent(), setParent() and hasParent() in FormBuilderInterface
+ * FormInterface::add() now accepts a FormInterface instance OR a field's name, type and options
+ * removed special characters between the choice or text fields of DateType unless
+ the option "format" is set to a custom value
+ * deprecated FormException and introduced ExceptionInterface instead
+ * [BC BREAK] FormException is now an interface
+ * protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig()
+ * [BC BREAK] inserted argument `$message` in the constructor of `FormError`
+ * the PropertyPath class and related classes were moved to a dedicated
+ PropertyAccess component. During the move, InvalidPropertyException was
+ renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify()
+ can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods
+ getValue() and setValue() from PropertyPath were extracted into a new class
+ PropertyAccessor.
+ * added an optional PropertyAccessorInterface parameter to FormType,
+ ObjectChoiceList and PropertyPathMapper
+ * [BC BREAK] PropertyPathMapper and FormType now have a constructor
+ * [BC BREAK] setting the option "validation_groups" to ``false`` now disables validation
+ instead of assuming group "Default"
+
+2.1.0
+-----
+
+ * [BC BREAK] ``read_only`` field attribute now renders as ``readonly="readonly"``, use ``disabled`` instead
+ * [BC BREAK] child forms now aren't validated anymore by default
+ * made validation of form children configurable (new option: cascade_validation)
+ * added support for validation groups as callbacks
+ * made the translation catalogue configurable via the "translation_domain" option
+ * added Form::getErrorsAsString() to help debugging forms
+ * allowed setting different options for RepeatedType fields (like the label)
+ * added support for empty form name at root level, this enables rendering forms
+ without form name prefix in field names
+ * [BC BREAK] form and field names must start with a letter, digit or underscore
+ and only contain letters, digits, underscores, hyphens and colons
+ * [BC BREAK] changed default name of the prototype in the "collection" type
+ from "$$name$$" to "\__name\__". No dollars are appended/prepended to custom
+ names anymore.
+ * [BC BREAK] improved ChoiceListInterface
+ * [BC BREAK] added SimpleChoiceList and LazyChoiceList as replacement of
+ ArrayChoiceList
+ * added ChoiceList and ObjectChoiceList to use objects as choices
+ * [BC BREAK] removed EntitiesToArrayTransformer and EntityToIdTransformer.
+ The former has been replaced by CollectionToArrayTransformer in combination
+ with EntityChoiceList, the latter is not required in the core anymore.
+ * [BC BREAK] renamed
+ * ArrayToBooleanChoicesTransformer to ChoicesToBooleanArrayTransformer
+ * ScalarToBooleanChoicesTransformer to ChoiceToBooleanArrayTransformer
+ * ArrayToChoicesTransformer to ChoicesToValuesTransformer
+ * ScalarToChoiceTransformer to ChoiceToValueTransformer
+ to be consistent with the naming in ChoiceListInterface.
+ They were merged into ChoiceList and have no public equivalent anymore.
+ * choice fields now throw a FormException if neither the "choices" nor the
+ "choice_list" option is set
+ * the radio type is now a child of the checkbox type
+ * the collection, choice (with multiple selection) and entity (with multiple
+ selection) types now make use of addXxx() and removeXxx() methods in your
+ model if you set "by_reference" to false. For a custom, non-recognized
+ singular form, set the "property_path" option like this: "plural|singular"
+ * forms now don't create an empty object anymore if they are completely
+ empty and not required. The empty value for such forms is null.
+ * added constant Guess::VERY_HIGH_CONFIDENCE
+ * [BC BREAK] The methods `add`, `remove`, `setParent`, `bind` and `setData`
+ in class Form now throw an exception if the form is already bound
+ * fields of constrained classes without a NotBlank or NotNull constraint are
+ set to not required now, as stated in the docs
+ * fixed TimeType and DateTimeType to not display seconds when "widget" is
+ "single_text" unless "with_seconds" is set to true
+ * checkboxes of in an expanded multiple-choice field don't include the choice
+ in their name anymore. Their names terminate with "[]" now.
+ * deprecated FormValidatorInterface and substituted its implementations
+ by event subscribers
+ * simplified CSRF protection and removed the csrf type
+ * deprecated FieldType and merged it into FormType
+ * added new option "compound" that lets you switch between field and form behavior
+ * [BC BREAK] renamed theme blocks
+ * "field_*" to "form_*"
+ * "field_widget" to "form_widget_simple"
+ * "widget_choice_options" to "choice_widget_options"
+ * "generic_label" to "form_label"
+ * added theme blocks "form_widget_compound", "choice_widget_expanded" and
+ "choice_widget_collapsed" to make theming more modular
+ * ValidatorTypeGuesser now guesses "collection" for array type constraint
+ * added method `guessPattern` to FormTypeGuesserInterface to guess which pattern to use in the HTML5 attribute "pattern"
+ * deprecated method `guessMinLength` in favor of `guessPattern`
+ * labels don't display field attributes anymore. Label attributes can be
+ passed in the "label_attr" option/variable
+ * added option "mapped" which should be used instead of setting "property_path" to false
+ * [BC BREAK] "data_class" now *must* be set if a form maps to an object and should be left empty otherwise
+ * improved error mapping on forms
+ * dot (".") rules are now allowed to map errors assigned to a form to
+ one of its children
+ * errors are not mapped to unsynchronized forms anymore
+ * [BC BREAK] changed Form constructor to accept a single `FormConfigInterface` object
+ * [BC BREAK] changed argument order in the FormBuilder constructor
+ * added Form method `getViewData`
+ * deprecated Form methods
+ * `getTypes`
+ * `getErrorBubbling`
+ * `getNormTransformers`
+ * `getClientTransformers`
+ * `getAttribute`
+ * `hasAttribute`
+ * `getClientData`
+ * added FormBuilder methods
+ * `getTypes`
+ * `addViewTransformer`
+ * `getViewTransformers`
+ * `resetViewTransformers`
+ * `addModelTransformer`
+ * `getModelTransformers`
+ * `resetModelTransformers`
+ * deprecated FormBuilder methods
+ * `prependClientTransformer`
+ * `appendClientTransformer`
+ * `getClientTransformers`
+ * `resetClientTransformers`
+ * `prependNormTransformer`
+ * `appendNormTransformer`
+ * `getNormTransformers`
+ * `resetNormTransformers`
+ * deprecated the option "validation_constraint" in favor of the new
+ option "constraints"
+ * removed superfluous methods from DataMapperInterface
+ * `mapFormToData`
+ * `mapDataToForm`
+ * added `setDefaultOptions` to FormTypeInterface and FormTypeExtensionInterface
+ which accepts an OptionsResolverInterface instance
+ * deprecated the methods `getDefaultOptions` and `getAllowedOptionValues`
+ in FormTypeInterface and FormTypeExtensionInterface
+ * options passed during construction can now be accessed from FormConfigInterface
+ * added FormBuilderInterface and FormConfigEditorInterface
+ * [BC BREAK] the method `buildForm` in FormTypeInterface and FormTypeExtensionInterface
+ now receives a FormBuilderInterface instead of a FormBuilder instance
+ * [BC BREAK] the method `buildViewBottomUp` was renamed to `finishView` in
+ FormTypeInterface and FormTypeExtensionInterface
+ * [BC BREAK] the options array is now passed as last argument of the
+ methods
+ * `buildView`
+ * `finishView`
+ in FormTypeInterface and FormTypeExtensionInterface
+ * [BC BREAK] no options are passed to `getParent` of FormTypeInterface anymore
+ * deprecated DataEvent and FilterDataEvent in favor of the new FormEvent which is
+ now passed to all events thrown by the component
+ * FormEvents::BIND now replaces FormEvents::BIND_NORM_DATA
+ * FormEvents::PRE_SET_DATA now replaces FormEvents::SET_DATA
+ * FormEvents::PRE_BIND now replaces FormEvents::BIND_CLIENT_DATA
+ * deprecated FormEvents::SET_DATA, FormEvents::BIND_CLIENT_DATA and
+ FormEvents::BIND_NORM_DATA
+ * [BC BREAK] reversed the order of the first two arguments to `createNamed`
+ and `createNamedBuilder` in `FormFactoryInterface`
+ * deprecated `getChildren` in Form and FormBuilder in favor of `all`
+ * deprecated `hasChildren` in Form and FormBuilder in favor of `count`
+ * FormBuilder now implements \IteratorAggregate
+ * [BC BREAK] compound forms now always need a data mapper
+ * FormBuilder now maintains the order when explicitly adding form builders as children
+ * ChoiceType now doesn't add the empty value anymore if the choices already contain an empty element
+ * DateType, TimeType and DateTimeType now show empty values again if not required
+ * [BC BREAK] fixed rendering of errors for DateType, BirthdayType and similar ones
+ * [BC BREAK] fixed: form constraints are only validated if they belong to the validated group
+ * deprecated `bindRequest` in `Form` and replaced it by a listener to FormEvents::PRE_BIND
+ * fixed: the "data" option supersedes default values from the model
+ * changed DateType to refer to the "format" option for calculating the year and day choices instead
+ of padding them automatically
+ * [BC BREAK] DateType defaults to the format "yyyy-MM-dd" now if the widget is
+ "single_text", in order to support the HTML 5 date field out of the box
+ * added the option "format" to DateTimeType
+ * [BC BREAK] DateTimeType now outputs RFC 3339 dates by default, as generated and
+ consumed by HTML5 browsers, if the widget is "single_text"
+ * deprecated the options "data_timezone" and "user_timezone" in DateType, DateTimeType and TimeType
+ and renamed them to "model_timezone" and "view_timezone"
+ * fixed: TransformationFailedExceptions thrown in the model transformer are now caught by the form
+ * added FormRegistryInterface, ResolvedFormTypeInterface and ResolvedFormTypeFactoryInterface
+ * deprecated FormFactory methods
+ * `addType`
+ * `hasType`
+ * `getType`
+ * [BC BREAK] FormFactory now expects a FormRegistryInterface and a ResolvedFormTypeFactoryInterface as constructor argument
+ * [BC BREAK] The method `createBuilder` in FormTypeInterface is not supported anymore for performance reasons
+ * [BC BREAK] Removed `setTypes` from FormBuilder
+ * deprecated AbstractType methods
+ * `getExtensions`
+ * `setExtensions`
+ * ChoiceType now caches its created choice lists to improve performance
+ * [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection
+ field now have the same block names, which contains "entry" where it previously contained the row index.
+ * [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
+ * added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces
+ * [BC BREAK] removed the following methods from FormUtil:
+ * `toArrayKey`
+ * `toArrayKeys`
+ * `isChoiceGroup`
+ * `isChoiceSelected`
+ * [BC BREAK] renamed method `renderBlock` in FormHelper to `block` and changed its signature
+ * made FormView properties public and deprecated their accessor methods
+ * made the normalized data of a form accessible in the template through the variable "form.vars.data"
+ * made the original data of a choice accessible in the template through the property "choice.data"
+ * added convenience class Forms and FormFactoryBuilderInterface
--- /dev/null
+<?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\Form;
+
+class CallbackTransformer implements DataTransformerInterface
+{
+ /**
+ * The callback used for forward transform
+ * @var \Closure
+ */
+ private $transform;
+
+ /**
+ * The callback used for reverse transform
+ * @var \Closure
+ */
+ private $reverseTransform;
+
+ /**
+ * Constructor.
+ *
+ * @param \Closure $transform The forward transform callback
+ * @param \Closure $reverseTransform The reverse transform callback
+ */
+ public function __construct(\Closure $transform, \Closure $reverseTransform)
+ {
+ $this->transform = $transform;
+ $this->reverseTransform = $reverseTransform;
+ }
+
+ /**
+ * Transforms a value from the original representation to a transformed representation.
+ *
+ * @param mixed $data The value in the original representation
+ *
+ * @return mixed The value in the transformed representation
+ *
+ * @throws UnexpectedTypeException when the argument is not a string
+ * @throws TransformationFailedException when the transformation fails
+ */
+ public function transform($data)
+ {
+ return call_user_func($this->transform, $data);
+ }
+
+ /**
+ * Transforms a value from the transformed representation to its original
+ * representation.
+ *
+ * @param mixed $data The value in the transformed representation
+ *
+ * @return mixed The value in the original representation
+ *
+ * @throws UnexpectedTypeException when the argument is not of the expected type
+ * @throws TransformationFailedException when the transformation fails
+ */
+ public function reverseTransform($data)
+ {
+ return call_user_func($this->reverseTransform, $data);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A clickable form element.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ClickableInterface
+{
+ /**
+ * Returns whether this element was clicked.
+ *
+ * @return Boolean Whether this element was clicked.
+ */
+ public function isClicked();
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface DataMapperInterface
+{
+ /**
+ * Maps properties of some data to a list of forms.
+ *
+ * @param mixed $data Structured data.
+ * @param FormInterface[] $forms A list of {@link FormInterface} instances.
+ *
+ * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported.
+ */
+ public function mapDataToForms($data, $forms);
+
+ /**
+ * Maps the data of a list of forms into the properties of some data.
+ *
+ * @param FormInterface[] $forms A list of {@link FormInterface} instances.
+ * @param mixed $data Structured data.
+ *
+ * @throws Exception\UnexpectedTypeException if the type of the data parameter is not supported.
+ */
+ public function mapFormsToData($forms, &$data);
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms a value between different representations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface DataTransformerInterface
+{
+ /**
+ * Transforms a value from the original representation to a transformed representation.
+ *
+ * This method is called on two occasions inside a form field:
+ *
+ * 1. When the form field is initialized with the data attached from the datasource (object or array).
+ * 2. When data from a request is submitted using {@link Form::submit()} to transform the new input data
+ * back into the renderable format. For example if you have a date field and submit '2009-10-10'
+ * you might accept this value because its easily parsed, but the transformer still writes back
+ * "2009/10/10" onto the form field (for further displaying or other purposes).
+ *
+ * This method must be able to deal with empty values. Usually this will
+ * be NULL, but depending on your implementation other empty values are
+ * possible as well (such as empty strings). The reasoning behind this is
+ * that value transformers must be chainable. If the transform() method
+ * of the first value transformer outputs NULL, the second value transformer
+ * must be able to process that value.
+ *
+ * By convention, transform() should return an empty string if NULL is
+ * passed.
+ *
+ * @param mixed $value The value in the original representation
+ *
+ * @return mixed The value in the transformed representation
+ *
+ * @throws TransformationFailedException When the transformation fails.
+ */
+ public function transform($value);
+
+ /**
+ * Transforms a value from the transformed representation to its original
+ * representation.
+ *
+ * This method is called when {@link Form::submit()} is called to transform the requests tainted data
+ * into an acceptable format for your data processing/model layer.
+ *
+ * This method must be able to deal with empty values. Usually this will
+ * be an empty string, but depending on your implementation other empty
+ * values are possible as well (such as empty strings). The reasoning behind
+ * this is that value transformers must be chainable. If the
+ * reverseTransform() method of the first value transformer outputs an
+ * empty string, the second value transformer must be able to process that
+ * value.
+ *
+ * By convention, reverseTransform() should return NULL if an empty string
+ * is passed.
+ *
+ * @param mixed $value The value in the transformed representation
+ *
+ * @return mixed The value in the original representation
+ *
+ * @throws TransformationFailedException When the transformation fails.
+ */
+ public function reverseTransform($value);
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Alias of {@link AlreadySubmittedException}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link AlreadySubmittedException} instead.
+ */
+class AlreadyBoundException extends LogicException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Thrown when an operation is called that is not acceptable after submitting
+ * a form.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class AlreadySubmittedException extends AlreadyBoundException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base BadMethodCallException for the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+class ErrorMappingException extends RuntimeException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base ExceptionInterface for the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base InvalidArgumentException for the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+class InvalidConfigurationException extends InvalidArgumentException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base LogicException for Form component.
+ *
+ * @author Alexander Kotynia <aleksander.kot@gmail.com>
+ */
+class LogicException extends \LogicException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base OutOfBoundsException for Form component.
+ *
+ * @author Alexander Kotynia <aleksander.kot@gmail.com>
+ */
+class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Base RuntimeException for the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+class StringCastException extends RuntimeException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+/**
+ * Indicates a value transformation error.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TransformationFailedException extends RuntimeException
+{
+}
--- /dev/null
+<?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\Form\Exception;
+
+class UnexpectedTypeException extends InvalidArgumentException
+{
+ public function __construct($value, $expectedType)
+ {
+ parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Exception\InvalidConfigurationException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+/**
+ * A choice list for choices of arbitrary data types.
+ *
+ * Choices and labels are passed in two arrays. The indices of the choices
+ * and the labels should match. Choices may also be given as hierarchy of
+ * unlimited depth by creating nested arrays. The title of the sub-hierarchy
+ * can be stored in the array key pointing to the nested array. The topmost
+ * level of the hierarchy may also be a \Traversable.
+ *
+ * <code>
+ * $choices = array(true, false);
+ * $labels = array('Agree', 'Disagree');
+ * $choiceList = new ChoiceList($choices, $labels);
+ * </code>
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoiceList implements ChoiceListInterface
+{
+ /**
+ * The choices with their indices as keys.
+ *
+ * @var array
+ */
+ private $choices = array();
+
+ /**
+ * The choice values with the indices of the matching choices as keys.
+ *
+ * @var array
+ */
+ private $values = array();
+
+ /**
+ * The preferred view objects as hierarchy containing also the choice groups
+ * with the indices of the matching choices as bottom-level keys.
+ *
+ * @var array
+ */
+ private $preferredViews = array();
+
+ /**
+ * The non-preferred view objects as hierarchy containing also the choice
+ * groups with the indices of the matching choices as bottom-level keys.
+ *
+ * @var array
+ */
+ private $remainingViews = array();
+
+ /**
+ * Creates a new choice list.
+ *
+ * @param array|\Traversable $choices The array of choices. Choices may also be given
+ * as hierarchy of unlimited depth. Hierarchies are
+ * created by creating nested arrays. The title of
+ * the sub-hierarchy can be stored in the array
+ * key pointing to the nested array. The topmost
+ * level of the hierarchy may also be a \Traversable.
+ * @param array $labels The array of labels. The structure of this array
+ * should match the structure of $choices.
+ * @param array $preferredChoices A flat array of choices that should be
+ * presented to the user with priority.
+ *
+ * @throws UnexpectedTypeException If the choices are not an array or \Traversable.
+ */
+ public function __construct($choices, array $labels, array $preferredChoices = array())
+ {
+ if (!is_array($choices) && !$choices instanceof \Traversable) {
+ throw new UnexpectedTypeException($choices, 'array or \Traversable');
+ }
+
+ $this->initialize($choices, $labels, $preferredChoices);
+ }
+
+ /**
+ * Initializes the list with choices.
+ *
+ * Safe to be called multiple times. The list is cleared on every call.
+ *
+ * @param array|\Traversable $choices The choices to write into the list.
+ * @param array $labels The labels belonging to the choices.
+ * @param array $preferredChoices The choices to display with priority.
+ */
+ protected function initialize($choices, array $labels, array $preferredChoices)
+ {
+ $this->choices = array();
+ $this->values = array();
+ $this->preferredViews = array();
+ $this->remainingViews = array();
+
+ $this->addChoices(
+ $this->preferredViews,
+ $this->remainingViews,
+ $choices,
+ $labels,
+ $preferredChoices
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChoices()
+ {
+ return $this->choices;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreferredViews()
+ {
+ return $this->preferredViews;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingViews()
+ {
+ return $this->remainingViews;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChoicesForValues(array $values)
+ {
+ $values = $this->fixValues($values);
+ $choices = array();
+
+ foreach ($values as $j => $givenValue) {
+ foreach ($this->values as $i => $value) {
+ if ($value === $givenValue) {
+ $choices[] = $this->choices[$i];
+ unset($values[$j]);
+
+ if (0 === count($values)) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ return $choices;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValuesForChoices(array $choices)
+ {
+ $choices = $this->fixChoices($choices);
+ $values = array();
+
+ foreach ($this->choices as $i => $choice) {
+ foreach ($choices as $j => $givenChoice) {
+ if ($choice === $givenChoice) {
+ $values[] = $this->values[$i];
+ unset($choices[$j]);
+
+ if (0 === count($choices)) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndicesForChoices(array $choices)
+ {
+ $choices = $this->fixChoices($choices);
+ $indices = array();
+
+ foreach ($this->choices as $i => $choice) {
+ foreach ($choices as $j => $givenChoice) {
+ if ($choice === $givenChoice) {
+ $indices[] = $i;
+ unset($choices[$j]);
+
+ if (0 === count($choices)) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ return $indices;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndicesForValues(array $values)
+ {
+ $values = $this->fixValues($values);
+ $indices = array();
+
+ foreach ($this->values as $i => $value) {
+ foreach ($values as $j => $givenValue) {
+ if ($value === $givenValue) {
+ $indices[] = $i;
+ unset($values[$j]);
+
+ if (0 === count($values)) {
+ break 2;
+ }
+ }
+ }
+ }
+
+ return $indices;
+ }
+
+ /**
+ * Recursively adds the given choices to the list.
+ *
+ * @param array $bucketForPreferred The bucket where to store the preferred
+ * view objects.
+ * @param array $bucketForRemaining The bucket where to store the
+ * non-preferred view objects.
+ * @param array|\Traversable $choices The list of choices.
+ * @param array $labels The labels corresponding to the choices.
+ * @param array $preferredChoices The preferred choices.
+ *
+ * @throws InvalidArgumentException If the structures of the choices and labels array do not match.
+ * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
+ */
+ protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
+ {
+ // Add choices to the nested buckets
+ foreach ($choices as $group => $choice) {
+ if (!array_key_exists($group, $labels)) {
+ throw new InvalidArgumentException('The structures of the choices and labels array do not match.');
+ }
+
+ if (is_array($choice)) {
+ // Don't do the work if the array is empty
+ if (count($choice) > 0) {
+ $this->addChoiceGroup(
+ $group,
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $choice,
+ $labels[$group],
+ $preferredChoices
+ );
+ }
+ } else {
+ $this->addChoice(
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $choice,
+ $labels[$group],
+ $preferredChoices
+ );
+ }
+ }
+ }
+
+ /**
+ * Recursively adds a choice group.
+ *
+ * @param string $group The name of the group.
+ * @param array $bucketForPreferred The bucket where to store the preferred
+ * view objects.
+ * @param array $bucketForRemaining The bucket where to store the
+ * non-preferred view objects.
+ * @param array $choices The list of choices in the group.
+ * @param array $labels The labels corresponding to the choices in the group.
+ * @param array $preferredChoices The preferred choices.
+ *
+ * @throws InvalidConfigurationException If no valid value or index could be created for a choice.
+ */
+ protected function addChoiceGroup($group, array &$bucketForPreferred, array &$bucketForRemaining, array $choices, array $labels, array $preferredChoices)
+ {
+ // If this is a choice group, create a new level in the choice
+ // key hierarchy
+ $bucketForPreferred[$group] = array();
+ $bucketForRemaining[$group] = array();
+
+ $this->addChoices(
+ $bucketForPreferred[$group],
+ $bucketForRemaining[$group],
+ $choices,
+ $labels,
+ $preferredChoices
+ );
+
+ // Remove child levels if empty
+ if (empty($bucketForPreferred[$group])) {
+ unset($bucketForPreferred[$group]);
+ }
+ if (empty($bucketForRemaining[$group])) {
+ unset($bucketForRemaining[$group]);
+ }
+ }
+
+ /**
+ * Adds a new choice.
+ *
+ * @param array $bucketForPreferred The bucket where to store the preferred
+ * view objects.
+ * @param array $bucketForRemaining The bucket where to store the
+ * non-preferred view objects.
+ * @param mixed $choice The choice to add.
+ * @param string $label The label for the choice.
+ * @param array $preferredChoices The preferred choices.
+ *
+ * @throws InvalidConfigurationException If no valid value or index could be created.
+ */
+ protected function addChoice(array &$bucketForPreferred, array &$bucketForRemaining, $choice, $label, array $preferredChoices)
+ {
+ $index = $this->createIndex($choice);
+
+ if ('' === $index || null === $index || !FormConfigBuilder::isValidName((string) $index)) {
+ throw new InvalidConfigurationException(sprintf('The index "%s" created by the choice list is invalid. It should be a valid, non-empty Form name.', $index));
+ }
+
+ $value = $this->createValue($choice);
+
+ if (!is_string($value)) {
+ throw new InvalidConfigurationException(sprintf('The value created by the choice list is of type "%s", but should be a string.', gettype($value)));
+ }
+
+ $view = new ChoiceView($choice, $value, $label);
+
+ $this->choices[$index] = $this->fixChoice($choice);
+ $this->values[$index] = $value;
+
+ if ($this->isPreferred($choice, $preferredChoices)) {
+ $bucketForPreferred[$index] = $view;
+ } else {
+ $bucketForRemaining[$index] = $view;
+ }
+ }
+
+ /**
+ * Returns whether the given choice should be preferred judging by the
+ * given array of preferred choices.
+ *
+ * Extension point to optimize performance by changing the structure of the
+ * $preferredChoices array.
+ *
+ * @param mixed $choice The choice to test.
+ * @param array $preferredChoices An array of preferred choices.
+ *
+ * @return Boolean Whether the choice is preferred.
+ */
+ protected function isPreferred($choice, array $preferredChoices)
+ {
+ return false !== array_search($choice, $preferredChoices, true);
+ }
+
+ /**
+ * Creates a new unique index for this choice.
+ *
+ * Extension point to change the indexing strategy.
+ *
+ * @param mixed $choice The choice to create an index for
+ *
+ * @return integer|string A unique index containing only ASCII letters,
+ * digits and underscores.
+ */
+ protected function createIndex($choice)
+ {
+ return count($this->choices);
+ }
+
+ /**
+ * Creates a new unique value for this choice.
+ *
+ * By default, an integer is generated since it cannot be guaranteed that
+ * all values in the list are convertible to (unique) strings. Subclasses
+ * can override this behaviour if they can guarantee this property.
+ *
+ * @param mixed $choice The choice to create a value for
+ *
+ * @return string A unique string.
+ */
+ protected function createValue($choice)
+ {
+ return (string) count($this->values);
+ }
+
+ /**
+ * Fixes the data type of the given choice value to avoid comparison
+ * problems.
+ *
+ * @param mixed $value The choice value.
+ *
+ * @return string The value as string.
+ */
+ protected function fixValue($value)
+ {
+ return (string) $value;
+ }
+
+ /**
+ * Fixes the data types of the given choice values to avoid comparison
+ * problems.
+ *
+ * @param array $values The choice values.
+ *
+ * @return array The values as strings.
+ */
+ protected function fixValues(array $values)
+ {
+ foreach ($values as $i => $value) {
+ $values[$i] = $this->fixValue($value);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Fixes the data type of the given choice index to avoid comparison
+ * problems.
+ *
+ * @param mixed $index The choice index.
+ *
+ * @return integer|string The index as PHP array key.
+ */
+ protected function fixIndex($index)
+ {
+ if (is_bool($index) || (string) (int) $index === (string) $index) {
+ return (int) $index;
+ }
+
+ return (string) $index;
+ }
+
+ /**
+ * Fixes the data types of the given choice indices to avoid comparison
+ * problems.
+ *
+ * @param array $indices The choice indices.
+ *
+ * @return array The indices as strings.
+ */
+ protected function fixIndices(array $indices)
+ {
+ foreach ($indices as $i => $index) {
+ $indices[$i] = $this->fixIndex($index);
+ }
+
+ return $indices;
+ }
+
+ /**
+ * Fixes the data type of the given choice to avoid comparison problems.
+ *
+ * Extension point. In this implementation, choices are guaranteed to
+ * always maintain their type and thus can be typesafely compared.
+ *
+ * @param mixed $choice The choice.
+ *
+ * @return mixed The fixed choice.
+ */
+ protected function fixChoice($choice)
+ {
+ return $choice;
+ }
+
+ /**
+ * Fixes the data type of the given choices to avoid comparison problems.
+ *
+ * @param array $choices The choices.
+ *
+ * @return array The fixed choices.
+ *
+ * @see fixChoice
+ */
+ protected function fixChoices(array $choices)
+ {
+ return $choices;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\ChoiceList;
+
+/**
+ * Contains choices that can be selected in a form field.
+ *
+ * Each choice has three different properties:
+ *
+ * - Choice: The choice that should be returned to the application by the
+ * choice field. Can be any scalar value or an object, but no
+ * array.
+ * - Label: A text representing the choice that is displayed to the user.
+ * - Value: A uniquely identifying value that can contain arbitrary
+ * characters, but no arrays or objects. This value is displayed
+ * in the HTML "value" attribute.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ChoiceListInterface
+{
+ /**
+ * Returns the list of choices
+ *
+ * @return array The choices with their indices as keys
+ */
+ public function getChoices();
+
+ /**
+ * Returns the values for the choices
+ *
+ * @return array The values with the corresponding choice indices as keys
+ */
+ public function getValues();
+
+ /**
+ * Returns the choice views of the preferred choices as nested array with
+ * the choice groups as top-level keys.
+ *
+ * Example:
+ *
+ * <source>
+ * array(
+ * 'Group 1' => array(
+ * 10 => ChoiceView object,
+ * 20 => ChoiceView object,
+ * ),
+ * 'Group 2' => array(
+ * 30 => ChoiceView object,
+ * ),
+ * )
+ * </source>
+ *
+ * @return array A nested array containing the views with the corresponding
+ * choice indices as keys on the lowest levels and the choice
+ * group names in the keys of the higher levels
+ */
+ public function getPreferredViews();
+
+ /**
+ * Returns the choice views of the choices that are not preferred as nested
+ * array with the choice groups as top-level keys.
+ *
+ * Example:
+ *
+ * <source>
+ * array(
+ * 'Group 1' => array(
+ * 10 => ChoiceView object,
+ * 20 => ChoiceView object,
+ * ),
+ * 'Group 2' => array(
+ * 30 => ChoiceView object,
+ * ),
+ * )
+ * </source>
+ *
+ * @return array A nested array containing the views with the corresponding
+ * choice indices as keys on the lowest levels and the choice
+ * group names in the keys of the higher levels
+ *
+ * @see getPreferredValues
+ */
+ public function getRemainingViews();
+
+ /**
+ * Returns the choices corresponding to the given values.
+ *
+ * The choices can have any data type.
+ *
+ * @param array $values An array of choice values. Not existing values in
+ * this array are ignored
+ *
+ * @return array An array of choices with ascending, 0-based numeric keys
+ */
+ public function getChoicesForValues(array $values);
+
+ /**
+ * Returns the values corresponding to the given choices.
+ *
+ * The values must be strings.
+ *
+ * @param array $choices An array of choices. Not existing choices in this
+ * array are ignored
+ *
+ * @return array An array of choice values with ascending, 0-based numeric
+ * keys
+ */
+ public function getValuesForChoices(array $choices);
+
+ /**
+ * Returns the indices corresponding to the given choices.
+ *
+ * The indices must be positive integers or strings accepted by
+ * {@link FormConfigBuilder::validateName()}.
+ *
+ * The index "placeholder" is internally reserved.
+ *
+ * @param array $choices An array of choices. Not existing choices in this
+ * array are ignored
+ *
+ * @return array An array of indices with ascending, 0-based numeric keys
+ */
+ public function getIndicesForChoices(array $choices);
+
+ /**
+ * Returns the indices corresponding to the given values.
+ *
+ * The indices must be positive integers or strings accepted by
+ * {@link FormConfigBuilder::validateName()}.
+ *
+ * The index "placeholder" is internally reserved.
+ *
+ * @param array $values An array of choice values. Not existing values in
+ * this array are ignored
+ *
+ * @return array An array of indices with ascending, 0-based numeric keys
+ */
+ public function getIndicesForValues(array $values);
+}
--- /dev/null
+<?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\Form\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * A choice list that is loaded lazily
+ *
+ * This list loads itself as soon as any of the getters is accessed for the
+ * first time. You should implement loadChoiceList() in your child classes,
+ * which should return a ChoiceListInterface instance.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class LazyChoiceList implements ChoiceListInterface
+{
+ /**
+ * The loaded choice list
+ *
+ * @var ChoiceListInterface
+ */
+ private $choiceList;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChoices()
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getChoices();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues()
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPreferredViews()
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getPreferredViews();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingViews()
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getRemainingViews();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChoicesForValues(array $values)
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getChoicesForValues($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValuesForChoices(array $choices)
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getValuesForChoices($choices);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndicesForChoices(array $choices)
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getIndicesForChoices($choices);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndicesForValues(array $values)
+ {
+ if (!$this->choiceList) {
+ $this->load();
+ }
+
+ return $this->choiceList->getIndicesForValues($values);
+ }
+
+ /**
+ * Loads the choice list
+ *
+ * Should be implemented by child classes.
+ *
+ * @return ChoiceListInterface The loaded choice list
+ */
+ abstract protected function loadChoiceList();
+
+ private function load()
+ {
+ $choiceList = $this->loadChoiceList();
+
+ if (!$choiceList instanceof ChoiceListInterface) {
+ throw new InvalidArgumentException(sprintf('loadChoiceList() should return a ChoiceListInterface instance. Got %s', gettype($choiceList)));
+ }
+
+ $this->choiceList = $choiceList;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Exception\StringCastException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+
+/**
+ * A choice list for object choices.
+ *
+ * Supports generation of choice labels, choice groups and choice values
+ * by calling getters of the object (or associated objects).
+ *
+ * <code>
+ * $choices = array($user1, $user2);
+ *
+ * // call getName() to determine the choice labels
+ * $choiceList = new ObjectChoiceList($choices, 'name');
+ * </code>
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ObjectChoiceList extends ChoiceList
+{
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
+ /**
+ * The property path used to obtain the choice label.
+ *
+ * @var PropertyPath
+ */
+ private $labelPath;
+
+ /**
+ * The property path used for object grouping.
+ *
+ * @var PropertyPath
+ */
+ private $groupPath;
+
+ /**
+ * The property path used to obtain the choice value.
+ *
+ * @var PropertyPath
+ */
+ private $valuePath;
+
+ /**
+ * Creates a new object choice list.
+ *
+ * @param array|\Traversable $choices The array of choices. Choices may also be given
+ * as hierarchy of unlimited depth by creating nested
+ * arrays. The title of the sub-hierarchy can be
+ * stored in the array key pointing to the nested
+ * array. The topmost level of the hierarchy may also
+ * be a \Traversable.
+ * @param string $labelPath A property path pointing to the property used
+ * for the choice labels. The value is obtained
+ * by calling the getter on the object. If the
+ * path is NULL, the object's __toString() method
+ * is used instead.
+ * @param array $preferredChoices A flat array of choices that should be
+ * presented to the user with priority.
+ * @param string $groupPath A property path pointing to the property used
+ * to group the choices. Only allowed if
+ * the choices are given as flat array.
+ * @param string $valuePath A property path pointing to the property used
+ * for the choice values. If not given, integers
+ * are generated instead.
+ * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths.
+ */
+ public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, PropertyAccessorInterface $propertyAccessor = null)
+ {
+ $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
+ $this->labelPath = null !== $labelPath ? new PropertyPath($labelPath) : null;
+ $this->groupPath = null !== $groupPath ? new PropertyPath($groupPath) : null;
+ $this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null;
+
+ parent::__construct($choices, array(), $preferredChoices);
+ }
+
+ /**
+ * Initializes the list with choices.
+ *
+ * Safe to be called multiple times. The list is cleared on every call.
+ *
+ * @param array|\Traversable $choices The choices to write into the list.
+ * @param array $labels Ignored.
+ * @param array $preferredChoices The choices to display with priority.
+ *
+ * @throws InvalidArgumentException When passing a hierarchy of choices and using
+ * the "groupPath" option at the same time.
+ */
+ protected function initialize($choices, array $labels, array $preferredChoices)
+ {
+ if (null !== $this->groupPath) {
+ $groupedChoices = array();
+
+ foreach ($choices as $i => $choice) {
+ if (is_array($choice)) {
+ throw new InvalidArgumentException('You should pass a plain object array (without groups) when using the "groupPath" option.');
+ }
+
+ try {
+ $group = $this->propertyAccessor->getValue($choice, $this->groupPath);
+ } catch (NoSuchPropertyException $e) {
+ // Don't group items whose group property does not exist
+ // see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf
+ $group = null;
+ }
+
+ if (null === $group) {
+ $groupedChoices[$i] = $choice;
+ } else {
+ if (!isset($groupedChoices[$group])) {
+ $groupedChoices[$group] = array();
+ }
+
+ $groupedChoices[$group][$i] = $choice;
+ }
+ }
+
+ $choices = $groupedChoices;
+ }
+
+ $labels = array();
+
+ $this->extractLabels($choices, $labels);
+
+ parent::initialize($choices, $labels, $preferredChoices);
+ }
+
+ /**
+ * Creates a new unique value for this choice.
+ *
+ * If a property path for the value was given at object creation,
+ * the getter behind that path is now called to obtain a new value.
+ * Otherwise a new integer is generated.
+ *
+ * @param mixed $choice The choice to create a value for
+ *
+ * @return integer|string A unique value without character limitations.
+ */
+ protected function createValue($choice)
+ {
+ if ($this->valuePath) {
+ return (string) $this->propertyAccessor->getValue($choice, $this->valuePath);
+ }
+
+ return parent::createValue($choice);
+ }
+
+ private function extractLabels($choices, array &$labels)
+ {
+ foreach ($choices as $i => $choice) {
+ if (is_array($choice)) {
+ $labels[$i] = array();
+ $this->extractLabels($choice, $labels[$i]);
+ } elseif ($this->labelPath) {
+ $labels[$i] = $this->propertyAccessor->getValue($choice, $this->labelPath);
+ } elseif (method_exists($choice, '__toString')) {
+ $labels[$i] = (string) $choice;
+ } else {
+ throw new StringCastException(sprintf('A "__toString()" method was not found on the objects of type "%s" passed to the choice field. To read a custom getter instead, set the argument $labelPath to the desired property path.', get_class($choice)));
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\ChoiceList;
+
+/**
+ * A choice list for choices of type string or integer.
+ *
+ * Choices and their associated labels can be passed in a single array. Since
+ * choices are passed as array keys, only strings or integer choices are
+ * allowed. Choices may also be given as hierarchy of unlimited depth by
+ * creating nested arrays. The title of the sub-hierarchy can be stored in the
+ * array key pointing to the nested array.
+ *
+ * <code>
+ * $choiceList = new SimpleChoiceList(array(
+ * 'creditcard' => 'Credit card payment',
+ * 'cash' => 'Cash payment',
+ * ));
+ * </code>
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SimpleChoiceList extends ChoiceList
+{
+ /**
+ * Creates a new simple choice list.
+ *
+ * @param array $choices The array of choices with the choices as keys and
+ * the labels as values. Choices may also be given
+ * as hierarchy of unlimited depth by creating nested
+ * arrays. The title of the sub-hierarchy is stored
+ * in the array key pointing to the nested array.
+ * @param array $preferredChoices A flat array of choices that should be
+ * presented to the user with priority.
+ */
+ public function __construct(array $choices, array $preferredChoices = array())
+ {
+ // Flip preferred choices to speed up lookup
+ parent::__construct($choices, $choices, array_flip($preferredChoices));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChoicesForValues(array $values)
+ {
+ $values = $this->fixValues($values);
+
+ // The values are identical to the choices, so we can just return them
+ // to improve performance a little bit
+ return $this->fixChoices(array_intersect($values, $this->getValues()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValuesForChoices(array $choices)
+ {
+ $choices = $this->fixChoices($choices);
+
+ // The choices are identical to the values, so we can just return them
+ // to improve performance a little bit
+ return $this->fixValues(array_intersect($choices, $this->getValues()));
+ }
+
+ /**
+ * Recursively adds the given choices to the list.
+ *
+ * Takes care of splitting the single $choices array passed in the
+ * constructor into choices and labels.
+ *
+ * @param array $bucketForPreferred The bucket where to store the preferred
+ * view objects.
+ * @param array $bucketForRemaining The bucket where to store the
+ * non-preferred view objects.
+ * @param array|\Traversable $choices The list of choices.
+ * @param array $labels Ignored.
+ * @param array $preferredChoices The preferred choices.
+ */
+ protected function addChoices(array &$bucketForPreferred, array &$bucketForRemaining, $choices, array $labels, array $preferredChoices)
+ {
+ // Add choices to the nested buckets
+ foreach ($choices as $choice => $label) {
+ if (is_array($label)) {
+ // Don't do the work if the array is empty
+ if (count($label) > 0) {
+ $this->addChoiceGroup(
+ $choice,
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $label,
+ $label,
+ $preferredChoices
+ );
+ }
+ } else {
+ $this->addChoice(
+ $bucketForPreferred,
+ $bucketForRemaining,
+ $choice,
+ $label,
+ $preferredChoices
+ );
+ }
+ }
+ }
+
+ /**
+ * Returns whether the given choice should be preferred judging by the
+ * given array of preferred choices.
+ *
+ * Optimized for performance by treating the preferred choices as array
+ * where choices are stored in the keys.
+ *
+ * @param mixed $choice The choice to test.
+ * @param array $preferredChoices An array of preferred choices.
+ *
+ * @return Boolean Whether the choice is preferred.
+ */
+ protected function isPreferred($choice, array $preferredChoices)
+ {
+ // Optimize performance over the default implementation
+ return isset($preferredChoices[$choice]);
+ }
+
+ /**
+ * Converts the choice to a valid PHP array key.
+ *
+ * @param mixed $choice The choice.
+ *
+ * @return string|integer A valid PHP array key.
+ */
+ protected function fixChoice($choice)
+ {
+ return $this->fixIndex($choice);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function fixChoices(array $choices)
+ {
+ return $this->fixIndices($choices);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function createValue($choice)
+ {
+ // Choices are guaranteed to be unique and scalar, so we can simply
+ // convert them to strings
+ return (string) $choice;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core;
+
+use Symfony\Component\Form\AbstractExtension;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+
+/**
+ * Represents the main form extension, which loads the core functionality.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CoreExtension extends AbstractExtension
+{
+ protected function loadTypes()
+ {
+ return array(
+ new Type\FormType(PropertyAccess::getPropertyAccessor()),
+ new Type\BirthdayType(),
+ new Type\CheckboxType(),
+ new Type\ChoiceType(),
+ new Type\CollectionType(),
+ new Type\CountryType(),
+ new Type\DateType(),
+ new Type\DateTimeType(),
+ new Type\EmailType(),
+ new Type\HiddenType(),
+ new Type\IntegerType(),
+ new Type\LanguageType(),
+ new Type\LocaleType(),
+ new Type\MoneyType(),
+ new Type\NumberType(),
+ new Type\PasswordType(),
+ new Type\PercentType(),
+ new Type\RadioType(),
+ new Type\RepeatedType(),
+ new Type\SearchType(),
+ new Type\TextareaType(),
+ new Type\TextType(),
+ new Type\TimeType(),
+ new Type\TimezoneType(),
+ new Type\UrlType(),
+ new Type\FileType(),
+ new Type\ButtonType(),
+ new Type\SubmitType(),
+ new Type\ResetType(),
+ new Type\CurrencyType(),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataMapper;
+
+use Symfony\Component\Form\DataMapperInterface;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+
+/**
+ * A data mapper using property paths to read/write data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPathMapper implements DataMapperInterface
+{
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
+ /**
+ * Creates a new property path mapper.
+ *
+ * @param PropertyAccessorInterface $propertyAccessor
+ */
+ public function __construct(PropertyAccessorInterface $propertyAccessor = null)
+ {
+ $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mapDataToForms($data, $forms)
+ {
+ if (null === $data || array() === $data) {
+ return;
+ }
+
+ if (!is_array($data) && !is_object($data)) {
+ throw new UnexpectedTypeException($data, 'object, array or empty');
+ }
+
+ foreach ($forms as $form) {
+ $propertyPath = $form->getPropertyPath();
+ $config = $form->getConfig();
+
+ if (null !== $propertyPath && $config->getMapped()) {
+ $form->setData($this->propertyAccessor->getValue($data, $propertyPath));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mapFormsToData($forms, &$data)
+ {
+ if (null === $data) {
+ return;
+ }
+
+ if (!is_array($data) && !is_object($data)) {
+ throw new UnexpectedTypeException($data, 'object, array or empty');
+ }
+
+ foreach ($forms as $form) {
+ $propertyPath = $form->getPropertyPath();
+ $config = $form->getConfig();
+
+ // Write-back is disabled if the form is not synchronized (transformation failed)
+ // and if the form is disabled (modification not allowed)
+ if (null !== $propertyPath && $config->getMapped() && $form->isSynchronized() && !$form->isDisabled()) {
+ // If the data is identical to the value in $data, we are
+ // dealing with a reference
+ if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) {
+ $this->propertyAccessor->setValue($data, $propertyPath, $form->getData());
+ }
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ArrayToPartsTransformer implements DataTransformerInterface
+{
+ private $partMapping;
+
+ public function __construct(array $partMapping)
+ {
+ $this->partMapping = $partMapping;
+ }
+
+ public function transform($array)
+ {
+ if (null === $array) {
+ $array = array();
+ }
+
+ if (!is_array($array) ) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ $result = array();
+
+ foreach ($this->partMapping as $partKey => $originalKeys) {
+ if (empty($array)) {
+ $result[$partKey] = null;
+ } else {
+ $result[$partKey] = array_intersect_key($array, array_flip($originalKeys));
+ }
+ }
+
+ return $result;
+ }
+
+ public function reverseTransform($array)
+ {
+ if (!is_array($array) ) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ $result = array();
+ $emptyKeys = array();
+
+ foreach ($this->partMapping as $partKey => $originalKeys) {
+ if (!empty($array[$partKey])) {
+ foreach ($originalKeys as $originalKey) {
+ if (isset($array[$partKey][$originalKey])) {
+ $result[$originalKey] = $array[$partKey][$originalKey];
+ }
+ }
+ } else {
+ $emptyKeys[] = $partKey;
+ }
+ }
+
+ if (count($emptyKeys) > 0) {
+ if (count($emptyKeys) === count($this->partMapping)) {
+ // All parts empty
+ return null;
+ }
+
+ throw new TransformationFailedException(
+ sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
+ ));
+ }
+
+ return $result;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+abstract class BaseDateTimeTransformer implements DataTransformerInterface
+{
+ protected static $formats = array(
+ \IntlDateFormatter::NONE,
+ \IntlDateFormatter::FULL,
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::MEDIUM,
+ \IntlDateFormatter::SHORT,
+ );
+
+ protected $inputTimezone;
+
+ protected $outputTimezone;
+
+ /**
+ * Constructor.
+ *
+ * @param string $inputTimezone The name of the input timezone
+ * @param string $outputTimezone The name of the output timezone
+ *
+ * @throws UnexpectedTypeException if a timezone is not a string
+ */
+ public function __construct($inputTimezone = null, $outputTimezone = null)
+ {
+ if (!is_string($inputTimezone) && null !== $inputTimezone) {
+ throw new UnexpectedTypeException($inputTimezone, 'string');
+ }
+
+ if (!is_string($outputTimezone) && null !== $outputTimezone) {
+ throw new UnexpectedTypeException($outputTimezone, 'string');
+ }
+
+ $this->inputTimezone = $inputTimezone ?: date_default_timezone_get();
+ $this->outputTimezone = $outputTimezone ?: date_default_timezone_get();
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between a Boolean and a string.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class BooleanToStringTransformer implements DataTransformerInterface
+{
+ /**
+ * The value emitted upon transform if the input is true
+ * @var string
+ */
+ private $trueValue;
+
+ /**
+ * Sets the value emitted upon transform if the input is true.
+ *
+ * @param string $trueValue
+ */
+ public function __construct($trueValue)
+ {
+ $this->trueValue = $trueValue;
+ }
+
+ /**
+ * Transforms a Boolean into a string.
+ *
+ * @param Boolean $value Boolean value.
+ *
+ * @return string String value.
+ *
+ * @throws TransformationFailedException If the given value is not a Boolean.
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!is_bool($value)) {
+ throw new TransformationFailedException('Expected a Boolean.');
+ }
+
+ return true === $value ? $this->trueValue : null;
+ }
+
+ /**
+ * Transforms a string into a Boolean.
+ *
+ * @param string $value String value.
+ *
+ * @return Boolean Boolean value.
+ *
+ * @throws TransformationFailedException If the given value is not a string.
+ */
+ public function reverseTransform($value)
+ {
+ if (null === $value) {
+ return false;
+ }
+
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ return true;
+ }
+
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoiceToBooleanArrayTransformer implements DataTransformerInterface
+{
+ private $choiceList;
+
+ private $placeholderPresent;
+
+ /**
+ * Constructor.
+ *
+ * @param ChoiceListInterface $choiceList
+ * @param Boolean $placeholderPresent
+ */
+ public function __construct(ChoiceListInterface $choiceList, $placeholderPresent)
+ {
+ $this->choiceList = $choiceList;
+ $this->placeholderPresent = $placeholderPresent;
+ }
+
+ /**
+ * Transforms a single choice to a format appropriate for the nested
+ * checkboxes/radio buttons.
+ *
+ * The result is an array with the options as keys and true/false as values,
+ * depending on whether a given option is selected. If this field is rendered
+ * as select tag, the value is not modified.
+ *
+ * @param mixed $choice An array if "multiple" is set to true, a scalar
+ * value otherwise.
+ *
+ * @return mixed An array
+ *
+ * @throws TransformationFailedException If the given value is not scalar or
+ * if the choices can not be retrieved.
+ */
+ public function transform($choice)
+ {
+ try {
+ $values = $this->choiceList->getValues();
+ } catch (\Exception $e) {
+ throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
+ }
+
+ $index = current($this->choiceList->getIndicesForChoices(array($choice)));
+
+ foreach ($values as $i => $value) {
+ $values[$i] = $i === $index;
+ }
+
+ if ($this->placeholderPresent) {
+ $values['placeholder'] = false === $index;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Transforms a checkbox/radio button array to a single choice.
+ *
+ * The input value is an array with the choices as keys and true/false as
+ * values, depending on whether a given choice is selected. The output
+ * is the selected choice.
+ *
+ * @param array $values An array of values
+ *
+ * @return mixed A scalar value
+ *
+ * @throws TransformationFailedException If the given value is not an array,
+ * if the recuperation of the choices
+ * fails or if some choice can't be
+ * found.
+ */
+ public function reverseTransform($values)
+ {
+ if (!is_array($values)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ try {
+ $choices = $this->choiceList->getChoices();
+ } catch (\Exception $e) {
+ throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
+ }
+
+ foreach ($values as $i => $selected) {
+ if ($selected) {
+ if (isset($choices[$i])) {
+ return $choices[$i] === '' ? null : $choices[$i];
+ } elseif ($this->placeholderPresent && 'placeholder' === $i) {
+ return null;
+ } else {
+ throw new TransformationFailedException(sprintf('The choice "%s" does not exist', $i));
+ }
+ }
+ }
+
+ return null;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoiceToValueTransformer implements DataTransformerInterface
+{
+ private $choiceList;
+
+ /**
+ * Constructor.
+ *
+ * @param ChoiceListInterface $choiceList
+ */
+ public function __construct(ChoiceListInterface $choiceList)
+ {
+ $this->choiceList = $choiceList;
+ }
+
+ public function transform($choice)
+ {
+ return (string) current($this->choiceList->getValuesForChoices(array($choice)));
+ }
+
+ public function reverseTransform($value)
+ {
+ if (null !== $value && !is_scalar($value)) {
+ throw new TransformationFailedException('Expected a scalar.');
+ }
+
+ // These are now valid ChoiceList values, so we can return null
+ // right away
+ if ('' === $value || null === $value) {
+ return null;
+ }
+
+ $choices = $this->choiceList->getChoicesForValues(array($value));
+
+ if (1 !== count($choices)) {
+ throw new TransformationFailedException(sprintf('The choice "%s" does not exist or is not unique', $value));
+ }
+
+ $choice = current($choices);
+
+ return '' === $choice ? null : $choice;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoicesToBooleanArrayTransformer implements DataTransformerInterface
+{
+ private $choiceList;
+
+ public function __construct(ChoiceListInterface $choiceList)
+ {
+ $this->choiceList = $choiceList;
+ }
+
+ /**
+ * Transforms an array of choices to a format appropriate for the nested
+ * checkboxes/radio buttons.
+ *
+ * The result is an array with the options as keys and true/false as values,
+ * depending on whether a given option is selected. If this field is rendered
+ * as select tag, the value is not modified.
+ *
+ * @param mixed $array An array
+ *
+ * @return mixed An array
+ *
+ * @throws TransformationFailedException If the given value is not an array
+ * or if the choices can not be retrieved.
+ */
+ public function transform($array)
+ {
+ if (null === $array) {
+ return array();
+ }
+
+ if (!is_array($array)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ try {
+ $values = $this->choiceList->getValues();
+ } catch (\Exception $e) {
+ throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
+ }
+
+ $indexMap = array_flip($this->choiceList->getIndicesForChoices($array));
+
+ foreach ($values as $i => $value) {
+ $values[$i] = isset($indexMap[$i]);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Transforms a checkbox/radio button array to an array of choices.
+ *
+ * The input value is an array with the choices as keys and true/false as
+ * values, depending on whether a given choice is selected. The output
+ * is an array with the selected choices.
+ *
+ * @param mixed $values An array
+ *
+ * @return mixed An array
+ *
+ * @throws TransformationFailedException If the given value is not an array,
+ * if the recuperation of the choices
+ * fails or if some choice can't be
+ * found.
+ */
+ public function reverseTransform($values)
+ {
+ if (!is_array($values)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ try {
+ $choices = $this->choiceList->getChoices();
+ } catch (\Exception $e) {
+ throw new TransformationFailedException('Can not get the choice list', $e->getCode(), $e);
+ }
+
+ $result = array();
+ $unknown = array();
+
+ foreach ($values as $i => $selected) {
+ if ($selected) {
+ if (isset($choices[$i])) {
+ $result[] = $choices[$i];
+ } else {
+ $unknown[] = $i;
+ }
+ }
+ }
+
+ if (count($unknown) > 0) {
+ throw new TransformationFailedException(sprintf('The choices "%s" were not found', implode('", "', $unknown)));
+ }
+
+ return $result;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoicesToValuesTransformer implements DataTransformerInterface
+{
+ private $choiceList;
+
+ /**
+ * Constructor.
+ *
+ * @param ChoiceListInterface $choiceList
+ */
+ public function __construct(ChoiceListInterface $choiceList)
+ {
+ $this->choiceList = $choiceList;
+ }
+
+ /**
+ * @param array $array
+ *
+ * @return array
+ *
+ * @throws TransformationFailedException If the given value is not an array.
+ */
+ public function transform($array)
+ {
+ if (null === $array) {
+ return array();
+ }
+
+ if (!is_array($array)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ return $this->choiceList->getValuesForChoices($array);
+ }
+
+ /**
+ * @param array $array
+ *
+ * @return array
+ *
+ * @throws TransformationFailedException If the given value is not an array
+ * or if no matching choice could be
+ * found for some given value.
+ */
+ public function reverseTransform($array)
+ {
+ if (null === $array) {
+ return array();
+ }
+
+ if (!is_array($array)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ $choices = $this->choiceList->getChoicesForValues($array);
+
+ if (count($choices) !== count($array)) {
+ throw new TransformationFailedException('Could not find all matching choices for the given values');
+ }
+
+ return $choices;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Passes a value through multiple value transformers
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class DataTransformerChain implements DataTransformerInterface
+{
+ /**
+ * The value transformers
+ * @var DataTransformerInterface[]
+ */
+ protected $transformers;
+
+ /**
+ * Uses the given value transformers to transform values
+ *
+ * @param array $transformers
+ */
+ public function __construct(array $transformers)
+ {
+ $this->transformers = $transformers;
+ }
+
+ /**
+ * Passes the value through the transform() method of all nested transformers
+ *
+ * The transformers receive the value in the same order as they were passed
+ * to the constructor. Each transformer receives the result of the previous
+ * transformer as input. The output of the last transformer is returned
+ * by this method.
+ *
+ * @param mixed $value The original value
+ *
+ * @return mixed The transformed value
+ *
+ * @throws TransformationFailedException
+ */
+ public function transform($value)
+ {
+ foreach ($this->transformers as $transformer) {
+ $value = $transformer->transform($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Passes the value through the reverseTransform() method of all nested
+ * transformers
+ *
+ * The transformers receive the value in the reverse order as they were passed
+ * to the constructor. Each transformer receives the result of the previous
+ * transformer as input. The output of the last transformer is returned
+ * by this method.
+ *
+ * @param mixed $value The transformed value
+ *
+ * @return mixed The reverse-transformed value
+ *
+ * @throws TransformationFailedException
+ */
+ public function reverseTransform($value)
+ {
+ for ($i = count($this->transformers) - 1; $i >= 0; --$i) {
+ $value = $this->transformers[$i]->reverseTransform($value);
+ }
+
+ return $value;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * Transforms between a normalized time and a localized time string/array.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class DateTimeToArrayTransformer extends BaseDateTimeTransformer
+{
+ private $pad;
+
+ private $fields;
+
+ /**
+ * Constructor.
+ *
+ * @param string $inputTimezone The input timezone
+ * @param string $outputTimezone The output timezone
+ * @param array $fields The date fields
+ * @param Boolean $pad Whether to use padding
+ *
+ * @throws UnexpectedTypeException if a timezone is not a string
+ */
+ public function __construct($inputTimezone = null, $outputTimezone = null, array $fields = null, $pad = false)
+ {
+ parent::__construct($inputTimezone, $outputTimezone);
+
+ if (null === $fields) {
+ $fields = array('year', 'month', 'day', 'hour', 'minute', 'second');
+ }
+
+ $this->fields = $fields;
+ $this->pad = (Boolean) $pad;
+ }
+
+ /**
+ * Transforms a normalized date into a localized date.
+ *
+ * @param \DateTime $dateTime Normalized date.
+ *
+ * @return array Localized date.
+ *
+ * @throws TransformationFailedException If the given value is not an
+ * instance of \DateTime or if the
+ * output timezone is not supported.
+ */
+ public function transform($dateTime)
+ {
+ if (null === $dateTime) {
+ return array_intersect_key(array(
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ 'hour' => '',
+ 'minute' => '',
+ 'second' => '',
+ ), array_flip($this->fields));
+ }
+
+ if (!$dateTime instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ $dateTime = clone $dateTime;
+ if ($this->inputTimezone !== $this->outputTimezone) {
+ try {
+ $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ $result = array_intersect_key(array(
+ 'year' => $dateTime->format('Y'),
+ 'month' => $dateTime->format('m'),
+ 'day' => $dateTime->format('d'),
+ 'hour' => $dateTime->format('H'),
+ 'minute' => $dateTime->format('i'),
+ 'second' => $dateTime->format('s'),
+ ), array_flip($this->fields));
+
+ if (!$this->pad) {
+ foreach ($result as &$entry) {
+ // remove leading zeros
+ $entry = (string) (int) $entry;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Transforms a localized date into a normalized date.
+ *
+ * @param array $value Localized date
+ *
+ * @return \DateTime Normalized date
+ *
+ * @throws TransformationFailedException If the given value is not an array,
+ * if the value could not be transformed
+ * or if the input timezone is not
+ * supported.
+ */
+ public function reverseTransform($value)
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!is_array($value)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ if ('' === implode('', $value)) {
+ return null;
+ }
+
+ $emptyFields = array();
+
+ foreach ($this->fields as $field) {
+ if (!isset($value[$field])) {
+ $emptyFields[] = $field;
+ }
+ }
+
+ if (count($emptyFields) > 0) {
+ throw new TransformationFailedException(
+ sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)
+ ));
+ }
+
+ if (isset($value['month']) && !ctype_digit($value['month']) && !is_int($value['month'])) {
+ throw new TransformationFailedException('This month is invalid');
+ }
+
+ if (isset($value['day']) && !ctype_digit($value['day']) && !is_int($value['day'])) {
+ throw new TransformationFailedException('This day is invalid');
+ }
+
+ if (isset($value['year']) && !ctype_digit($value['year']) && !is_int($value['year'])) {
+ throw new TransformationFailedException('This year is invalid');
+ }
+
+ if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) {
+ throw new TransformationFailedException('This is an invalid date');
+ }
+
+ try {
+ $dateTime = new \DateTime(sprintf(
+ '%s-%s-%s %s:%s:%s %s',
+ empty($value['year']) ? '1970' : $value['year'],
+ empty($value['month']) ? '1' : $value['month'],
+ empty($value['day']) ? '1' : $value['day'],
+ empty($value['hour']) ? '0' : $value['hour'],
+ empty($value['minute']) ? '0' : $value['minute'],
+ empty($value['second']) ? '0' : $value['second'],
+ $this->outputTimezone
+ ));
+
+ if ($this->inputTimezone !== $this->outputTimezone) {
+ $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
+ }
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return $dateTime;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * Transforms between a normalized time and a localized time string
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
+{
+ private $dateFormat;
+ private $timeFormat;
+ private $pattern;
+ private $calendar;
+
+ /**
+ * Constructor.
+ *
+ * @see BaseDateTimeTransformer::formats for available format options
+ *
+ * @param string $inputTimezone The name of the input timezone
+ * @param string $outputTimezone The name of the output timezone
+ * @param integer $dateFormat The date format
+ * @param integer $timeFormat The time format
+ * @param integer $calendar One of the \IntlDateFormatter calendar constants
+ * @param string $pattern A pattern to pass to \IntlDateFormatter
+ *
+ * @throws UnexpectedTypeException If a format is not supported or if a timezone is not a string
+ */
+ public function __construct($inputTimezone = null, $outputTimezone = null, $dateFormat = null, $timeFormat = null, $calendar = \IntlDateFormatter::GREGORIAN, $pattern = null)
+ {
+ parent::__construct($inputTimezone, $outputTimezone);
+
+ if (null === $dateFormat) {
+ $dateFormat = \IntlDateFormatter::MEDIUM;
+ }
+
+ if (null === $timeFormat) {
+ $timeFormat = \IntlDateFormatter::SHORT;
+ }
+
+ if (!in_array($dateFormat, self::$formats, true)) {
+ throw new UnexpectedTypeException($dateFormat, implode('", "', self::$formats));
+ }
+
+ if (!in_array($timeFormat, self::$formats, true)) {
+ throw new UnexpectedTypeException($timeFormat, implode('", "', self::$formats));
+ }
+
+ $this->dateFormat = $dateFormat;
+ $this->timeFormat = $timeFormat;
+ $this->calendar = $calendar;
+ $this->pattern = $pattern;
+ }
+
+ /**
+ * Transforms a normalized date into a localized date string/array.
+ *
+ * @param \DateTime $dateTime Normalized date.
+ *
+ * @return string|array Localized date string/array.
+ *
+ * @throws TransformationFailedException If the given value is not an instance
+ * of \DateTime or if the date could not
+ * be transformed.
+ */
+ public function transform($dateTime)
+ {
+ if (null === $dateTime) {
+ return '';
+ }
+
+ if (!$dateTime instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ // convert time to UTC before passing it to the formatter
+ $dateTime = clone $dateTime;
+ if ('UTC' !== $this->inputTimezone) {
+ $dateTime->setTimezone(new \DateTimeZone('UTC'));
+ }
+
+ $value = $this->getIntlDateFormatter()->format((int) $dateTime->format('U'));
+
+ if (intl_get_error_code() != 0) {
+ throw new TransformationFailedException(intl_get_error_message());
+ }
+
+ return $value;
+ }
+
+ /**
+ * Transforms a localized date string/array into a normalized date.
+ *
+ * @param string|array $value Localized date string/array
+ *
+ * @return \DateTime Normalized date
+ *
+ * @throws TransformationFailedException if the given value is not a string,
+ * if the date could not be parsed or
+ * if the input timezone is not supported
+ */
+ public function reverseTransform($value)
+ {
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ if ('' === $value) {
+ return null;
+ }
+
+ $timestamp = $this->getIntlDateFormatter()->parse($value);
+
+ if (intl_get_error_code() != 0) {
+ throw new TransformationFailedException(intl_get_error_message());
+ }
+
+ try {
+ // read timestamp into DateTime object - the formatter delivers in UTC
+ $dateTime = new \DateTime(sprintf('@%s UTC', $timestamp));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ if ('UTC' !== $this->inputTimezone) {
+ try {
+ $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ return $dateTime;
+ }
+
+ /**
+ * Returns a preconfigured IntlDateFormatter instance
+ *
+ * @return \IntlDateFormatter
+ */
+ protected function getIntlDateFormatter()
+ {
+ $dateFormat = $this->dateFormat;
+ $timeFormat = $this->timeFormat;
+ $timezone = $this->outputTimezone;
+ $calendar = $this->calendar;
+ $pattern = $this->pattern;
+
+ $intlDateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern);
+ $intlDateFormatter->setLenient(false);
+
+ return $intlDateFormatter;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class DateTimeToRfc3339Transformer extends BaseDateTimeTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function transform($dateTime)
+ {
+ if (null === $dateTime) {
+ return '';
+ }
+
+ if (!$dateTime instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ if ($this->inputTimezone !== $this->outputTimezone) {
+ $dateTime = clone $dateTime;
+ $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
+ }
+
+ return preg_replace('/\+00:00$/', 'Z', $dateTime->format('c'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function reverseTransform($rfc3339)
+ {
+ if (!is_string($rfc3339)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ if ('' === $rfc3339) {
+ return null;
+ }
+
+ try {
+ $dateTime = new \DateTime($rfc3339);
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ if ($this->outputTimezone !== $this->inputTimezone) {
+ try {
+ $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $rfc3339, $matches)) {
+ if (!checkdate($matches[2], $matches[3], $matches[1])) {
+ throw new TransformationFailedException(sprintf(
+ 'The date "%s-%s-%s" is not a valid date.',
+ $matches[1],
+ $matches[2],
+ $matches[3]
+ ));
+ }
+ }
+
+ return $dateTime;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * Transforms between a date string and a DateTime object
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class DateTimeToStringTransformer extends BaseDateTimeTransformer
+{
+ /**
+ * Format used for generating strings
+ * @var string
+ */
+ private $generateFormat;
+
+ /**
+ * Format used for parsing strings
+ *
+ * Different than the {@link $generateFormat} because formats for parsing
+ * support additional characters in PHP that are not supported for
+ * generating strings.
+ *
+ * @var string
+ */
+ private $parseFormat;
+
+ /**
+ * Whether to parse by appending a pipe "|" to the parse format.
+ *
+ * This only works as of PHP 5.3.7.
+ *
+ * @var Boolean
+ */
+ private $parseUsingPipe;
+
+ /**
+ * Transforms a \DateTime instance to a string
+ *
+ * @see \DateTime::format() for supported formats
+ *
+ * @param string $inputTimezone The name of the input timezone
+ * @param string $outputTimezone The name of the output timezone
+ * @param string $format The date format
+ * @param Boolean $parseUsingPipe Whether to parse by appending a pipe "|" to the parse format
+ *
+ * @throws UnexpectedTypeException if a timezone is not a string
+ */
+ public function __construct($inputTimezone = null, $outputTimezone = null, $format = 'Y-m-d H:i:s', $parseUsingPipe = null)
+ {
+ parent::__construct($inputTimezone, $outputTimezone);
+
+ $this->generateFormat = $this->parseFormat = $format;
+
+ // The pipe in the parser pattern only works as of PHP 5.3.7
+ // See http://bugs.php.net/54316
+ $this->parseUsingPipe = null === $parseUsingPipe
+ ? version_compare(phpversion(), '5.3.7', '>=')
+ : $parseUsingPipe;
+
+ // See http://php.net/manual/en/datetime.createfromformat.php
+ // The character "|" in the format makes sure that the parts of a date
+ // that are *not* specified in the format are reset to the corresponding
+ // values from 1970-01-01 00:00:00 instead of the current time.
+ // Without "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 12:32:47",
+ // where the time corresponds to the current server time.
+ // With "|" and "Y-m-d", "2010-02-03" becomes "2010-02-03 00:00:00",
+ // which is at least deterministic and thus used here.
+ if ($this->parseUsingPipe && false === strpos($this->parseFormat, '|')) {
+ $this->parseFormat .= '|';
+ }
+ }
+
+ /**
+ * Transforms a DateTime object into a date string with the configured format
+ * and timezone
+ *
+ * @param \DateTime $value A DateTime object
+ *
+ * @return string A value as produced by PHP's date() function
+ *
+ * @throws TransformationFailedException If the given value is not a \DateTime
+ * instance or if the output timezone
+ * is not supported.
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return '';
+ }
+
+ if (!$value instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ $value = clone $value;
+ try {
+ $value->setTimezone(new \DateTimeZone($this->outputTimezone));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return $value->format($this->generateFormat);
+ }
+
+ /**
+ * Transforms a date string in the configured timezone into a DateTime object.
+ *
+ * @param string $value A value as produced by PHP's date() function
+ *
+ * @return \DateTime An instance of \DateTime
+ *
+ * @throws TransformationFailedException If the given value is not a string,
+ * if the date could not be parsed or
+ * if the input timezone is not supported.
+ */
+ public function reverseTransform($value)
+ {
+ if (empty($value)) {
+ return null;
+ }
+
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ try {
+ $outputTz = new \DateTimeZone($this->outputTimezone);
+ $dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
+
+ $lastErrors = \DateTime::getLastErrors();
+
+ if (0 < $lastErrors['warning_count'] || 0 < $lastErrors['error_count']) {
+ throw new TransformationFailedException(
+ implode(', ', array_merge(
+ array_values($lastErrors['warnings']),
+ array_values($lastErrors['errors'])
+ ))
+ );
+ }
+
+ // On PHP versions < 5.3.7 we need to emulate the pipe operator
+ // and reset parts not given in the format to their equivalent
+ // of the UNIX base timestamp.
+ if (!$this->parseUsingPipe) {
+ list($year, $month, $day, $hour, $minute, $second) = explode('-', $dateTime->format('Y-m-d-H-i-s'));
+
+ // Check which of the date parts are present in the pattern
+ preg_match(
+ '/(' .
+ '(?P<day>[djDl])|' .
+ '(?P<month>[FMmn])|' .
+ '(?P<year>[Yy])|' .
+ '(?P<hour>[ghGH])|' .
+ '(?P<minute>i)|' .
+ '(?P<second>s)|' .
+ '(?P<dayofyear>z)|' .
+ '(?P<timestamp>U)|' .
+ '[^djDlFMmnYyghGHiszU]' .
+ ')*/',
+ $this->parseFormat,
+ $matches
+ );
+
+ // preg_match() does not guarantee to set all indices, so
+ // set them unless given
+ $matches = array_merge(array(
+ 'day' => false,
+ 'month' => false,
+ 'year' => false,
+ 'hour' => false,
+ 'minute' => false,
+ 'second' => false,
+ 'dayofyear' => false,
+ 'timestamp' => false,
+ ), $matches);
+
+ // Reset all parts that don't exist in the format to the
+ // corresponding part of the UNIX base timestamp
+ if (!$matches['timestamp']) {
+ if (!$matches['dayofyear']) {
+ if (!$matches['day']) {
+ $day = 1;
+ }
+ if (!$matches['month']) {
+ $month = 1;
+ }
+ }
+ if (!$matches['year']) {
+ $year = 1970;
+ }
+ if (!$matches['hour']) {
+ $hour = 0;
+ }
+ if (!$matches['minute']) {
+ $minute = 0;
+ }
+ if (!$matches['second']) {
+ $second = 0;
+ }
+ $dateTime->setDate($year, $month, $day);
+ $dateTime->setTime($hour, $minute, $second);
+ }
+ }
+
+ if ($this->inputTimezone !== $this->outputTimezone) {
+ $dateTime->setTimeZone(new \DateTimeZone($this->inputTimezone));
+ }
+ } catch (TransformationFailedException $e) {
+ throw $e;
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return $dateTime;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between a timestamp and a DateTime object
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
+{
+ /**
+ * Transforms a DateTime object into a timestamp in the configured timezone.
+ *
+ * @param \DateTime $value A \DateTime object
+ *
+ * @return integer A timestamp
+ *
+ * @throws TransformationFailedException If the given value is not an instance
+ * of \DateTime or if the output
+ * timezone is not supported.
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!$value instanceof \DateTime) {
+ throw new TransformationFailedException('Expected a \DateTime.');
+ }
+
+ $value = clone $value;
+ try {
+ $value->setTimezone(new \DateTimeZone($this->outputTimezone));
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return (int) $value->format('U');
+ }
+
+ /**
+ * Transforms a timestamp in the configured timezone into a DateTime object
+ *
+ * @param string $value A timestamp
+ *
+ * @return \DateTime A \DateTime object
+ *
+ * @throws TransformationFailedException If the given value is not a timestamp
+ * or if the given timestamp is invalid.
+ */
+ public function reverseTransform($value)
+ {
+ if (null === $value) {
+ return null;
+ }
+
+ if (!is_numeric($value)) {
+ throw new TransformationFailedException('Expected a numeric.');
+ }
+
+ try {
+ $dateTime = new \DateTime();
+ $dateTime->setTimezone(new \DateTimeZone($this->outputTimezone));
+ $dateTime->setTimestamp($value);
+
+ if ($this->inputTimezone !== $this->outputTimezone) {
+ $dateTime->setTimezone(new \DateTimeZone($this->inputTimezone));
+ }
+ } catch (\Exception $e) {
+ throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
+ }
+
+ return $dateTime;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between an integer and a localized number with grouping
+ * (each thousand) and comma separators.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function reverseTransform($value)
+ {
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ if ('' === $value) {
+ return null;
+ }
+
+ if ('NaN' === $value) {
+ throw new TransformationFailedException('"NaN" is not a valid integer');
+ }
+
+ $formatter = $this->getNumberFormatter();
+ $value = $formatter->parse(
+ $value,
+ PHP_INT_SIZE == 8 ? $formatter::TYPE_INT64 : $formatter::TYPE_INT32
+ );
+
+ if (intl_is_failure($formatter->getErrorCode())) {
+ throw new TransformationFailedException($formatter->getErrorMessage());
+ }
+
+ return $value;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between a normalized format and a localized money string.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer
+{
+
+ private $divisor;
+
+ public function __construct($precision = null, $grouping = null, $roundingMode = null, $divisor = null)
+ {
+ if (null === $grouping) {
+ $grouping = true;
+ }
+
+ if (null === $precision) {
+ $precision = 2;
+ }
+
+ parent::__construct($precision, $grouping, $roundingMode);
+
+ if (null === $divisor) {
+ $divisor = 1;
+ }
+
+ $this->divisor = $divisor;
+ }
+
+ /**
+ * Transforms a normalized format into a localized money string.
+ *
+ * @param number $value Normalized number
+ *
+ * @return string Localized money string.
+ *
+ * @throws TransformationFailedException If the given value is not numeric or
+ * if the value can not be transformed.
+ */
+ public function transform($value)
+ {
+ if (null !== $value) {
+ if (!is_numeric($value)) {
+ throw new TransformationFailedException('Expected a numeric.');
+ }
+
+ $value /= $this->divisor;
+ }
+
+ return parent::transform($value);
+ }
+
+ /**
+ * Transforms a localized money string into a normalized format.
+ *
+ * @param string $value Localized money string
+ *
+ * @return number Normalized number
+ *
+ * @throws TransformationFailedException If the given value is not a string
+ * or if the value can not be transformed.
+ */
+ public function reverseTransform($value)
+ {
+ $value = parent::reverseTransform($value);
+
+ if (null !== $value) {
+ $value *= $this->divisor;
+ }
+
+ return $value;
+ }
+
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * Transforms between a number type and a localized number with grouping
+ * (each thousand) and comma separators.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class NumberToLocalizedStringTransformer implements DataTransformerInterface
+{
+ const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
+ const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
+ const ROUND_HALFDOWN = \NumberFormatter::ROUND_HALFDOWN;
+ const ROUND_HALFEVEN = \NumberFormatter::ROUND_HALFEVEN;
+ const ROUND_HALFUP = \NumberFormatter::ROUND_HALFUP;
+ const ROUND_UP = \NumberFormatter::ROUND_UP;
+ const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
+
+ protected $precision;
+
+ protected $grouping;
+
+ protected $roundingMode;
+
+ public function __construct($precision = null, $grouping = null, $roundingMode = null)
+ {
+ if (null === $grouping) {
+ $grouping = false;
+ }
+
+ if (null === $roundingMode) {
+ $roundingMode = self::ROUND_HALFUP;
+ }
+
+ $this->precision = $precision;
+ $this->grouping = $grouping;
+ $this->roundingMode = $roundingMode;
+ }
+
+ /**
+ * Transforms a number type into localized number.
+ *
+ * @param integer|float $value Number value.
+ *
+ * @return string Localized value.
+ *
+ * @throws TransformationFailedException If the given value is not numeric
+ * or if the value can not be transformed.
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return '';
+ }
+
+ if (!is_numeric($value)) {
+ throw new TransformationFailedException('Expected a numeric.');
+ }
+
+ $formatter = $this->getNumberFormatter();
+ $value = $formatter->format($value);
+
+ if (intl_is_failure($formatter->getErrorCode())) {
+ throw new TransformationFailedException($formatter->getErrorMessage());
+ }
+
+ // Convert fixed spaces to normal ones
+ $value = str_replace("\xc2\xa0", ' ', $value);
+
+ return $value;
+ }
+
+ /**
+ * Transforms a localized number into an integer or float
+ *
+ * @param string $value The localized value
+ *
+ * @return integer|float The numeric value
+ *
+ * @throws TransformationFailedException If the given value is not a string
+ * or if the value can not be transformed.
+ */
+ public function reverseTransform($value)
+ {
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ if ('' === $value) {
+ return null;
+ }
+
+ if ('NaN' === $value) {
+ throw new TransformationFailedException('"NaN" is not a valid number');
+ }
+
+ $position = 0;
+ $formatter = $this->getNumberFormatter();
+ $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
+ $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
+
+ if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
+ $value = str_replace('.', $decSep, $value);
+ }
+
+ if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
+ $value = str_replace(',', $decSep, $value);
+ }
+
+ $result = $formatter->parse($value, \NumberFormatter::TYPE_DOUBLE, $position);
+
+ if (intl_is_failure($formatter->getErrorCode())) {
+ throw new TransformationFailedException($formatter->getErrorMessage());
+ }
+
+ if ($result >= PHP_INT_MAX || $result <= -PHP_INT_MAX) {
+ throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like');
+ }
+
+ if (function_exists('mb_detect_encoding') && false !== $encoding = mb_detect_encoding($value)) {
+ $strlen = function ($string) use ($encoding) {
+ return mb_strlen($string, $encoding);
+ };
+ $substr = function ($string, $offset, $length) use ($encoding) {
+ return mb_substr($string, $offset, $length, $encoding);
+ };
+ } else {
+ $strlen = 'strlen';
+ $substr = 'substr';
+ }
+
+ $length = $strlen($value);
+
+ // After parsing, position holds the index of the character where the
+ // parsing stopped
+ if ($position < $length) {
+ // Check if there are unrecognized characters at the end of the
+ // number (excluding whitespace characters)
+ $remainder = trim($substr($value, $position, $length), " \t\n\r\0\x0b\xc2\xa0");
+
+ if ('' !== $remainder) {
+ throw new TransformationFailedException(
+ sprintf('The number contains unrecognized characters: "%s"', $remainder)
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a preconfigured \NumberFormatter instance
+ *
+ * @return \NumberFormatter
+ */
+ protected function getNumberFormatter()
+ {
+ $formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
+
+ if (null !== $this->precision) {
+ $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision);
+ $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
+ }
+
+ $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
+
+ return $formatter;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * Transforms between a normalized format (integer or float) and a percentage value.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Florian Eckerstorfer <florian@eckerstorfer.org>
+ */
+class PercentToLocalizedStringTransformer implements DataTransformerInterface
+{
+ const FRACTIONAL = 'fractional';
+ const INTEGER = 'integer';
+
+ protected static $types = array(
+ self::FRACTIONAL,
+ self::INTEGER,
+ );
+
+ private $type;
+
+ private $precision;
+
+ /**
+ * Constructor.
+ *
+ * @see self::$types for a list of supported types
+ *
+ * @param integer $precision The precision
+ * @param string $type One of the supported types
+ *
+ * @throws UnexpectedTypeException if the given value of type is unknown
+ */
+ public function __construct($precision = null, $type = null)
+ {
+ if (null === $precision) {
+ $precision = 0;
+ }
+
+ if (null === $type) {
+ $type = self::FRACTIONAL;
+ }
+
+ if (!in_array($type, self::$types, true)) {
+ throw new UnexpectedTypeException($type, implode('", "', self::$types));
+ }
+
+ $this->type = $type;
+ $this->precision = $precision;
+ }
+
+ /**
+ * Transforms between a normalized format (integer or float) into a percentage value.
+ *
+ * @param number $value Normalized value
+ *
+ * @return number Percentage value
+ *
+ * @throws TransformationFailedException If the given value is not numeric or
+ * if the value could not be transformed.
+ */
+ public function transform($value)
+ {
+ if (null === $value) {
+ return '';
+ }
+
+ if (!is_numeric($value)) {
+ throw new TransformationFailedException('Expected a numeric.');
+ }
+
+ if (self::FRACTIONAL == $this->type) {
+ $value *= 100;
+ }
+
+ $formatter = $this->getNumberFormatter();
+ $value = $formatter->format($value);
+
+ if (intl_is_failure($formatter->getErrorCode())) {
+ throw new TransformationFailedException($formatter->getErrorMessage());
+ }
+
+ // replace the UTF-8 non break spaces
+ return $value;
+ }
+
+ /**
+ * Transforms between a percentage value into a normalized format (integer or float).
+ *
+ * @param number $value Percentage value.
+ *
+ * @return number Normalized value.
+ *
+ * @throws TransformationFailedException If the given value is not a string or
+ * if the value could not be transformed.
+ */
+ public function reverseTransform($value)
+ {
+ if (!is_string($value)) {
+ throw new TransformationFailedException('Expected a string.');
+ }
+
+ if ('' === $value) {
+ return null;
+ }
+
+ $formatter = $this->getNumberFormatter();
+ // replace normal spaces so that the formatter can read them
+ $value = $formatter->parse(str_replace(' ', ' ', $value));
+
+ if (intl_is_failure($formatter->getErrorCode())) {
+ throw new TransformationFailedException($formatter->getErrorMessage());
+ }
+
+ if (self::FRACTIONAL == $this->type) {
+ $value /= 100;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns a preconfigured \NumberFormatter instance
+ *
+ * @return \NumberFormatter
+ */
+ protected function getNumberFormatter()
+ {
+ $formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
+
+ $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->precision);
+
+ return $formatter;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ValueToDuplicatesTransformer implements DataTransformerInterface
+{
+ private $keys;
+
+ public function __construct(array $keys)
+ {
+ $this->keys = $keys;
+ }
+
+ /**
+ * Duplicates the given value through the array.
+ *
+ * @param mixed $value The value
+ *
+ * @return array The array
+ */
+ public function transform($value)
+ {
+ $result = array();
+
+ foreach ($this->keys as $key) {
+ $result[$key] = $value;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Extracts the duplicated value from an array.
+ *
+ * @param array $array
+ *
+ * @return mixed The value
+ *
+ * @throws TransformationFailedException If the given value is not an array or
+ * if the given array can not be transformed.
+ */
+ public function reverseTransform($array)
+ {
+ if (!is_array($array)) {
+ throw new TransformationFailedException('Expected an array.');
+ }
+
+ $result = current($array);
+ $emptyKeys = array();
+
+ foreach ($this->keys as $key) {
+ if (!empty($array[$key])) {
+ if ($array[$key] !== $result) {
+ throw new TransformationFailedException(
+ 'All values in the array should be the same'
+ );
+ }
+ } else {
+ $emptyKeys[] = $key;
+ }
+ }
+
+ if (count($emptyKeys) > 0) {
+ if (count($emptyKeys) == count($this->keys)) {
+ // All keys empty
+ return null;
+ }
+
+ throw new TransformationFailedException(
+ sprintf('The keys "%s" should not be empty', implode('", "', $emptyKeys)
+ ));
+ }
+
+ return $result;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+
+/**
+ * Takes care of converting the input from a list of checkboxes to a correctly
+ * indexed array.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FixCheckboxInputListener implements EventSubscriberInterface
+{
+ private $choiceList;
+
+ /**
+ * Constructor.
+ *
+ * @param ChoiceListInterface $choiceList
+ */
+ public function __construct(ChoiceListInterface $choiceList)
+ {
+ $this->choiceList = $choiceList;
+ }
+
+ public function preSubmit(FormEvent $event)
+ {
+ $values = (array) $event->getData();
+ $indices = $this->choiceList->getIndicesForValues($values);
+
+ $event->setData(count($indices) > 0 ? array_combine($indices, $values) : array());
+ }
+
+ /**
+ * Alias of {@link preSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link preSubmit()} instead.
+ */
+ public function preBind(FormEvent $event)
+ {
+ $this->preSubmit($event);
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(FormEvents::PRE_SUBMIT => 'preSubmit');
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
+
+/**
+ * Takes care of converting the input from a single radio button
+ * to an array.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FixRadioInputListener implements EventSubscriberInterface
+{
+ private $choiceList;
+
+ private $placeholderPresent;
+
+ /**
+ * Constructor.
+ *
+ * @param ChoiceListInterface $choiceList
+ * @param Boolean $placeholderPresent
+ */
+ public function __construct(ChoiceListInterface $choiceList, $placeholderPresent)
+ {
+ $this->choiceList = $choiceList;
+ $this->placeholderPresent = $placeholderPresent;
+ }
+
+ public function preSubmit(FormEvent $event)
+ {
+ $value = $event->getData();
+ $index = current($this->choiceList->getIndicesForValues(array($value)));
+
+ $event->setData(false !== $index ? array($index => $value) : ($this->placeholderPresent ? array('placeholder' => '') : array())) ;
+ }
+
+ /**
+ * Alias of {@link preSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link preSubmit()} instead.
+ */
+ public function preBind(FormEvent $event)
+ {
+ $this->preSubmit($event);
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(FormEvents::PRE_SUBMIT => 'preSubmit');
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Adds a protocol to a URL if it doesn't already have one.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FixUrlProtocolListener implements EventSubscriberInterface
+{
+ private $defaultProtocol;
+
+ public function __construct($defaultProtocol = 'http')
+ {
+ $this->defaultProtocol = $defaultProtocol;
+ }
+
+ public function onSubmit(FormEvent $event)
+ {
+ $data = $event->getData();
+
+ if ($this->defaultProtocol && $data && !preg_match('~^\w+://~', $data)) {
+ $event->setData($this->defaultProtocol.'://'.$data);
+ }
+ }
+
+ /**
+ * Alias of {@link onSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link onSubmit()} instead.
+ */
+ public function onBind(FormEvent $event)
+ {
+ $this->onSubmit($event);
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(FormEvents::SUBMIT => 'onSubmit');
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class MergeCollectionListener implements EventSubscriberInterface
+{
+ /**
+ * Whether elements may be added to the collection
+ * @var Boolean
+ */
+ private $allowAdd;
+
+ /**
+ * Whether elements may be removed from the collection
+ * @var Boolean
+ */
+ private $allowDelete;
+
+ /**
+ * Creates a new listener.
+ *
+ * @param Boolean $allowAdd Whether values might be added to the
+ * collection.
+ * @param Boolean $allowDelete Whether values might be removed from the
+ * collection.
+ */
+ public function __construct($allowAdd = false, $allowDelete = false)
+ {
+ $this->allowAdd = $allowAdd;
+ $this->allowDelete = $allowDelete;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(
+ FormEvents::SUBMIT => 'onSubmit',
+ );
+ }
+
+ public function onSubmit(FormEvent $event)
+ {
+ $dataToMergeInto = $event->getForm()->getNormData();
+ $data = $event->getData();
+
+ if (null === $data) {
+ $data = array();
+ }
+
+ if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
+ throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
+ }
+
+ if (null !== $dataToMergeInto && !is_array($dataToMergeInto) && !($dataToMergeInto instanceof \Traversable && $dataToMergeInto instanceof \ArrayAccess)) {
+ throw new UnexpectedTypeException($dataToMergeInto, 'array or (\Traversable and \ArrayAccess)');
+ }
+
+ // If we are not allowed to change anything, return immediately
+ if ((!$this->allowAdd && !$this->allowDelete) || $data === $dataToMergeInto) {
+ $event->setData($dataToMergeInto);
+
+ return;
+ }
+
+ if (!$dataToMergeInto) {
+ // No original data was set. Set it if allowed
+ if ($this->allowAdd) {
+ $dataToMergeInto = $data;
+ }
+ } else {
+ // Calculate delta
+ $itemsToAdd = is_object($data) ? clone $data : $data;
+ $itemsToDelete = array();
+
+ foreach ($dataToMergeInto as $beforeKey => $beforeItem) {
+ foreach ($data as $afterKey => $afterItem) {
+ if ($afterItem === $beforeItem) {
+ // Item found, next original item
+ unset($itemsToAdd[$afterKey]);
+ continue 2;
+ }
+ }
+
+ // Item not found, remember for deletion
+ $itemsToDelete[] = $beforeKey;
+ }
+
+ // Remove deleted items before adding to free keys that are to be
+ // replaced
+ if ($this->allowDelete) {
+ foreach ($itemsToDelete as $key) {
+ unset($dataToMergeInto[$key]);
+ }
+ }
+
+ // Add remaining items
+ if ($this->allowAdd) {
+ foreach ($itemsToAdd as $key => $item) {
+ if (!isset($dataToMergeInto[$key])) {
+ $dataToMergeInto[$key] = $item;
+ } else {
+ $dataToMergeInto[] = $item;
+ }
+ }
+ }
+ }
+
+ $event->setData($dataToMergeInto);
+ }
+
+ /**
+ * Alias of {@link onSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link onSubmit()} instead.
+ */
+ public function onBind(FormEvent $event)
+ {
+ $this->onSubmit($event);
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Resize a collection form element based on the data sent from the client.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ResizeFormListener implements EventSubscriberInterface
+{
+ /**
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * @var array
+ */
+ protected $options;
+
+ /**
+ * Whether children could be added to the group
+ * @var Boolean
+ */
+ protected $allowAdd;
+
+ /**
+ * Whether children could be removed from the group
+ * @var Boolean
+ */
+ protected $allowDelete;
+
+ public function __construct($type, array $options = array(), $allowAdd = false, $allowDelete = false)
+ {
+ $this->type = $type;
+ $this->allowAdd = $allowAdd;
+ $this->allowDelete = $allowDelete;
+ $this->options = $options;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(
+ FormEvents::PRE_SET_DATA => 'preSetData',
+ FormEvents::PRE_SUBMIT => 'preSubmit',
+ // (MergeCollectionListener, MergeDoctrineCollectionListener)
+ FormEvents::SUBMIT => array('onSubmit', 50),
+ );
+ }
+
+ public function preSetData(FormEvent $event)
+ {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ if (null === $data) {
+ $data = array();
+ }
+
+ if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
+ throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
+ }
+
+ // First remove all rows
+ foreach ($form as $name => $child) {
+ $form->remove($name);
+ }
+
+ // Then add all rows again in the correct order
+ foreach ($data as $name => $value) {
+ $form->add($name, $this->type, array_replace(array(
+ 'property_path' => '['.$name.']',
+ ), $this->options));
+ }
+ }
+
+ public function preSubmit(FormEvent $event)
+ {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ if (null === $data || '' === $data) {
+ $data = array();
+ }
+
+ if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
+ throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
+ }
+
+ // Remove all empty rows
+ if ($this->allowDelete) {
+ foreach ($form as $name => $child) {
+ if (!isset($data[$name])) {
+ $form->remove($name);
+ }
+ }
+ }
+
+ // Add all additional rows
+ if ($this->allowAdd) {
+ foreach ($data as $name => $value) {
+ if (!$form->has($name)) {
+ $form->add($name, $this->type, array_replace(array(
+ 'property_path' => '['.$name.']',
+ ), $this->options));
+ }
+ }
+ }
+ }
+
+ public function onSubmit(FormEvent $event)
+ {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ if (null === $data) {
+ $data = array();
+ }
+
+ if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) {
+ throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)');
+ }
+
+ // The data mapper only adds, but does not remove items, so do this
+ // here
+ if ($this->allowDelete) {
+ foreach ($data as $name => $child) {
+ if (!$form->has($name)) {
+ unset($data[$name]);
+ }
+ }
+ }
+
+ $event->setData($data);
+ }
+
+ /**
+ * Alias of {@link preSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link preSubmit()} instead.
+ */
+ public function preBind(FormEvent $event)
+ {
+ $this->preSubmit($event);
+ }
+
+ /**
+ * Alias of {@link onSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link onSubmit()} instead.
+ */
+ public function onBind(FormEvent $event)
+ {
+ $this->onSubmit($event);
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Trims string data
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TrimListener implements EventSubscriberInterface
+{
+ public function preSubmit(FormEvent $event)
+ {
+ $data = $event->getData();
+
+ if (!is_string($data)) {
+ return;
+ }
+
+ if (null !== $result = @preg_replace('/^[\pZ\p{Cc}]+|[\pZ\p{Cc}]+$/u', '', $data)) {
+ $event->setData($result);
+ } else {
+ $event->setData(trim($data));
+ }
+ }
+
+ /**
+ * Alias of {@link preSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link preSubmit()} instead.
+ */
+ public function preBind(FormEvent $event)
+ {
+ $this->preSubmit($event);
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(FormEvents::PRE_SUBMIT => 'preSubmit');
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * Encapsulates common logic of {@link FormType} and {@link ButtonType}.
+ *
+ * This type does not appear in the form's type inheritance chain and as such
+ * cannot be extended (via {@link FormTypeExtension}s) nor themed.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class BaseType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->setDisabled($options['disabled']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $name = $form->getName();
+ $blockName = $options['block_name'] ?: $form->getName();
+ $translationDomain = $options['translation_domain'];
+
+ if ($view->parent) {
+ if ('' !== ($parentFullName = $view->parent->vars['full_name'])) {
+ $id = sprintf('%s_%s', $view->parent->vars['id'], $name);
+ $fullName = sprintf('%s[%s]', $parentFullName, $name);
+ $uniqueBlockPrefix = sprintf('%s_%s', $view->parent->vars['unique_block_prefix'], $blockName);
+ } else {
+ $id = $name;
+ $fullName = $name;
+ $uniqueBlockPrefix = '_'.$blockName;
+ }
+
+ if (!$translationDomain) {
+ $translationDomain = $view->parent->vars['translation_domain'];
+ }
+ } else {
+ $id = $name;
+ $fullName = $name;
+ $uniqueBlockPrefix = '_'.$blockName;
+
+ // Strip leading underscores and digits. These are allowed in
+ // form names, but not in HTML4 ID attributes.
+ // http://www.w3.org/TR/html401/struct/global.html#adef-id
+ $id = ltrim($id, '_0123456789');
+ }
+
+ $blockPrefixes = array();
+ for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) {
+ array_unshift($blockPrefixes, $type->getName());
+ }
+ $blockPrefixes[] = $uniqueBlockPrefix;
+
+ if (!$translationDomain) {
+ $translationDomain = 'messages';
+ }
+
+ $view->vars = array_replace($view->vars, array(
+ 'form' => $view,
+ 'id' => $id,
+ 'name' => $name,
+ 'full_name' => $fullName,
+ 'disabled' => $form->isDisabled(),
+ 'label' => $options['label'],
+ 'multipart' => false,
+ 'attr' => $options['attr'],
+ 'block_prefixes' => $blockPrefixes,
+ 'unique_block_prefix' => $uniqueBlockPrefix,
+ 'translation_domain' => $translationDomain,
+ // Using the block name here speeds up performance in collection
+ // forms, where each entry has the same full block name.
+ // Including the type is important too, because if rows of a
+ // collection form have different types (dynamically), they should
+ // be rendered differently.
+ // https://github.com/symfony/symfony/issues/5038
+ 'cache_key' => $uniqueBlockPrefix.'_'.$form->getConfig()->getType()->getName(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'block_name' => null,
+ 'disabled' => false,
+ 'label' => null,
+ 'attr' => array(),
+ 'translation_domain' => null,
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'attr' => 'array',
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class BirthdayType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'years' => range(date('Y') - 120, date('Y')),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'date';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'birthday';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\ButtonTypeInterface;
+
+/**
+ * A form button.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ButtonType extends BaseType implements ButtonTypeInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'button';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class CheckboxType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->addViewTransformer(new BooleanToStringTransformer($options['value']))
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars = array_replace($view->vars, array(
+ 'value' => $options['value'],
+ 'checked' => null !== $form->getViewData(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $emptyData = function (FormInterface $form, $clientData) {
+ return $clientData;
+ };
+
+ $resolver->setDefaults(array(
+ 'value' => '1',
+ 'empty_data' => $emptyData,
+ 'compound' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'checkbox';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
+use Symfony\Component\Form\Extension\Core\EventListener\FixCheckboxInputListener;
+use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToBooleanArrayTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToBooleanArrayTransformer;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class ChoiceType extends AbstractType
+{
+ /**
+ * Caches created choice lists.
+ * @var array
+ */
+ private $choiceListCache = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
+ throw new LogicException('Either the option "choices" or "choice_list" must be set.');
+ }
+
+ if ($options['expanded']) {
+ // Initialize all choices before doing the index check below.
+ // This helps in cases where index checks are optimized for non
+ // initialized choice lists. For example, when using an SQL driver,
+ // the index check would read in one SQL query and the initialization
+ // requires another SQL query. When the initialization is done first,
+ // one SQL query is sufficient.
+ $preferredViews = $options['choice_list']->getPreferredViews();
+ $remainingViews = $options['choice_list']->getRemainingViews();
+
+ // Check if the choices already contain the empty value
+ // Only add the empty value option if this is not the case
+ if (null !== $options['empty_value'] && 0 === count($options['choice_list']->getIndicesForValues(array('')))) {
+ $placeholderView = new ChoiceView(null, '', $options['empty_value']);
+
+ // "placeholder" is a reserved index
+ // see also ChoiceListInterface::getIndicesForChoices()
+ $this->addSubForms($builder, array('placeholder' => $placeholderView), $options);
+ }
+
+ $this->addSubForms($builder, $preferredViews, $options);
+ $this->addSubForms($builder, $remainingViews, $options);
+
+ if ($options['multiple']) {
+ $builder->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']));
+ $builder->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10);
+ } else {
+ $builder->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list'], $builder->has('placeholder')));
+ $builder->addEventSubscriber(new FixRadioInputListener($options['choice_list'], $builder->has('placeholder')), 10);
+ }
+ } else {
+ if ($options['multiple']) {
+ $builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
+ } else {
+ $builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
+ }
+ }
+
+ if ($options['multiple'] && $options['by_reference']) {
+ // Make sure the collection created during the client->norm
+ // transformation is merged back into the original collection
+ $builder->addEventSubscriber(new MergeCollectionListener(true, true));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars = array_replace($view->vars, array(
+ 'multiple' => $options['multiple'],
+ 'expanded' => $options['expanded'],
+ 'preferred_choices' => $options['choice_list']->getPreferredViews(),
+ 'choices' => $options['choice_list']->getRemainingViews(),
+ 'separator' => '-------------------',
+ 'empty_value' => null,
+ ));
+
+ // The decision, whether a choice is selected, is potentially done
+ // thousand of times during the rendering of a template. Provide a
+ // closure here that is optimized for the value of the form, to
+ // avoid making the type check inside the closure.
+ if ($options['multiple']) {
+ $view->vars['is_selected'] = function ($choice, array $values) {
+ return false !== array_search($choice, $values, true);
+ };
+ } else {
+ $view->vars['is_selected'] = function ($choice, $value) {
+ return $choice === $value;
+ };
+ }
+
+ // Check if the choices already contain the empty value
+ // Only add the empty value option if this is not the case
+ if (null !== $options['empty_value'] && 0 === count($options['choice_list']->getIndicesForValues(array('')))) {
+ $view->vars['empty_value'] = $options['empty_value'];
+ }
+
+ if ($options['multiple'] && !$options['expanded']) {
+ // Add "[]" to the name in case a select tag with multiple options is
+ // displayed. Otherwise only one of the selected options is sent in the
+ // POST request.
+ $view->vars['full_name'] = $view->vars['full_name'].'[]';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ if ($options['expanded']) {
+ // Radio buttons should have the same name as the parent
+ $childName = $view->vars['full_name'];
+
+ // Checkboxes should append "[]" to allow multiple selection
+ if ($options['multiple']) {
+ $childName .= '[]';
+ }
+
+ foreach ($view as $childView) {
+ $childView->vars['full_name'] = $childName;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $choiceListCache =& $this->choiceListCache;
+
+ $choiceList = function (Options $options) use (&$choiceListCache) {
+ // Harden against NULL values (like in EntityType and ModelType)
+ $choices = null !== $options['choices'] ? $options['choices'] : array();
+
+ // Reuse existing choice lists in order to increase performance
+ $hash = md5(json_encode(array($choices, $options['preferred_choices'])));
+
+ if (!isset($choiceListCache[$hash])) {
+ $choiceListCache[$hash] = new SimpleChoiceList($choices, $options['preferred_choices']);
+ }
+
+ return $choiceListCache[$hash];
+ };
+
+ $emptyData = function (Options $options) {
+ if ($options['multiple'] || $options['expanded']) {
+ return array();
+ }
+
+ return '';
+ };
+
+ $emptyValue = function (Options $options) {
+ return $options['required'] ? null : '';
+ };
+
+ $emptyValueNormalizer = function (Options $options, $emptyValue) {
+ if ($options['multiple']) {
+ // never use an empty value for this case
+ return null;
+ } elseif (false === $emptyValue) {
+ // an empty value should be added but the user decided otherwise
+ return null;
+ } elseif ($options['expanded'] && '' === $emptyValue) {
+ // never use an empty label for radio buttons
+ return 'None';
+ }
+
+ // empty value has been set explicitly
+ return $emptyValue;
+ };
+
+ $compound = function (Options $options) {
+ return $options['expanded'];
+ };
+
+ $resolver->setDefaults(array(
+ 'multiple' => false,
+ 'expanded' => false,
+ 'choice_list' => $choiceList,
+ 'choices' => array(),
+ 'preferred_choices' => array(),
+ 'empty_data' => $emptyData,
+ 'empty_value' => $emptyValue,
+ 'error_bubbling' => false,
+ 'compound' => $compound,
+ // The view data is always a string, even if the "data" option
+ // is manually set to an object.
+ // See https://github.com/symfony/symfony/pull/5582
+ 'data_class' => null,
+ ));
+
+ $resolver->setNormalizers(array(
+ 'empty_value' => $emptyValueNormalizer,
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'choice_list' => array('null', 'Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface'),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'choice';
+ }
+
+ /**
+ * Adds the sub fields for an expanded choice field.
+ *
+ * @param FormBuilderInterface $builder The form builder.
+ * @param array $choiceViews The choice view objects.
+ * @param array $options The build options.
+ */
+ private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
+ {
+ foreach ($choiceViews as $i => $choiceView) {
+ if (is_array($choiceView)) {
+ // Flatten groups
+ $this->addSubForms($builder, $choiceView, $options);
+ } else {
+ $choiceOpts = array(
+ 'value' => $choiceView->value,
+ 'label' => $choiceView->label,
+ 'translation_domain' => $options['translation_domain'],
+ );
+
+ if ($options['multiple']) {
+ $choiceType = 'checkbox';
+ // The user can check 0 or more checkboxes. If required
+ // is true, he is required to check all of them.
+ $choiceOpts['required'] = false;
+ } else {
+ $choiceType = 'radio';
+ }
+
+ $builder->add($i, $choiceType, $choiceOpts);
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class CollectionType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ if ($options['allow_add'] && $options['prototype']) {
+ $prototype = $builder->create($options['prototype_name'], $options['type'], array_replace(array(
+ 'label' => $options['prototype_name'].'label__',
+ ), $options['options']));
+ $builder->setAttribute('prototype', $prototype->getForm());
+ }
+
+ $resizeListener = new ResizeFormListener(
+ $options['type'],
+ $options['options'],
+ $options['allow_add'],
+ $options['allow_delete']
+ );
+
+ $builder->addEventSubscriber($resizeListener);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars = array_replace($view->vars, array(
+ 'allow_add' => $options['allow_add'],
+ 'allow_delete' => $options['allow_delete'],
+ ));
+
+ if ($form->getConfig()->hasAttribute('prototype')) {
+ $view->vars['prototype'] = $form->getConfig()->getAttribute('prototype')->createView($view);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ if ($form->getConfig()->hasAttribute('prototype') && $view->vars['prototype']->vars['multipart']) {
+ $view->vars['multipart'] = true;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $optionsNormalizer = function (Options $options, $value) {
+ $value['block_name'] = 'entry';
+
+ return $value;
+ };
+
+ $resolver->setDefaults(array(
+ 'allow_add' => false,
+ 'allow_delete' => false,
+ 'prototype' => true,
+ 'prototype_name' => '__name__',
+ 'type' => 'text',
+ 'options' => array(),
+ ));
+
+ $resolver->setNormalizers(array(
+ 'options' => $optionsNormalizer,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'collection';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class CountryType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'choices' => Intl::getRegionBundle()->getCountryNames(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'choice';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'country';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class CurrencyType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'choices' => Intl::getCurrencyBundle()->getCurrencyNames(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'choice';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'currency';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\ReversedTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class DateTimeType extends AbstractType
+{
+ const DEFAULT_DATE_FORMAT = \IntlDateFormatter::MEDIUM;
+
+ const DEFAULT_TIME_FORMAT = \IntlDateFormatter::MEDIUM;
+
+ /**
+ * This is not quite the HTML5 format yet, because ICU lacks the
+ * capability of parsing and generating RFC 3339 dates, which
+ * are like the below pattern but with a timezone suffix. The
+ * timezone suffix is
+ *
+ * * "Z" for UTC
+ * * "(-|+)HH:mm" for other timezones (note the colon!)
+ *
+ * For more information see:
+ *
+ * http://userguide.icu-project.org/formatparse/datetime#TOC-Date-Time-Format-Syntax
+ * http://www.w3.org/TR/html-markup/input.datetime.html
+ * http://tools.ietf.org/html/rfc3339
+ *
+ * An ICU ticket was created:
+ * http://icu-project.org/trac/ticket/9421
+ *
+ * It was supposedly fixed, but is not available in all PHP installations
+ * yet. To temporarily circumvent this issue, DateTimeToRfc3339Transformer
+ * is used when the format matches this constant.
+ */
+ const HTML5_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";
+
+ private static $acceptedFormats = array(
+ \IntlDateFormatter::FULL,
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::MEDIUM,
+ \IntlDateFormatter::SHORT,
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $parts = array('year', 'month', 'day', 'hour');
+ $dateParts = array('year', 'month', 'day');
+ $timeParts = array('hour');
+
+ if ($options['with_minutes']) {
+ $parts[] = 'minute';
+ $timeParts[] = 'minute';
+ }
+
+ if ($options['with_seconds']) {
+ $parts[] = 'second';
+ $timeParts[] = 'second';
+ }
+
+ $dateFormat = is_int($options['date_format']) ? $options['date_format'] : self::DEFAULT_DATE_FORMAT;
+ $timeFormat = self::DEFAULT_TIME_FORMAT;
+ $calendar = \IntlDateFormatter::GREGORIAN;
+ $pattern = is_string($options['format']) ? $options['format'] : null;
+
+ if (!in_array($dateFormat, self::$acceptedFormats, true)) {
+ throw new InvalidOptionsException('The "date_format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
+ }
+
+ if ('single_text' === $options['widget']) {
+ if (self::HTML5_FORMAT === $pattern) {
+ $builder->addViewTransformer(new DateTimeToRfc3339Transformer(
+ $options['model_timezone'],
+ $options['view_timezone']
+ ));
+ } else {
+ $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
+ $options['model_timezone'],
+ $options['view_timezone'],
+ $dateFormat,
+ $timeFormat,
+ $calendar,
+ $pattern
+ ));
+ }
+ } else {
+ // Only pass a subset of the options to children
+ $dateOptions = array_intersect_key($options, array_flip(array(
+ 'years',
+ 'months',
+ 'days',
+ 'empty_value',
+ 'required',
+ 'translation_domain',
+ )));
+
+ $timeOptions = array_intersect_key($options, array_flip(array(
+ 'hours',
+ 'minutes',
+ 'seconds',
+ 'with_minutes',
+ 'with_seconds',
+ 'empty_value',
+ 'required',
+ 'translation_domain',
+ )));
+
+ if (null !== $options['date_widget']) {
+ $dateOptions['widget'] = $options['date_widget'];
+ }
+
+ if (null !== $options['time_widget']) {
+ $timeOptions['widget'] = $options['time_widget'];
+ }
+
+ if (null !== $options['date_format']) {
+ $dateOptions['format'] = $options['date_format'];
+ }
+
+ $dateOptions['input'] = $timeOptions['input'] = 'array';
+ $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
+
+ $builder
+ ->addViewTransformer(new DataTransformerChain(array(
+ new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
+ new ArrayToPartsTransformer(array(
+ 'date' => $dateParts,
+ 'time' => $timeParts,
+ )),
+ )))
+ ->add('date', 'date', $dateOptions)
+ ->add('time', 'time', $timeOptions)
+ ;
+ }
+
+ if ('string' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'])
+ ));
+ } elseif ('timestamp' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
+ ));
+ } elseif ('array' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
+ ));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['widget'] = $options['widget'];
+
+ // Change the input to a HTML5 date input if
+ // * the widget is set to "single_text"
+ // * the format matches the one expected by HTML5
+ if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
+ $view->vars['type'] = 'datetime';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $compound = function (Options $options) {
+ return $options['widget'] !== 'single_text';
+ };
+
+ // Defaults to the value of "widget"
+ $dateWidget = function (Options $options) {
+ return $options['widget'];
+ };
+
+ // Defaults to the value of "widget"
+ $timeWidget = function (Options $options) {
+ return $options['widget'];
+ };
+
+ $resolver->setDefaults(array(
+ 'input' => 'datetime',
+ 'model_timezone' => null,
+ 'view_timezone' => null,
+ 'format' => self::HTML5_FORMAT,
+ 'date_format' => null,
+ 'widget' => null,
+ 'date_widget' => $dateWidget,
+ 'time_widget' => $timeWidget,
+ 'with_minutes' => true,
+ 'with_seconds' => false,
+ // Don't modify \DateTime classes by reference, we treat
+ // them like immutable value objects
+ 'by_reference' => false,
+ 'error_bubbling' => false,
+ // If initialized with a \DateTime object, FormType initializes
+ // this option to "\DateTime". Since the internal, normalized
+ // representation is not \DateTime, but an array, we need to unset
+ // this option.
+ 'data_class' => null,
+ 'compound' => $compound,
+ ));
+
+ // Don't add some defaults in order to preserve the defaults
+ // set in DateType and TimeType
+ $resolver->setOptional(array(
+ 'empty_value',
+ 'years',
+ 'months',
+ 'days',
+ 'hours',
+ 'minutes',
+ 'seconds',
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'input' => array(
+ 'datetime',
+ 'string',
+ 'timestamp',
+ 'array',
+ ),
+ 'date_widget' => array(
+ null, // inherit default from DateType
+ 'single_text',
+ 'text',
+ 'choice',
+ ),
+ 'time_widget' => array(
+ null, // inherit default from TimeType
+ 'single_text',
+ 'text',
+ 'choice',
+ ),
+ // This option will overwrite "date_widget" and "time_widget" options
+ 'widget' => array(
+ null, // default, don't overwrite options
+ 'single_text',
+ 'text',
+ 'choice',
+ ),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'datetime';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
+use Symfony\Component\Form\ReversedTransformer;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
+
+class DateType extends AbstractType
+{
+ const DEFAULT_FORMAT = \IntlDateFormatter::MEDIUM;
+
+ const HTML5_FORMAT = 'yyyy-MM-dd';
+
+ private static $acceptedFormats = array(
+ \IntlDateFormatter::FULL,
+ \IntlDateFormatter::LONG,
+ \IntlDateFormatter::MEDIUM,
+ \IntlDateFormatter::SHORT,
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $dateFormat = is_int($options['format']) ? $options['format'] : self::DEFAULT_FORMAT;
+ $timeFormat = \IntlDateFormatter::NONE;
+ $calendar = \IntlDateFormatter::GREGORIAN;
+ $pattern = is_string($options['format']) ? $options['format'] : null;
+
+ if (!in_array($dateFormat, self::$acceptedFormats, true)) {
+ throw new InvalidOptionsException('The "format" option must be one of the IntlDateFormatter constants (FULL, LONG, MEDIUM, SHORT) or a string representing a custom format.');
+ }
+
+ if (null !== $pattern && (false === strpos($pattern, 'y') || false === strpos($pattern, 'M') || false === strpos($pattern, 'd'))) {
+ throw new InvalidOptionsException(sprintf('The "format" option should contain the letters "y", "M" and "d". Its current value is "%s".', $pattern));
+ }
+
+ if ('single_text' === $options['widget']) {
+ $builder->addViewTransformer(new DateTimeToLocalizedStringTransformer(
+ $options['model_timezone'],
+ $options['view_timezone'],
+ $dateFormat,
+ $timeFormat,
+ $calendar,
+ $pattern
+ ));
+ } else {
+ $yearOptions = $monthOptions = $dayOptions = array(
+ 'error_bubbling' => true,
+ );
+
+ $formatter = new \IntlDateFormatter(
+ \Locale::getDefault(),
+ $dateFormat,
+ $timeFormat,
+ 'UTC',
+ $calendar,
+ $pattern
+ );
+ $formatter->setLenient(false);
+
+ if ('choice' === $options['widget']) {
+ // Only pass a subset of the options to children
+ $yearOptions['choices'] = $this->formatTimestamps($formatter, '/y+/', $this->listYears($options['years']));
+ $yearOptions['empty_value'] = $options['empty_value']['year'];
+ $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months']));
+ $monthOptions['empty_value'] = $options['empty_value']['month'];
+ $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days']));
+ $dayOptions['empty_value'] = $options['empty_value']['day'];
+ }
+
+ // Append generic carry-along options
+ foreach (array('required', 'translation_domain') as $passOpt) {
+ $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt];
+ }
+
+ $builder
+ ->add('year', $options['widget'], $yearOptions)
+ ->add('month', $options['widget'], $monthOptions)
+ ->add('day', $options['widget'], $dayOptions)
+ ->addViewTransformer(new DateTimeToArrayTransformer(
+ $options['model_timezone'], $options['view_timezone'], array('year', 'month', 'day')
+ ))
+ ->setAttribute('formatter', $formatter)
+ ;
+ }
+
+ if ('string' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'Y-m-d')
+ ));
+ } elseif ('timestamp' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
+ ));
+ } elseif ('array' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], array('year', 'month', 'day'))
+ ));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['widget'] = $options['widget'];
+
+ // Change the input to a HTML5 date input if
+ // * the widget is set to "single_text"
+ // * the format matches the one expected by HTML5
+ if ('single_text' === $options['widget'] && self::HTML5_FORMAT === $options['format']) {
+ $view->vars['type'] = 'date';
+ }
+
+ if ($form->getConfig()->hasAttribute('formatter')) {
+ $pattern = $form->getConfig()->getAttribute('formatter')->getPattern();
+
+ // remove special characters unless the format was explicitly specified
+ if (!is_string($options['format'])) {
+ $pattern = preg_replace('/[^yMd]+/', '', $pattern);
+ }
+
+ // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy)
+ // lookup various formats at http://userguide.icu-project.org/formatparse/datetime
+ if (preg_match('/^([yMd]+)[^yMd]*([yMd]+)[^yMd]*([yMd]+)$/', $pattern)) {
+ $pattern = preg_replace(array('/y+/', '/M+/', '/d+/'), array('{{ year }}', '{{ month }}', '{{ day }}'), $pattern);
+ } else {
+ // default fallback
+ $pattern = '{{ year }}{{ month }}{{ day }}';
+ }
+
+ $view->vars['date_pattern'] = $pattern;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $compound = function (Options $options) {
+ return $options['widget'] !== 'single_text';
+ };
+
+ $emptyValue = $emptyValueDefault = function (Options $options) {
+ return $options['required'] ? null : '';
+ };
+
+ $emptyValueNormalizer = function (Options $options, $emptyValue) use ($emptyValueDefault) {
+ if (is_array($emptyValue)) {
+ $default = $emptyValueDefault($options);
+
+ return array_merge(
+ array('year' => $default, 'month' => $default, 'day' => $default),
+ $emptyValue
+ );
+ }
+
+ return array(
+ 'year' => $emptyValue,
+ 'month' => $emptyValue,
+ 'day' => $emptyValue
+ );
+ };
+
+ $format = function (Options $options) {
+ return $options['widget'] === 'single_text' ? DateType::HTML5_FORMAT : DateType::DEFAULT_FORMAT;
+ };
+
+ $resolver->setDefaults(array(
+ 'years' => range(date('Y') - 5, date('Y') + 5),
+ 'months' => range(1, 12),
+ 'days' => range(1, 31),
+ 'widget' => 'choice',
+ 'input' => 'datetime',
+ 'format' => $format,
+ 'model_timezone' => null,
+ 'view_timezone' => null,
+ 'empty_value' => $emptyValue,
+ // Don't modify \DateTime classes by reference, we treat
+ // them like immutable value objects
+ 'by_reference' => false,
+ 'error_bubbling' => false,
+ // If initialized with a \DateTime object, FormType initializes
+ // this option to "\DateTime". Since the internal, normalized
+ // representation is not \DateTime, but an array, we need to unset
+ // this option.
+ 'data_class' => null,
+ 'compound' => $compound,
+ ));
+
+ $resolver->setNormalizers(array(
+ 'empty_value' => $emptyValueNormalizer,
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'input' => array(
+ 'datetime',
+ 'string',
+ 'timestamp',
+ 'array',
+ ),
+ 'widget' => array(
+ 'single_text',
+ 'text',
+ 'choice',
+ ),
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'format' => array('int', 'string'),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'date';
+ }
+
+ private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps)
+ {
+ $pattern = $formatter->getPattern();
+ $timezone = $formatter->getTimezoneId();
+
+ if (version_compare(\PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone(\DateTimeZone::UTC);
+ } else {
+ $formatter->setTimeZoneId(\DateTimeZone::UTC);
+ }
+
+ if (preg_match($regex, $pattern, $matches)) {
+ $formatter->setPattern($matches[0]);
+
+ foreach ($timestamps as $key => $timestamp) {
+ $timestamps[$key] = $formatter->format($timestamp);
+ }
+
+ // I'd like to clone the formatter above, but then we get a
+ // segmentation fault, so let's restore the old state instead
+ $formatter->setPattern($pattern);
+ }
+
+ if (version_compare(\PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone($timezone);
+ } else {
+ $formatter->setTimeZoneId($timezone);
+ }
+
+ return $timestamps;
+ }
+
+ private function listYears(array $years)
+ {
+ $result = array();
+
+ foreach ($years as $year) {
+ $result[$year] = gmmktime(0, 0, 0, 6, 15, $year);
+ }
+
+ return $result;
+ }
+
+ private function listMonths(array $months)
+ {
+ $result = array();
+
+ foreach ($months as $month) {
+ $result[$month] = gmmktime(0, 0, 0, $month, 15);
+ }
+
+ return $result;
+ }
+
+ private function listDays(array $days)
+ {
+ $result = array();
+
+ foreach ($days as $day) {
+ $result[$day] = gmmktime(0, 0, 0, 5, $day);
+ }
+
+ return $result;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+
+class EmailType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'text';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'email';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class FileType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars = array_replace($view->vars, array(
+ 'type' => 'file',
+ 'value' => '',
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ $view
+ ->vars['multipart'] = true
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'compound' => false,
+ 'data_class' => 'Symfony\Component\HttpFoundation\File\File',
+ 'empty_data' => null,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'file';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
+use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
+use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+use Symfony\Component\PropertyAccess\PropertyAccess;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+
+class FormType extends BaseType
+{
+ /**
+ * @var PropertyAccessorInterface
+ */
+ private $propertyAccessor;
+
+ public function __construct(PropertyAccessorInterface $propertyAccessor = null)
+ {
+ $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ parent::buildForm($builder, $options);
+
+ $builder
+ ->setRequired($options['required'])
+ ->setErrorBubbling($options['error_bubbling'])
+ ->setEmptyData($options['empty_data'])
+ ->setPropertyPath($options['property_path'])
+ ->setMapped($options['mapped'])
+ ->setByReference($options['by_reference'])
+ ->setInheritData($options['inherit_data'])
+ ->setCompound($options['compound'])
+ ->setData(isset($options['data']) ? $options['data'] : null)
+ ->setDataLocked(isset($options['data']))
+ ->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null)
+ ->setMethod($options['method'])
+ ->setAction($options['action'])
+ ->setAutoInitialize($options['auto_initialize'])
+ ;
+
+ if ($options['trim']) {
+ $builder->addEventSubscriber(new TrimListener());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ parent::buildView($view, $form, $options);
+
+ $name = $form->getName();
+ $readOnly = $options['read_only'];
+
+ if ($view->parent) {
+ if ('' === $name) {
+ throw new LogicException('Form node with empty name can be used only as root form node.');
+ }
+
+ // Complex fields are read-only if they themselves or their parents are.
+ if (!$readOnly) {
+ $readOnly = $view->parent->vars['read_only'];
+ }
+ }
+
+ $view->vars = array_replace($view->vars, array(
+ 'read_only' => $readOnly,
+ 'errors' => $form->getErrors(),
+ 'valid' => $form->isSubmitted() ? $form->isValid() : true,
+ 'value' => $form->getViewData(),
+ 'data' => $form->getNormData(),
+ 'required' => $form->isRequired(),
+ 'max_length' => $options['max_length'],
+ 'pattern' => $options['pattern'],
+ 'size' => null,
+ 'label_attr' => $options['label_attr'],
+ 'compound' => $form->getConfig()->getCompound(),
+ 'method' => $form->getConfig()->getMethod(),
+ 'action' => $form->getConfig()->getAction(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ $multipart = false;
+
+ foreach ($view->children as $child) {
+ if ($child->vars['multipart']) {
+ $multipart = true;
+ break;
+ }
+ }
+
+ $view->vars['multipart'] = $multipart;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ parent::setDefaultOptions($resolver);
+
+ // Derive "data_class" option from passed "data" object
+ $dataClass = function (Options $options) {
+ return isset($options['data']) && is_object($options['data']) ? get_class($options['data']) : null;
+ };
+
+ // Derive "empty_data" closure from "data_class" option
+ $emptyData = function (Options $options) {
+ $class = $options['data_class'];
+
+ if (null !== $class) {
+ return function (FormInterface $form) use ($class) {
+ return $form->isEmpty() && !$form->isRequired() ? null : new $class();
+ };
+ }
+
+ return function (FormInterface $form) {
+ return $form->getConfig()->getCompound() ? array() : '';
+ };
+ };
+
+ // For any form that is not represented by a single HTML control,
+ // errors should bubble up by default
+ $errorBubbling = function (Options $options) {
+ return $options['compound'];
+ };
+
+ // BC with old "virtual" option
+ $inheritData = function (Options $options) {
+ if (null !== $options['virtual']) {
+ // Uncomment this as soon as the deprecation note should be shown
+ // trigger_error('The form option "virtual" is deprecated since version 2.3 and will be removed in 3.0. Use "inherit_data" instead.', E_USER_DEPRECATED);
+ return $options['virtual'];
+ }
+
+ return false;
+ };
+
+ // If data is given, the form is locked to that data
+ // (independent of its value)
+ $resolver->setOptional(array(
+ 'data',
+ ));
+
+ $resolver->setDefaults(array(
+ 'data_class' => $dataClass,
+ 'empty_data' => $emptyData,
+ 'trim' => true,
+ 'required' => true,
+ 'read_only' => false,
+ 'max_length' => null,
+ 'pattern' => null,
+ 'property_path' => null,
+ 'mapped' => true,
+ 'by_reference' => true,
+ 'error_bubbling' => $errorBubbling,
+ 'label_attr' => array(),
+ 'virtual' => null,
+ 'inherit_data' => $inheritData,
+ 'compound' => true,
+ 'method' => 'POST',
+ // According to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt)
+ // section 4.2., empty URIs are considered same-document references
+ 'action' => '',
+ 'auto_initialize' => true,
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'label_attr' => 'array',
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class HiddenType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ // hidden fields cannot have a required attribute
+ 'required' => false,
+ // Pass errors to the parent
+ 'error_bubbling' => true,
+ 'compound' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'hidden';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class IntegerType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addViewTransformer(
+ new IntegerToLocalizedStringTransformer(
+ $options['precision'],
+ $options['grouping'],
+ $options['rounding_mode']
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ // default precision is locale specific (usually around 3)
+ 'precision' => null,
+ 'grouping' => false,
+ // Integer cast rounds towards 0, so do the same when displaying fractions
+ 'rounding_mode' => \NumberFormatter::ROUND_DOWN,
+ 'compound' => false,
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'rounding_mode' => array(
+ \NumberFormatter::ROUND_FLOOR,
+ \NumberFormatter::ROUND_DOWN,
+ \NumberFormatter::ROUND_HALFDOWN,
+ \NumberFormatter::ROUND_HALFEVEN,
+ \NumberFormatter::ROUND_HALFUP,
+ \NumberFormatter::ROUND_UP,
+ \NumberFormatter::ROUND_CEILING,
+ ),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'integer';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class LanguageType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'choices' => Intl::getLanguageBundle()->getLanguageNames(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'choice';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'language';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Locale\Locale;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class LocaleType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'choices' => Intl::getLocaleBundle()->getLocaleNames(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'choice';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'locale';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class MoneyType extends AbstractType
+{
+ protected static $patterns = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->addViewTransformer(new MoneyToLocalizedStringTransformer(
+ $options['precision'],
+ $options['grouping'],
+ null,
+ $options['divisor']
+ ))
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['money_pattern'] = self::getPattern($options['currency']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'precision' => 2,
+ 'grouping' => false,
+ 'divisor' => 1,
+ 'currency' => 'EUR',
+ 'compound' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'money';
+ }
+
+ /**
+ * Returns the pattern for this locale
+ *
+ * The pattern contains the placeholder "{{ widget }}" where the HTML tag should
+ * be inserted
+ */
+ protected static function getPattern($currency)
+ {
+ if (!$currency) {
+ return '{{ widget }}';
+ }
+
+ $locale = \Locale::getDefault();
+
+ if (!isset(self::$patterns[$locale])) {
+ self::$patterns[$locale] = array();
+ }
+
+ if (!isset(self::$patterns[$locale][$currency])) {
+ $format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+ $pattern = $format->formatCurrency('123', $currency);
+
+ // the spacings between currency symbol and number are ignored, because
+ // a single space leads to better readability in combination with input
+ // fields
+
+ // the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8)
+
+ preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123(?:[,.]0+)?[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/u', $pattern, $matches);
+
+ if (!empty($matches[1])) {
+ self::$patterns[$locale][$currency] = $matches[1].' {{ widget }}';
+ } elseif (!empty($matches[2])) {
+ self::$patterns[$locale][$currency] = '{{ widget }} '.$matches[2];
+ } else {
+ self::$patterns[$locale][$currency] = '{{ widget }}';
+ }
+ }
+
+ return self::$patterns[$locale][$currency];
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class NumberType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addViewTransformer(new NumberToLocalizedStringTransformer(
+ $options['precision'],
+ $options['grouping'],
+ $options['rounding_mode']
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ // default precision is locale specific (usually around 3)
+ 'precision' => null,
+ 'grouping' => false,
+ 'rounding_mode' => \NumberFormatter::ROUND_HALFUP,
+ 'compound' => false,
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'rounding_mode' => array(
+ \NumberFormatter::ROUND_FLOOR,
+ \NumberFormatter::ROUND_DOWN,
+ \NumberFormatter::ROUND_HALFDOWN,
+ \NumberFormatter::ROUND_HALFEVEN,
+ \NumberFormatter::ROUND_HALFUP,
+ \NumberFormatter::ROUND_UP,
+ \NumberFormatter::ROUND_CEILING,
+ ),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'number';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class PasswordType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ if ($options['always_empty'] || !$form->isSubmitted()) {
+ $view->vars['value'] = '';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'always_empty' => true,
+ 'trim' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'text';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'password';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class PercentType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addViewTransformer(new PercentToLocalizedStringTransformer($options['precision'], $options['type']));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'precision' => 0,
+ 'type' => 'fractional',
+ 'compound' => false,
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'type' => array(
+ 'fractional',
+ 'integer',
+ ),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'percent';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+
+class RadioType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'checkbox';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'radio';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class RepeatedType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ // Overwrite required option for child fields
+ $options['first_options']['required'] = $options['required'];
+ $options['second_options']['required'] = $options['required'];
+
+ if (!isset($options['options']['error_bubbling'])) {
+ $options['options']['error_bubbling'] = $options['error_bubbling'];
+ }
+
+ $builder
+ ->addViewTransformer(new ValueToDuplicatesTransformer(array(
+ $options['first_name'],
+ $options['second_name'],
+ )))
+ ->add($options['first_name'], $options['type'], array_merge($options['options'], $options['first_options']))
+ ->add($options['second_name'], $options['type'], array_merge($options['options'], $options['second_options']))
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'type' => 'text',
+ 'options' => array(),
+ 'first_options' => array(),
+ 'second_options' => array(),
+ 'first_name' => 'first',
+ 'second_name' => 'second',
+ 'error_bubbling' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'repeated';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\ButtonTypeInterface;
+
+/**
+ * A reset button.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ResetType extends AbstractType implements ButtonTypeInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'button';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'reset';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+
+class SearchType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'text';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'search';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\SubmitButtonTypeInterface;
+
+/**
+ * A submit button.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SubmitType extends AbstractType implements SubmitButtonTypeInterface
+{
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['clicked'] = $form->isClicked();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'button';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'submit';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class TextType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'compound' => false,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'text';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\FormInterface;
+
+class TextareaType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['pattern'] = null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'text';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'textarea';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\ReversedTransformer;
+use Symfony\Component\Form\Exception\InvalidConfigurationException;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class TimeType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $parts = array('hour');
+ $format = 'H';
+
+ if ($options['with_seconds'] && !$options['with_minutes']) {
+ throw new InvalidConfigurationException('You can not disable minutes if you have enabled seconds.');
+ }
+
+ if ($options['with_minutes']) {
+ $format .= ':i';
+ $parts[] = 'minute';
+ }
+
+ if ($options['with_seconds']) {
+ $format .= ':s';
+ $parts[] = 'second';
+ }
+
+ if ('single_text' === $options['widget']) {
+ $builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
+ } else {
+ $hourOptions = $minuteOptions = $secondOptions = array(
+ 'error_bubbling' => true,
+ );
+
+ if ('choice' === $options['widget']) {
+ $hours = $minutes = array();
+
+ foreach ($options['hours'] as $hour) {
+ $hours[$hour] = str_pad($hour, 2, '0', STR_PAD_LEFT);
+ }
+
+ // Only pass a subset of the options to children
+ $hourOptions['choices'] = $hours;
+ $hourOptions['empty_value'] = $options['empty_value']['hour'];
+
+ if ($options['with_minutes']) {
+ foreach ($options['minutes'] as $minute) {
+ $minutes[$minute] = str_pad($minute, 2, '0', STR_PAD_LEFT);
+ }
+
+ $minuteOptions['choices'] = $minutes;
+ $minuteOptions['empty_value'] = $options['empty_value']['minute'];
+ }
+
+ if ($options['with_seconds']) {
+ $seconds = array();
+
+ foreach ($options['seconds'] as $second) {
+ $seconds[$second] = str_pad($second, 2, '0', STR_PAD_LEFT);
+ }
+
+ $secondOptions['choices'] = $seconds;
+ $secondOptions['empty_value'] = $options['empty_value']['second'];
+ }
+
+ // Append generic carry-along options
+ foreach (array('required', 'translation_domain') as $passOpt) {
+ $hourOptions[$passOpt] = $options[$passOpt];
+
+ if ($options['with_minutes']) {
+ $minuteOptions[$passOpt] = $options[$passOpt];
+ }
+
+ if ($options['with_seconds']) {
+ $secondOptions[$passOpt] = $options[$passOpt];
+ }
+ }
+ }
+
+ $builder->add('hour', $options['widget'], $hourOptions);
+
+ if ($options['with_minutes']) {
+ $builder->add('minute', $options['widget'], $minuteOptions);
+ }
+
+ if ($options['with_seconds']) {
+ $builder->add('second', $options['widget'], $secondOptions);
+ }
+
+ $builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
+ }
+
+ if ('string' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], 'H:i:s')
+ ));
+ } elseif ('timestamp' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToTimestampTransformer($options['model_timezone'], $options['model_timezone'])
+ ));
+ } elseif ('array' === $options['input']) {
+ $builder->addModelTransformer(new ReversedTransformer(
+ new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
+ ));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars = array_replace($view->vars, array(
+ 'widget' => $options['widget'],
+ 'with_minutes' => $options['with_minutes'],
+ 'with_seconds' => $options['with_seconds'],
+ ));
+
+ if ('single_text' === $options['widget']) {
+ $view->vars['type'] = 'time';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $compound = function (Options $options) {
+ return $options['widget'] !== 'single_text';
+ };
+
+ $emptyValue = $emptyValueDefault = function (Options $options) {
+ return $options['required'] ? null : '';
+ };
+
+ $emptyValueNormalizer = function (Options $options, $emptyValue) use ($emptyValueDefault) {
+ if (is_array($emptyValue)) {
+ $default = $emptyValueDefault($options);
+
+ return array_merge(
+ array('hour' => $default, 'minute' => $default, 'second' => $default),
+ $emptyValue
+ );
+ }
+
+ return array(
+ 'hour' => $emptyValue,
+ 'minute' => $emptyValue,
+ 'second' => $emptyValue
+ );
+ };
+
+ $resolver->setDefaults(array(
+ 'hours' => range(0, 23),
+ 'minutes' => range(0, 59),
+ 'seconds' => range(0, 59),
+ 'widget' => 'choice',
+ 'input' => 'datetime',
+ 'with_minutes' => true,
+ 'with_seconds' => false,
+ 'model_timezone' => null,
+ 'view_timezone' => null,
+ 'empty_value' => $emptyValue,
+ // Don't modify \DateTime classes by reference, we treat
+ // them like immutable value objects
+ 'by_reference' => false,
+ 'error_bubbling' => false,
+ // If initialized with a \DateTime object, FormType initializes
+ // this option to "\DateTime". Since the internal, normalized
+ // representation is not \DateTime, but an array, we need to unset
+ // this option.
+ 'data_class' => null,
+ 'compound' => $compound,
+ ));
+
+ $resolver->setNormalizers(array(
+ 'empty_value' => $emptyValueNormalizer,
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'input' => array(
+ 'datetime',
+ 'string',
+ 'timestamp',
+ 'array',
+ ),
+ 'widget' => array(
+ 'single_text',
+ 'text',
+ 'choice',
+ ),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'time';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class TimezoneType extends AbstractType
+{
+ /**
+ * Stores the available timezone choices
+ * @var array
+ */
+ private static $timezones;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'choices' => self::getTimezones(),
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'choice';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'timezone';
+ }
+
+ /**
+ * Returns the timezone choices.
+ *
+ * The choices are generated from the ICU function
+ * \DateTimeZone::listIdentifiers(). They are cached during a single request,
+ * so multiple timezone fields on the same page don't lead to unnecessary
+ * overhead.
+ *
+ * @return array The timezone choices
+ */
+ public static function getTimezones()
+ {
+ if (null === static::$timezones) {
+ static::$timezones = array();
+
+ foreach (\DateTimeZone::listIdentifiers() as $timezone) {
+ $parts = explode('/', $timezone);
+
+ if (count($parts) > 2) {
+ $region = $parts[0];
+ $name = $parts[1].' - '.$parts[2];
+ } elseif (count($parts) > 1) {
+ $region = $parts[0];
+ $name = $parts[1];
+ } else {
+ $region = 'Other';
+ $name = $parts[0];
+ }
+
+ static::$timezones[$region][$timezone] = str_replace('_', ' ', $name);
+ }
+ }
+
+ return static::$timezones;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class UrlType extends AbstractType
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addEventSubscriber(new FixUrlProtocolListener($options['default_protocol']));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'default_protocol' => 'http',
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return 'text';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'url';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Core\View;
+
+/**
+ * Represents a choice in templates.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoiceView
+{
+ /**
+ * The original choice value.
+ *
+ * @var mixed
+ */
+ public $data;
+
+ /**
+ * The view representation of the choice.
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * The label displayed to humans.
+ *
+ * @var string
+ */
+ public $label;
+
+ /**
+ * Creates a new ChoiceView.
+ *
+ * @param mixed $data The original choice.
+ * @param string $value The view representation of the choice.
+ * @param string $label The label displayed to humans.
+ */
+ public function __construct($data, $value, $label)
+ {
+ $this->data = $data;
+ $this->value = $value;
+ $this->label = $label;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Csrf;
+
+use Symfony\Component\Form\Extension\Csrf\Type;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+use Symfony\Component\Form\AbstractExtension;
+use Symfony\Component\Translation\TranslatorInterface;
+
+/**
+ * This extension protects forms by using a CSRF token.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CsrfExtension extends AbstractExtension
+{
+ /**
+ * @var CsrfProviderInterface
+ */
+ private $csrfProvider;
+
+ /**
+ * @var TranslatorInterface
+ */
+ private $translator;
+
+ /**
+ * @var null|string
+ */
+ private $translationDomain;
+
+ /**
+ * Constructor.
+ *
+ * @param CsrfProviderInterface $csrfProvider The CSRF provider
+ * @param TranslatorInterface $translator The translator for translating error messages.
+ * @param null|string $translationDomain The translation domain for translating.
+ */
+ public function __construct(CsrfProviderInterface $csrfProvider, TranslatorInterface $translator = null, $translationDomain = null)
+ {
+ $this->csrfProvider = $csrfProvider;
+ $this->translator = $translator;
+ $this->translationDomain = $translationDomain;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function loadTypeExtensions()
+ {
+ return array(
+ new Type\FormTypeCsrfExtension($this->csrfProvider, true, '_token', $this->translator, $this->translationDomain),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Extension\Csrf\CsrfProvider;
+
+/**
+ * Marks classes able to provide CSRF protection
+ *
+ * You can generate a CSRF token by using the method generateCsrfToken(). To
+ * this method you should pass a value that is unique to the page that should
+ * be secured against CSRF attacks. This value doesn't necessarily have to be
+ * secret. Implementations of this interface are responsible for adding more
+ * secret information.
+ *
+ * If you want to secure a form submission against CSRF attacks, you could
+ * supply an "intention" string. This way you make sure that the form can only
+ * be submitted to pages that are designed to handle the form, that is, that use
+ * the same intention string to validate the CSRF token with isCsrfTokenValid().
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface CsrfProviderInterface
+{
+ /**
+ * Generates a CSRF token for a page of your application.
+ *
+ * @param string $intention Some value that identifies the action intention
+ * (i.e. "authenticate"). Doesn't have to be a secret value.
+ */
+ public function generateCsrfToken($intention);
+
+ /**
+ * Validates a CSRF token.
+ *
+ * @param string $intention The intention used when generating the CSRF token
+ * @param string $token The token supplied by the browser
+ *
+ * @return Boolean Whether the token supplied by the browser is correct
+ */
+ public function isCsrfTokenValid($intention, $token);
+}
--- /dev/null
+<?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\Form\Extension\Csrf\CsrfProvider;
+
+/**
+ * Default implementation of CsrfProviderInterface.
+ *
+ * This provider uses the session ID returned by session_id() as well as a
+ * user-defined secret value to secure the CSRF token.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class DefaultCsrfProvider implements CsrfProviderInterface
+{
+ /**
+ * A secret value used for generating the CSRF token
+ * @var string
+ */
+ protected $secret;
+
+ /**
+ * Initializes the provider with a secret value
+ *
+ * A recommended value for the secret is a generated value with at least
+ * 32 characters and mixed letters, digits and special characters.
+ *
+ * @param string $secret A secret value included in the CSRF token
+ */
+ public function __construct($secret)
+ {
+ $this->secret = $secret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function generateCsrfToken($intention)
+ {
+ return sha1($this->secret.$intention.$this->getSessionId());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isCsrfTokenValid($intention, $token)
+ {
+ return $token === $this->generateCsrfToken($intention);
+ }
+
+ /**
+ * Returns the ID of the user session.
+ *
+ * Automatically starts the session if necessary.
+ *
+ * @return string The session ID
+ */
+ protected function getSessionId()
+ {
+ if (version_compare(PHP_VERSION, '5.4', '>=')) {
+ if (PHP_SESSION_NONE === session_status()) {
+ session_start();
+ }
+ } elseif (!session_id()) {
+ session_start();
+ }
+
+ return session_id();
+ }
+}
--- /dev/null
+<?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\Form\Extension\Csrf\CsrfProvider;
+
+use Symfony\Component\HttpFoundation\Session\Session;
+
+/**
+ * This provider uses a Symfony2 Session object to retrieve the user's
+ * session ID.
+ *
+ * @see DefaultCsrfProvider
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SessionCsrfProvider extends DefaultCsrfProvider
+{
+ /**
+ * The user session from which the session ID is returned
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * Initializes the provider with a Session object and a secret value.
+ *
+ * A recommended value for the secret is a generated value with at least
+ * 32 characters and mixed letters, digits and special characters.
+ *
+ * @param Session $session The user session
+ * @param string $secret A secret value included in the CSRF token
+ */
+ public function __construct(Session $session, $secret)
+ {
+ parent::__construct($secret);
+
+ $this->session = $session;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSessionId()
+ {
+ $this->session->start();
+
+ return $this->session->getId();
+ }
+}
--- /dev/null
+<?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\Form\Extension\Csrf\EventListener;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+use Symfony\Component\Translation\TranslatorInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CsrfValidationListener implements EventSubscriberInterface
+{
+ /**
+ * The name of the CSRF field
+ * @var string
+ */
+ private $fieldName;
+
+ /**
+ * The provider for generating and validating CSRF tokens
+ * @var CsrfProviderInterface
+ */
+ private $csrfProvider;
+
+ /**
+ * A text mentioning the intention of the CSRF token
+ *
+ * Validation of the token will only succeed if it was generated in the
+ * same session and with the same intention.
+ *
+ * @var string
+ */
+ private $intention;
+
+ /**
+ * The message displayed in case of an error.
+ * @var string
+ */
+ private $errorMessage;
+
+ /**
+ * @var TranslatorInterface
+ */
+ private $translator;
+
+ /**
+ * @var null|string
+ */
+ private $translationDomain;
+
+ public static function getSubscribedEvents()
+ {
+ return array(
+ FormEvents::PRE_SUBMIT => 'preSubmit',
+ );
+ }
+
+ public function __construct($fieldName, CsrfProviderInterface $csrfProvider, $intention, $errorMessage, TranslatorInterface $translator = null, $translationDomain = null)
+ {
+ $this->fieldName = $fieldName;
+ $this->csrfProvider = $csrfProvider;
+ $this->intention = $intention;
+ $this->errorMessage = $errorMessage;
+ $this->translator = $translator;
+ $this->translationDomain = $translationDomain;
+ }
+
+ public function preSubmit(FormEvent $event)
+ {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ if ($form->isRoot() && $form->getConfig()->getOption('compound')) {
+ if (!isset($data[$this->fieldName]) || !$this->csrfProvider->isCsrfTokenValid($this->intention, $data[$this->fieldName])) {
+ $errorMessage = $this->errorMessage;
+
+ if (null !== $this->translator) {
+ $errorMessage = $this->translator->trans($errorMessage, array(), $this->translationDomain);
+ }
+
+ $form->addError(new FormError($errorMessage));
+ }
+
+ if (is_array($data)) {
+ unset($data[$this->fieldName]);
+ }
+ }
+
+ $event->setData($data);
+ }
+
+ /**
+ * Alias of {@link preSubmit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link preSubmit()} instead.
+ */
+ public function preBind(FormEvent $event)
+ {
+ $this->preSubmit($event);
+ }
+}
--- /dev/null
+<?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\Form\Extension\Csrf\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+use Symfony\Component\Translation\TranslatorInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormTypeCsrfExtension extends AbstractTypeExtension
+{
+ /**
+ * @var CsrfProviderInterface
+ */
+ private $defaultCsrfProvider;
+
+ /**
+ * @var Boolean
+ */
+ private $defaultEnabled;
+
+ /**
+ * @var string
+ */
+ private $defaultFieldName;
+
+ /**
+ * @var TranslatorInterface
+ */
+ private $translator;
+
+ /**
+ * @var null|string
+ */
+ private $translationDomain;
+
+ public function __construct(CsrfProviderInterface $defaultCsrfProvider, $defaultEnabled = true, $defaultFieldName = '_token', TranslatorInterface $translator = null, $translationDomain = null)
+ {
+ $this->defaultCsrfProvider = $defaultCsrfProvider;
+ $this->defaultEnabled = $defaultEnabled;
+ $this->defaultFieldName = $defaultFieldName;
+ $this->translator = $translator;
+ $this->translationDomain = $translationDomain;
+ }
+
+ /**
+ * Adds a CSRF field to the form when the CSRF protection is enabled.
+ *
+ * @param FormBuilderInterface $builder The form builder
+ * @param array $options The options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ if (!$options['csrf_protection']) {
+ return;
+ }
+
+ $builder
+ ->setAttribute('csrf_factory', $builder->getFormFactory())
+ ->addEventSubscriber(new CsrfValidationListener(
+ $options['csrf_field_name'],
+ $options['csrf_provider'],
+ $options['intention'],
+ $options['csrf_message'],
+ $this->translator,
+ $this->translationDomain
+ ))
+ ;
+ }
+
+ /**
+ * Adds a CSRF field to the root form view.
+ *
+ * @param FormView $view The form view
+ * @param FormInterface $form The form
+ * @param array $options The options
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ if ($options['csrf_protection'] && !$view->parent && $options['compound']) {
+ $factory = $form->getConfig()->getAttribute('csrf_factory');
+ $data = $options['csrf_provider']->generateCsrfToken($options['intention']);
+
+ $csrfForm = $factory->createNamed($options['csrf_field_name'], 'hidden', $data, array(
+ 'mapped' => false,
+ ));
+
+ $view->children[$options['csrf_field_name']] = $csrfForm->createView($view);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'csrf_protection' => $this->defaultEnabled,
+ 'csrf_field_name' => $this->defaultFieldName,
+ 'csrf_provider' => $this->defaultCsrfProvider,
+ 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.',
+ 'intention' => 'unknown',
+ ));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getExtendedType()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form\Extension\DependencyInjection;
+
+use Symfony\Component\Form\FormExtensionInterface;
+use Symfony\Component\Form\FormTypeGuesserChain;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class DependencyInjectionExtension implements FormExtensionInterface
+{
+ private $container;
+
+ private $typeServiceIds;
+
+ private $guesserServiceIds;
+
+ private $guesser;
+
+ private $guesserLoaded = false;
+
+ public function __construct(ContainerInterface $container,
+ array $typeServiceIds, array $typeExtensionServiceIds,
+ array $guesserServiceIds)
+ {
+ $this->container = $container;
+ $this->typeServiceIds = $typeServiceIds;
+ $this->typeExtensionServiceIds = $typeExtensionServiceIds;
+ $this->guesserServiceIds = $guesserServiceIds;
+ }
+
+ public function getType($name)
+ {
+ if (!isset($this->typeServiceIds[$name])) {
+ throw new InvalidArgumentException(sprintf('The field type "%s" is not registered with the service container.', $name));
+ }
+
+ $type = $this->container->get($this->typeServiceIds[$name]);
+
+ if ($type->getName() !== $name) {
+ throw new InvalidArgumentException(
+ sprintf('The type name specified for the service "%s" does not match the actual name. Expected "%s", given "%s"',
+ $this->typeServiceIds[$name],
+ $name,
+ $type->getName()
+ ));
+ }
+
+ return $type;
+ }
+
+ public function hasType($name)
+ {
+ return isset($this->typeServiceIds[$name]);
+ }
+
+ public function getTypeExtensions($name)
+ {
+ $extensions = array();
+
+ if (isset($this->typeExtensionServiceIds[$name])) {
+ foreach ($this->typeExtensionServiceIds[$name] as $serviceId) {
+ $extensions[] = $this->container->get($serviceId);
+ }
+ }
+
+ return $extensions;
+ }
+
+ public function hasTypeExtensions($name)
+ {
+ return isset($this->typeExtensionServiceIds[$name]);
+ }
+
+ public function getTypeGuesser()
+ {
+ if (!$this->guesserLoaded) {
+ $this->guesserLoaded = true;
+ $guessers = array();
+
+ foreach ($this->guesserServiceIds as $serviceId) {
+ $guessers[] = $this->container->get($serviceId);
+ }
+
+ if (count($guessers) > 0) {
+ $this->guesser = new FormTypeGuesserChain($guessers);
+ }
+ }
+
+ return $this->guesser;
+ }
+}
--- /dev/null
+<?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\Form\Extension\HttpFoundation\EventListener;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Pass the
+ * Request instance to {@link Form::process()} instead.
+ */
+class BindRequestListener implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ // High priority in order to supersede other listeners
+ return array(FormEvents::PRE_BIND => array('preBind', 128));
+ }
+
+ public function preBind(FormEvent $event)
+ {
+ $form = $event->getForm();
+
+ /* @var Request $request */
+ $request = $event->getData();
+
+ // Only proceed if we actually deal with a Request
+ if (!$request instanceof Request) {
+ return;
+ }
+
+ // Uncomment this as soon as the deprecation note should be shown
+ // trigger_error('Passing a Request instance to Form::submit() is deprecated since version 2.3 and will be disabled in 3.0. Call Form::process($request) instead.', E_USER_DEPRECATED);
+
+ $name = $form->getConfig()->getName();
+ $default = $form->getConfig()->getCompound() ? array() : null;
+
+ // Store the bound data in case of a post request
+ switch ($request->getMethod()) {
+ case 'POST':
+ case 'PUT':
+ case 'DELETE':
+ case 'PATCH':
+ if ('' === $name) {
+ // Form bound without name
+ $params = $request->request->all();
+ $files = $request->files->all();
+ } else {
+ $params = $request->request->get($name, $default);
+ $files = $request->files->get($name, $default);
+ }
+
+ if (is_array($params) && is_array($files)) {
+ $data = array_replace_recursive($params, $files);
+ } else {
+ $data = $params ?: $files;
+ }
+
+ break;
+
+ case 'GET':
+ $data = '' === $name
+ ? $request->query->all()
+ : $request->query->get($name, $default);
+
+ break;
+
+ default:
+ throw new LogicException(sprintf(
+ 'The request method "%s" is not supported',
+ $request->getMethod()
+ ));
+ }
+
+ $event->setData($data);
+ }
+}
--- /dev/null
+<?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\Form\Extension\HttpFoundation;
+
+use Symfony\Component\Form\AbstractExtension;
+
+/**
+ * Integrates the HttpFoundation component with the Form library.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class HttpFoundationExtension extends AbstractExtension
+{
+ protected function loadTypeExtensions()
+ {
+ return array(
+ new Type\FormTypeHttpFoundationExtension(),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Extension\HttpFoundation;
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\RequestHandlerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A request processor using the {@link Request} class of the HttpFoundation
+ * component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class HttpFoundationRequestHandler implements RequestHandlerInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function handleRequest(FormInterface $form, $request = null)
+ {
+ if (!$request instanceof Request) {
+ throw new UnexpectedTypeException($request, 'Symfony\Component\HttpFoundation\Request');
+ }
+
+ $name = $form->getName();
+ $method = $form->getConfig()->getMethod();
+
+ if ($method !== $request->getMethod()) {
+ return;
+ }
+
+ if ('GET' === $method) {
+ if ('' === $name) {
+ $data = $request->query->all();
+ } else {
+ // Don't submit GET requests if the form's name does not exist
+ // in the request
+ if (!$request->query->has($name)) {
+ return;
+ }
+
+ $data = $request->query->get($name);
+ }
+ } else {
+ if ('' === $name) {
+ $params = $request->request->all();
+ $files = $request->files->all();
+ } else {
+ $default = $form->getConfig()->getCompound() ? array() : null;
+ $params = $request->request->get($name, $default);
+ $files = $request->files->get($name, $default);
+ }
+
+ if (is_array($params) && is_array($files)) {
+ $data = array_replace_recursive($params, $files);
+ } else {
+ $data = $params ?: $files;
+ }
+ }
+
+ // Don't auto-submit the form unless at least one field is present.
+ if ('' === $name && count(array_intersect_key($data, $form->all())) <= 0) {
+ return;
+ }
+
+ $form->submit($data, 'PATCH' !== $method);
+ }
+}
--- /dev/null
+<?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\Form\Extension\HttpFoundation\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
+use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
+use Symfony\Component\Form\FormBuilderInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormTypeHttpFoundationExtension extends AbstractTypeExtension
+{
+ /**
+ * @var BindRequestListener
+ */
+ private $listener;
+
+ /**
+ * @var HttpFoundationRequestHandler
+ */
+ private $requestHandler;
+
+ public function __construct()
+ {
+ $this->listener = new BindRequestListener();
+ $this->requestHandler = new HttpFoundationRequestHandler();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addEventSubscriber($this->listener);
+ $builder->setRequestHandler($this->requestHandler);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtendedType()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Templating;
+
+use Symfony\Component\Form\AbstractExtension;
+use Symfony\Component\Form\FormRenderer;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+use Symfony\Component\Templating\PhpEngine;
+use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
+
+/**
+ * Integrates the Templating component with the Form library.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TemplatingExtension extends AbstractExtension
+{
+ public function __construct(PhpEngine $engine, CsrfProviderInterface $csrfProvider = null, array $defaultThemes = array())
+ {
+ $engine->addHelpers(array(
+ new FormHelper(new FormRenderer(new TemplatingRendererEngine($engine, $defaultThemes), $csrfProvider))
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Extension\Templating;
+
+use Symfony\Component\Form\AbstractRendererEngine;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Templating\EngineInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TemplatingRendererEngine extends AbstractRendererEngine
+{
+ /**
+ * @var EngineInterface
+ */
+ private $engine;
+
+ public function __construct(EngineInterface $engine, array $defaultThemes = array())
+ {
+ parent::__construct($defaultThemes);
+
+ $this->engine = $engine;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderBlock(FormView $view, $resource, $blockName, array $variables = array())
+ {
+ return trim($this->engine->render($resource, $variables));
+ }
+
+ /**
+ * Loads the cache with the resource for a given block name.
+ *
+ * This implementation tries to load as few blocks as possible, since each block
+ * is represented by a template on the file system.
+ *
+ * @see getResourceForBlock()
+ *
+ * @param string $cacheKey The cache key of the form view.
+ * @param FormView $view The form view for finding the applying themes.
+ * @param string $blockName The name of the block to load.
+ *
+ * @return Boolean True if the resource could be loaded, false otherwise.
+ */
+ protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName)
+ {
+ // Recursively try to find the block in the themes assigned to $view,
+ // then of its parent form, then of the parent form of the parent and so on.
+ // When the root form is reached in this recursion, also the default
+ // themes are taken into account.
+
+ // Check each theme whether it contains the searched block
+ if (isset($this->themes[$cacheKey])) {
+ for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
+ if ($this->loadResourceFromTheme($cacheKey, $blockName, $this->themes[$cacheKey][$i])) {
+ return true;
+ }
+ }
+ }
+
+ // Check the default themes once we reach the root form without success
+ if (!$view->parent) {
+ for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
+ if ($this->loadResourceFromTheme($cacheKey, $blockName, $this->defaultThemes[$i])) {
+ return true;
+ }
+ }
+ }
+
+ // If we did not find anything in the themes of the current view, proceed
+ // with the themes of the parent view
+ if ($view->parent) {
+ $parentCacheKey = $view->parent->vars[self::CACHE_KEY_VAR];
+
+ if (!isset($this->resources[$parentCacheKey][$blockName])) {
+ $this->loadResourceForBlockName($parentCacheKey, $view->parent, $blockName);
+ }
+
+ // If a template exists in the parent themes, cache that template
+ // for the current theme as well to speed up further accesses
+ if ($this->resources[$parentCacheKey][$blockName]) {
+ $this->resources[$cacheKey][$blockName] = $this->resources[$parentCacheKey][$blockName];
+
+ return true;
+ }
+ }
+
+ // Cache that we didn't find anything to speed up further accesses
+ $this->resources[$cacheKey][$blockName] = false;
+
+ return false;
+ }
+
+ /**
+ * Tries to load the resource for a block from a theme.
+ *
+ * @param string $cacheKey The cache key for storing the resource.
+ * @param string $blockName The name of the block to load a resource for.
+ * @param mixed $theme The theme to load the block from.
+ *
+ * @return Boolean True if the resource could be loaded, false otherwise.
+ */
+ protected function loadResourceFromTheme($cacheKey, $blockName, $theme)
+ {
+ if ($this->engine->exists($templateName = $theme.':'.$blockName.'.html.php')) {
+ $this->resources[$cacheKey][$blockName] = $templateName;
+
+ return true;
+ }
+
+ return false;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Constraints;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Form extends Constraint
+{
+ /**
+ * Violation code marking an invalid form.
+ */
+ const ERR_INVALID = 1;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTargets()
+ {
+ return self::CLASS_CONSTRAINT;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Constraints;
+
+use Symfony\Component\Form\ClickableInterface;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Extension\Validator\Util\ServerParams;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormValidator extends ConstraintValidator
+{
+ /**
+ * @var ServerParams
+ */
+ private $serverParams;
+
+ /**
+ * Creates a validator with the given server parameters.
+ *
+ * @param ServerParams $params The server parameters. Default
+ * parameters are created if null.
+ */
+ public function __construct(ServerParams $params = null)
+ {
+ $this->serverParams = $params ?: new ServerParams();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate($form, Constraint $constraint)
+ {
+ if (!$form instanceof FormInterface) {
+ return;
+ }
+
+ /* @var FormInterface $form */
+ $config = $form->getConfig();
+
+ if ($form->isSynchronized()) {
+ // Validate the form data only if transformation succeeded
+ $groups = self::getValidationGroups($form);
+
+ // Validate the data against its own constraints
+ if (self::allowDataWalking($form)) {
+ foreach ($groups as $group) {
+ $this->context->validate($form->getData(), 'data', $group, true);
+ }
+ }
+
+ // Validate the data against the constraints defined
+ // in the form
+ $constraints = $config->getOption('constraints');
+ foreach ($constraints as $constraint) {
+ foreach ($groups as $group) {
+ if (in_array($group, $constraint->groups)) {
+ $this->context->validateValue($form->getData(), $constraint, 'data', $group);
+
+ // Prevent duplicate validation
+ continue 2;
+ }
+ }
+ }
+ } else {
+ $childrenSynchronized = true;
+
+ foreach ($form as $child) {
+ if (!$child->isSynchronized()) {
+ $childrenSynchronized = false;
+ break;
+ }
+ }
+
+ // Mark the form with an error if it is not synchronized BUT all
+ // of its children are synchronized. If any child is not
+ // synchronized, an error is displayed there already and showing
+ // a second error in its parent form is pointless, or worse, may
+ // lead to duplicate errors if error bubbling is enabled on the
+ // child.
+ // See also https://github.com/symfony/symfony/issues/4359
+ if ($childrenSynchronized) {
+ $clientDataAsString = is_scalar($form->getViewData())
+ ? (string) $form->getViewData()
+ : gettype($form->getViewData());
+
+ $this->context->addViolation(
+ $config->getOption('invalid_message'),
+ array_replace(array('{{ value }}' => $clientDataAsString), $config->getOption('invalid_message_parameters')),
+ $form->getViewData(),
+ null,
+ Form::ERR_INVALID
+ );
+ }
+ }
+
+ // Mark the form with an error if it contains extra fields
+ if (count($form->getExtraData()) > 0) {
+ $this->context->addViolation(
+ $config->getOption('extra_fields_message'),
+ array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))),
+ $form->getExtraData()
+ );
+ }
+
+ // Mark the form with an error if the uploaded size was too large
+ $length = $this->serverParams->getContentLength();
+
+ if ($form->isRoot() && null !== $length) {
+ $max = $this->serverParams->getPostMaxSize();
+
+ if (!empty($max) && $length > $max) {
+ $this->context->addViolation(
+ $config->getOption('post_max_size_message'),
+ array('{{ max }}' => $this->serverParams->getNormalizedIniPostMaxSize()),
+ $length
+ );
+ }
+ }
+ }
+
+ /**
+ * Returns whether the data of a form may be walked.
+ *
+ * @param FormInterface $form The form to test.
+ *
+ * @return Boolean Whether the graph walker may walk the data.
+ */
+ private static function allowDataWalking(FormInterface $form)
+ {
+ $data = $form->getData();
+
+ // Scalar values cannot have mapped constraints
+ if (!is_object($data) && !is_array($data)) {
+ return false;
+ }
+
+ // Root forms are always validated
+ if ($form->isRoot()) {
+ return true;
+ }
+
+ // Non-root forms are validated if validation cascading
+ // is enabled in all ancestor forms
+ while (null !== ($form = $form->getParent())) {
+ if (!$form->getConfig()->getOption('cascade_validation')) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the validation groups of the given form.
+ *
+ * @param FormInterface $form The form.
+ *
+ * @return array The validation groups.
+ */
+ private static function getValidationGroups(FormInterface $form)
+ {
+ $button = self::findClickedButton($form->getRoot());
+
+ if (null !== $button) {
+ $groups = $button->getConfig()->getOption('validation_groups');
+
+ if (null !== $groups) {
+ return self::resolveValidationGroups($groups, $form);
+ }
+ }
+
+ do {
+ $groups = $form->getConfig()->getOption('validation_groups');
+
+ if (null !== $groups) {
+ return self::resolveValidationGroups($groups, $form);
+ }
+
+ $form = $form->getParent();
+ } while (null !== $form);
+
+ return array(Constraint::DEFAULT_GROUP);
+ }
+
+ /**
+ * Extracts a clicked button from a form tree, if one exists.
+ *
+ * @param FormInterface $form The root form.
+ *
+ * @return ClickableInterface|null The clicked button or null.
+ */
+ private static function findClickedButton(FormInterface $form)
+ {
+ if ($form instanceof ClickableInterface && $form->isClicked()) {
+ return $form;
+ }
+
+ foreach ($form as $child) {
+ if (null !== ($button = self::findClickedButton($child))) {
+ return $button;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Post-processes the validation groups option for a given form.
+ *
+ * @param array|callable $groups The validation groups.
+ * @param FormInterface $form The validated form.
+ *
+ * @return array The validation groups.
+ */
+ private static function resolveValidationGroups($groups, FormInterface $form)
+ {
+ if (!is_string($groups) && is_callable($groups)) {
+ $groups = call_user_func($groups, $form);
+ }
+
+ return (array) $groups;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\EventListener;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface;
+use Symfony\Component\Validator\ValidatorInterface;
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Validator\Constraints\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ValidationListener implements EventSubscriberInterface
+{
+ private $validator;
+
+ private $violationMapper;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents()
+ {
+ return array(FormEvents::POST_SUBMIT => 'validateForm');
+ }
+
+ public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper)
+ {
+ $this->validator = $validator;
+ $this->violationMapper = $violationMapper;
+ }
+
+ /**
+ * Validates the form and its domain object.
+ *
+ * @param FormEvent $event The event object
+ */
+ public function validateForm(FormEvent $event)
+ {
+ $form = $event->getForm();
+
+ if ($form->isRoot()) {
+ // Validate the form in group "Default"
+ $violations = $this->validator->validate($form);
+
+ if (count($violations) > 0) {
+ foreach ($violations as $violation) {
+ // Allow the "invalid" constraint to be put onto
+ // non-synchronized forms
+ $allowNonSynchronized = Form::ERR_INVALID === $violation->getCode();
+
+ $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * Encapsulates common logic of {@link FormTypeValidatorExtension} and
+ * {@link SubmitTypeValidatorExtension}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class BaseValidatorExtension extends AbstractTypeExtension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ // Make sure that validation groups end up as null, closure or array
+ $validationGroupsNormalizer = function (Options $options, $groups) {
+ if (false === $groups) {
+ return array();
+ }
+
+ if (empty($groups)) {
+ return null;
+ }
+
+ if (is_callable($groups)) {
+ return $groups;
+ }
+
+ return (array) $groups;
+ };
+
+ $resolver->setDefaults(array(
+ 'validation_groups' => null,
+ ));
+
+ $resolver->setNormalizers(array(
+ 'validation_groups' => $validationGroupsNormalizer,
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
+use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
+use Symfony\Component\Validator\ValidatorInterface;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormTypeValidatorExtension extends BaseValidatorExtension
+{
+ /**
+ * @var ValidatorInterface
+ */
+ private $validator;
+
+ /**
+ * @var ViolationMapper
+ */
+ private $violationMapper;
+
+ public function __construct(ValidatorInterface $validator)
+ {
+ $this->validator = $validator;
+ $this->violationMapper = new ViolationMapper();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ parent::setDefaultOptions($resolver);
+
+ // Constraint should always be converted to an array
+ $constraintsNormalizer = function (Options $options, $constraints) {
+ return is_object($constraints) ? array($constraints) : (array) $constraints;
+ };
+
+ $resolver->setDefaults(array(
+ 'error_mapping' => array(),
+ 'constraints' => array(),
+ 'cascade_validation' => false,
+ 'invalid_message' => 'This value is not valid.',
+ 'invalid_message_parameters' => array(),
+ 'extra_fields_message' => 'This form should not contain extra fields.',
+ 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
+ ));
+
+ $resolver->setNormalizers(array(
+ 'constraints' => $constraintsNormalizer,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtendedType()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RepeatedTypeValidatorExtension extends AbstractTypeExtension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ // Map errors to the first field
+ $errorMapping = function (Options $options) {
+ return array('.' => $options['first_name']);
+ };
+
+ $resolver->setDefaults(array(
+ 'error_mapping' => $errorMapping,
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtendedType()
+ {
+ return 'repeated';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Type;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SubmitTypeValidatorExtension extends AbstractTypeExtension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtendedType()
+ {
+ return 'submit';
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\Util;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ServerParams
+{
+ /**
+ * Returns maximum post size in bytes.
+ *
+ * @return null|integer The maximum post size in bytes
+ */
+ public function getPostMaxSize()
+ {
+ $iniMax = $this->getNormalizedIniPostMaxSize();
+
+ if ('' === $iniMax) {
+ return null;
+ }
+
+ if (preg_match('#^\+?(0X?)?(.*?)([KMG]?)$#', $iniMax, $match)) {
+ $shifts = array('' => 0, 'K' => 10, 'M' => 20, 'G' => 30);
+ $bases = array('' => 10, '0' => 8, '0X' => 16);
+
+ return intval($match[2], $bases[$match[1]]) << $shifts[$match[3]];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the normalized "post_max_size" ini setting.
+ *
+ * @return string
+ */
+ public function getNormalizedIniPostMaxSize()
+ {
+ return strtoupper(trim(ini_get('post_max_size')));
+ }
+
+ /**
+ * Returns the content length of the request.
+ *
+ * @return mixed The request content length.
+ */
+ public function getContentLength()
+ {
+ return isset($_SERVER['CONTENT_LENGTH'])
+ ? (int) $_SERVER['CONTENT_LENGTH']
+ : null;
+ }
+
+}
--- /dev/null
+<?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\Form\Extension\Validator;
+
+use Symfony\Component\Form\Extension\Validator\Type;
+use Symfony\Component\Form\Extension\Validator\Constraints\Form;
+use Symfony\Component\Form\AbstractExtension;
+use Symfony\Component\Validator\ValidatorInterface;
+use Symfony\Component\Validator\Constraints\Valid;
+
+/**
+ * Extension supporting the Symfony2 Validator component in forms.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ValidatorExtension extends AbstractExtension
+{
+ private $validator;
+
+ public function __construct(ValidatorInterface $validator)
+ {
+ $this->validator = $validator;
+
+ // Register the form constraints in the validator programmatically.
+ // This functionality is required when using the Form component without
+ // the DIC, where the XML file is loaded automatically. Thus the following
+ // code must be kept synchronized with validation.xml
+
+ /** @var \Symfony\Component\Validator\Mapping\ClassMetadata $metadata */
+ $metadata = $this->validator->getMetadataFactory()->getMetadataFor('Symfony\Component\Form\Form');
+ $metadata->addConstraint(new Form());
+ $metadata->addPropertyConstraint('children', new Valid());
+ }
+
+ public function loadTypeGuesser()
+ {
+ return new ValidatorTypeGuesser($this->validator->getMetadataFactory());
+ }
+
+ protected function loadTypeExtensions()
+ {
+ return array(
+ new Type\FormTypeValidatorExtension($this->validator),
+ new Type\RepeatedTypeValidatorExtension(),
+ new Type\SubmitTypeValidatorExtension(),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator;
+
+use Symfony\Component\Form\FormTypeGuesserInterface;
+use Symfony\Component\Form\Guess\Guess;
+use Symfony\Component\Form\Guess\TypeGuess;
+use Symfony\Component\Form\Guess\ValueGuess;
+use Symfony\Component\Validator\MetadataFactoryInterface;
+use Symfony\Component\Validator\Constraint;
+
+class ValidatorTypeGuesser implements FormTypeGuesserInterface
+{
+ private $metadataFactory;
+
+ public function __construct(MetadataFactoryInterface $metadataFactory)
+ {
+ $this->metadataFactory = $metadataFactory;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessType($class, $property)
+ {
+ $guesser = $this;
+
+ return $this->guess($class, $property, function (Constraint $constraint) use ($guesser) {
+ return $guesser->guessTypeForConstraint($constraint);
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessRequired($class, $property)
+ {
+ $guesser = $this;
+
+ return $this->guess($class, $property, function (Constraint $constraint) use ($guesser) {
+ return $guesser->guessRequiredForConstraint($constraint);
+ // If we don't find any constraint telling otherwise, we can assume
+ // that a field is not required (with LOW_CONFIDENCE)
+ }, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessMaxLength($class, $property)
+ {
+ $guesser = $this;
+
+ return $this->guess($class, $property, function (Constraint $constraint) use ($guesser) {
+ return $guesser->guessMaxLengthForConstraint($constraint);
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessPattern($class, $property)
+ {
+ $guesser = $this;
+
+ return $this->guess($class, $property, function (Constraint $constraint) use ($guesser) {
+ return $guesser->guessPatternForConstraint($constraint);
+ });
+ }
+
+ /**
+ * Guesses a field class name for a given constraint
+ *
+ * @param Constraint $constraint The constraint to guess for
+ *
+ * @return TypeGuess The guessed field class and options
+ */
+ public function guessTypeForConstraint(Constraint $constraint)
+ {
+ switch (get_class($constraint)) {
+ case 'Symfony\Component\Validator\Constraints\Type':
+ switch ($constraint->type) {
+ case 'array':
+ return new TypeGuess('collection', array(), Guess::MEDIUM_CONFIDENCE);
+ case 'boolean':
+ case 'bool':
+ return new TypeGuess('checkbox', array(), Guess::MEDIUM_CONFIDENCE);
+
+ case 'double':
+ case 'float':
+ case 'numeric':
+ case 'real':
+ return new TypeGuess('number', array(), Guess::MEDIUM_CONFIDENCE);
+
+ case 'integer':
+ case 'int':
+ case 'long':
+ return new TypeGuess('integer', array(), Guess::MEDIUM_CONFIDENCE);
+
+ case '\DateTime':
+ return new TypeGuess('date', array(), Guess::MEDIUM_CONFIDENCE);
+
+ case 'string':
+ return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE);
+ }
+ break;
+
+ case 'Symfony\Component\Validator\Constraints\Country':
+ return new TypeGuess('country', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Date':
+ return new TypeGuess('date', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\DateTime':
+ return new TypeGuess('datetime', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Email':
+ return new TypeGuess('email', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\File':
+ case 'Symfony\Component\Validator\Constraints\Image':
+ return new TypeGuess('file', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Language':
+ return new TypeGuess('language', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Locale':
+ return new TypeGuess('locale', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Time':
+ return new TypeGuess('time', array('input' => 'string'), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Url':
+ return new TypeGuess('url', array(), Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Ip':
+ return new TypeGuess('text', array(), Guess::MEDIUM_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\MaxLength':
+ case 'Symfony\Component\Validator\Constraints\MinLength':
+ case 'Symfony\Component\Validator\Constraints\Regex':
+ return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Min':
+ case 'Symfony\Component\Validator\Constraints\Max':
+ return new TypeGuess('number', array(), Guess::LOW_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\MinCount':
+ case 'Symfony\Component\Validator\Constraints\MaxCount':
+ return new TypeGuess('collection', array(), Guess::LOW_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\True':
+ case 'Symfony\Component\Validator\Constraints\False':
+ return new TypeGuess('checkbox', array(), Guess::MEDIUM_CONFIDENCE);
+ }
+
+ return null;
+ }
+
+ /**
+ * Guesses whether a field is required based on the given constraint
+ *
+ * @param Constraint $constraint The constraint to guess for
+ *
+ * @return Guess The guess whether the field is required
+ */
+ public function guessRequiredForConstraint(Constraint $constraint)
+ {
+ switch (get_class($constraint)) {
+ case 'Symfony\Component\Validator\Constraints\NotNull':
+ case 'Symfony\Component\Validator\Constraints\NotBlank':
+ case 'Symfony\Component\Validator\Constraints\True':
+ return new ValueGuess(true, Guess::HIGH_CONFIDENCE);
+ }
+
+ return null;
+ }
+
+ /**
+ * Guesses a field's maximum length based on the given constraint
+ *
+ * @param Constraint $constraint The constraint to guess for
+ *
+ * @return Guess The guess for the maximum length
+ */
+ public function guessMaxLengthForConstraint(Constraint $constraint)
+ {
+ switch (get_class($constraint)) {
+ case 'Symfony\Component\Validator\Constraints\MaxLength':
+ return new ValueGuess($constraint->limit, Guess::HIGH_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Type':
+ if (in_array($constraint->type, array('double', 'float', 'numeric', 'real'))) {
+ return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
+ }
+ break;
+
+ case 'Symfony\Component\Validator\Constraints\Max':
+ return new ValueGuess(strlen((string) $constraint->limit), Guess::LOW_CONFIDENCE);
+ }
+
+ return null;
+ }
+
+ /**
+ * Guesses a field's pattern based on the given constraint
+ *
+ * @param Constraint $constraint The constraint to guess for
+ *
+ * @return Guess The guess for the pattern
+ */
+ public function guessPatternForConstraint(Constraint $constraint)
+ {
+ switch (get_class($constraint)) {
+ case 'Symfony\Component\Validator\Constraints\MinLength':
+ return new ValueGuess(sprintf('.{%s,}', (string) $constraint->limit), Guess::LOW_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Regex':
+ $htmlPattern = $constraint->getHtmlPattern();
+
+ if (null !== $htmlPattern) {
+ return new ValueGuess($htmlPattern, Guess::HIGH_CONFIDENCE);
+ }
+ break;
+
+ case 'Symfony\Component\Validator\Constraints\Min':
+ return new ValueGuess(sprintf('.{%s,}', strlen((string) $constraint->limit)), Guess::LOW_CONFIDENCE);
+
+ case 'Symfony\Component\Validator\Constraints\Type':
+ if (in_array($constraint->type, array('double', 'float', 'numeric', 'real'))) {
+ return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE);
+ }
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Iterates over the constraints of a property, executes a constraints on
+ * them and returns the best guess
+ *
+ * @param string $class The class to read the constraints from
+ * @param string $property The property for which to find constraints
+ * @param \Closure $closure The closure that returns a guess
+ * for a given constraint
+ * @param mixed $defaultValue The default value assumed if no other value
+ * can be guessed.
+ *
+ * @return Guess The guessed value with the highest confidence
+ */
+ protected function guess($class, $property, \Closure $closure, $defaultValue = null)
+ {
+ $guesses = array();
+ $classMetadata = $this->metadataFactory->getMetadataFor($class);
+
+ if ($classMetadata->hasMemberMetadatas($property)) {
+ $memberMetadatas = $classMetadata->getMemberMetadatas($property);
+
+ foreach ($memberMetadatas as $memberMetadata) {
+ $constraints = $memberMetadata->getConstraints();
+
+ foreach ($constraints as $constraint) {
+ if ($guess = $closure($constraint)) {
+ $guesses[] = $guess;
+ }
+ }
+ }
+
+ if (null !== $defaultValue) {
+ $guesses[] = new ValueGuess($defaultValue, Guess::LOW_CONFIDENCE);
+ }
+ }
+
+ return Guess::getBestGuess($guesses);
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Exception\ErrorMappingException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class MappingRule
+{
+ /**
+ * @var FormInterface
+ */
+ private $origin;
+
+ /**
+ * @var string
+ */
+ private $propertyPath;
+
+ /**
+ * @var string
+ */
+ private $targetPath;
+
+ public function __construct(FormInterface $origin, $propertyPath, $targetPath)
+ {
+ $this->origin = $origin;
+ $this->propertyPath = $propertyPath;
+ $this->targetPath = $targetPath;
+ }
+
+ /**
+ * @return FormInterface
+ */
+ public function getOrigin()
+ {
+ return $this->origin;
+ }
+
+ /**
+ * Matches a property path against the rule path.
+ *
+ * If the rule matches, the form mapped by the rule is returned.
+ * Otherwise this method returns false.
+ *
+ * @param string $propertyPath The property path to match against the rule.
+ *
+ * @return null|FormInterface The mapped form or null.
+ */
+ public function match($propertyPath)
+ {
+ if ($propertyPath === (string) $this->propertyPath) {
+ return $this->getTarget();
+ }
+
+ return null;
+ }
+
+ /**
+ * Matches a property path against a prefix of the rule path.
+ *
+ * @param string $propertyPath The property path to match against the rule.
+ *
+ * @return Boolean Whether the property path is a prefix of the rule or not.
+ */
+ public function isPrefix($propertyPath)
+ {
+ $length = strlen($propertyPath);
+ $prefix = substr($this->propertyPath, 0, $length);
+ $next = isset($this->propertyPath[$length]) ? $this->propertyPath[$length] : null;
+
+ return $prefix === $propertyPath && ('[' === $next || '.' === $next);
+ }
+
+ /**
+ * @return FormInterface
+ *
+ * @throws ErrorMappingException
+ */
+ public function getTarget()
+ {
+ $childNames = explode('.', $this->targetPath);
+ $target = $this->origin;
+
+ foreach ($childNames as $childName) {
+ if (!$target->has($childName)) {
+ throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
+ }
+ $target = $target->get($childName);
+ }
+
+ return $target;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\PropertyAccess\PropertyPath;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RelativePath extends PropertyPath
+{
+ /**
+ * @var FormInterface
+ */
+ private $root;
+
+ /**
+ * @param FormInterface $root
+ * @param string $propertyPath
+ */
+ public function __construct(FormInterface $root, $propertyPath)
+ {
+ parent::__construct($propertyPath);
+
+ $this->root = $root;
+ }
+
+ /**
+ * @return FormInterface
+ */
+ public function getRoot()
+ {
+ return $this->root;
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Util\InheritDataAwareIterator;
+use Symfony\Component\PropertyAccess\PropertyPathIterator;
+use Symfony\Component\PropertyAccess\PropertyPathBuilder;
+use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface;
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationMapper implements ViolationMapperInterface
+{
+ /**
+ * @var Boolean
+ */
+ private $allowNonSynchronized;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false)
+ {
+ $this->allowNonSynchronized = $allowNonSynchronized;
+
+ // The scope is the currently found most specific form that
+ // an error should be mapped to. After setting the scope, the
+ // mapper will try to continue to find more specific matches in
+ // the children of scope. If it cannot, the error will be
+ // mapped to this scope.
+ $scope = null;
+
+ $violationPath = null;
+ $relativePath = null;
+ $match = false;
+
+ // Don't create a ViolationPath instance for empty property paths
+ if (strlen($violation->getPropertyPath()) > 0) {
+ $violationPath = new ViolationPath($violation->getPropertyPath());
+ $relativePath = $this->reconstructPath($violationPath, $form);
+ }
+
+ // This case happens if the violation path is empty and thus
+ // the violation should be mapped to the root form
+ if (null === $violationPath) {
+ $scope = $form;
+ }
+
+ // In general, mapping happens from the root form to the leaf forms
+ // First, the rules of the root form are applied to determine
+ // the subsequent descendant. The rules of this descendant are then
+ // applied to find the next and so on, until we have found the
+ // most specific form that matches the violation.
+
+ // If any of the forms found in this process is not synchronized,
+ // mapping is aborted. Non-synchronized forms could not reverse
+ // transform the value entered by the user, thus any further violations
+ // caused by the (invalid) reverse transformed value should be
+ // ignored.
+
+ if (null !== $relativePath) {
+ // Set the scope to the root of the relative path
+ // This root will usually be $form. If the path contains
+ // an unmapped form though, the last unmapped form found
+ // will be the root of the path.
+ $scope = $relativePath->getRoot();
+ $it = new PropertyPathIterator($relativePath);
+
+ while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
+ $scope = $child;
+ $it->next();
+ $match = true;
+ }
+ }
+
+ // This case happens if an error happened in the data under a
+ // form inheriting its parent data that does not match any of the
+ // children of that form.
+ if (null !== $violationPath && !$match) {
+ // If we could not map the error to anything more specific
+ // than the root element, map it to the innermost directly
+ // mapped form of the violation path
+ // e.g. "children[foo].children[bar].data.baz"
+ // Here the innermost directly mapped child is "bar"
+
+ $scope = $form;
+ $it = new ViolationPathIterator($violationPath);
+
+ // Note: acceptsErrors() will always return true for forms inheriting
+ // their parent data, because these forms can never be non-synchronized
+ // (they don't do any data transformation on their own)
+ while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
+ if (!$scope->has($it->current())) {
+ // Break if we find a reference to a non-existing child
+ break;
+ }
+
+ $scope = $scope->get($it->current());
+ $it->next();
+ }
+ }
+
+ // Follow dot rules until we have the final target
+ $mapping = $scope->getConfig()->getOption('error_mapping');
+
+ while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
+ $dotRule = new MappingRule($scope, '.', $mapping['.']);
+ $scope = $dotRule->getTarget();
+ $mapping = $scope->getConfig()->getOption('error_mapping');
+ }
+
+ // Only add the error if the form is synchronized
+ if ($this->acceptsErrors($scope)) {
+ $scope->addError(new FormError(
+ $violation->getMessage(),
+ $violation->getMessageTemplate(),
+ $violation->getMessageParameters(),
+ $violation->getMessagePluralization()
+ ));
+ }
+ }
+
+ /**
+ * Tries to match the beginning of the property path at the
+ * current position against the children of the scope.
+ *
+ * If a matching child is found, it is returned. Otherwise
+ * null is returned.
+ *
+ * @param FormInterface $form The form to search.
+ * @param PropertyPathIteratorInterface $it The iterator at its current position.
+ *
+ * @return null|FormInterface The found match or null.
+ */
+ private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it)
+ {
+ // Remember at what property path underneath "data"
+ // we are looking. Check if there is a child with that
+ // path, otherwise increase path by one more piece
+ $chunk = '';
+ $foundChild = null;
+ $foundAtIndex = 0;
+
+ // Construct mapping rules for the given form
+ $rules = array();
+
+ foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
+ // Dot rules are considered at the very end
+ if ('.' !== $propertyPath) {
+ $rules[] = new MappingRule($form, $propertyPath, $targetPath);
+ }
+ }
+
+ // Skip forms inheriting their parent data when iterating the children
+ $childIterator = new \RecursiveIteratorIterator(
+ new InheritDataAwareIterator($form->all())
+ );
+
+ // Make the path longer until we find a matching child
+ while (true) {
+ if (!$it->valid()) {
+ return null;
+ }
+
+ if ($it->isIndex()) {
+ $chunk .= '['.$it->current().']';
+ } else {
+ $chunk .= ('' === $chunk ? '' : '.').$it->current();
+ }
+
+ // Test mapping rules as long as we have any
+ foreach ($rules as $key => $rule) {
+ /* @var MappingRule $rule */
+
+ // Mapping rule matches completely, terminate.
+ if (null !== ($form = $rule->match($chunk))) {
+ return $form;
+ }
+
+ // Keep only rules that have $chunk as prefix
+ if (!$rule->isPrefix($chunk)) {
+ unset($rules[$key]);
+ }
+ }
+
+ // Test children unless we already found one
+ if (null === $foundChild) {
+ foreach ($childIterator as $child) {
+ /* @var FormInterface $child */
+ $childPath = (string) $child->getPropertyPath();
+
+ // Child found, mark as return value
+ if ($chunk === $childPath) {
+ $foundChild = $child;
+ $foundAtIndex = $it->key();
+ }
+ }
+ }
+
+ // Add element to the chunk
+ $it->next();
+
+ // If we reached the end of the path or if there are no
+ // more matching mapping rules, return the found child
+ if (null !== $foundChild && (!$it->valid() || count($rules) === 0)) {
+ // Reset index in case we tried to find mapping
+ // rules further down the path
+ $it->seek($foundAtIndex);
+
+ return $foundChild;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reconstructs a property path from a violation path and a form tree.
+ *
+ * @param ViolationPath $violationPath The violation path.
+ * @param FormInterface $origin The root form of the tree.
+ *
+ * @return RelativePath The reconstructed path.
+ */
+ private function reconstructPath(ViolationPath $violationPath, FormInterface $origin)
+ {
+ $propertyPathBuilder = new PropertyPathBuilder($violationPath);
+ $it = $violationPath->getIterator();
+ $scope = $origin;
+
+ // Remember the current index in the builder
+ $i = 0;
+
+ // Expand elements that map to a form (like "children[address]")
+ for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
+ if (!$scope->has($it->current())) {
+ // Scope relates to a form that does not exist
+ // Bail out
+ break;
+ }
+
+ // Process child form
+ $scope = $scope->get($it->current());
+
+ if ($scope->getConfig()->getInheritData()) {
+ // Form inherits its parent data
+ // Cut the piece out of the property path and proceed
+ $propertyPathBuilder->remove($i);
+ } elseif (!$scope->getConfig()->getMapped()) {
+ // Form is not mapped
+ // Set the form as new origin and strip everything
+ // we have so far in the path
+ $origin = $scope;
+ $propertyPathBuilder->remove(0, $i + 1);
+ $i = 0;
+ } else {
+ /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */
+ $propertyPath = $scope->getPropertyPath();
+
+ if (null === $propertyPath) {
+ // Property path of a mapped form is null
+ // Should not happen, bail out
+ break;
+ }
+
+ $propertyPathBuilder->replace($i, 1, $propertyPath);
+ $i += $propertyPath->getLength();
+ }
+ }
+
+ $finalPath = $propertyPathBuilder->getPropertyPath();
+
+ return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
+ }
+
+ /**
+ * @param FormInterface $form
+ *
+ * @return Boolean
+ */
+ private function acceptsErrors(FormInterface $form)
+ {
+ return $this->allowNonSynchronized || $form->isSynchronized();
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ViolationMapperInterface
+{
+ /**
+ * Maps a constraint violation to a form in the form tree under
+ * the given form.
+ *
+ * @param ConstraintViolation $violation The violation to map.
+ * @param FormInterface $form The root form of the tree
+ * to map it to.
+ * @param Boolean $allowNonSynchronized Whether to allow
+ * mapping to non-synchronized forms.
+ */
+ public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false);
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\Exception\OutOfBoundsException;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\PropertyAccess\PropertyPathInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPath implements \IteratorAggregate, PropertyPathInterface
+{
+ /**
+ * @var array
+ */
+ private $elements = array();
+
+ /**
+ * @var array
+ */
+ private $isIndex = array();
+
+ /**
+ * @var array
+ */
+ private $mapsForm = array();
+
+ /**
+ * @var string
+ */
+ private $pathAsString = '';
+
+ /**
+ * @var integer
+ */
+ private $length = 0;
+
+ /**
+ * Creates a new violation path from a string.
+ *
+ * @param string $violationPath The property path of a {@link ConstraintViolation}
+ * object.
+ */
+ public function __construct($violationPath)
+ {
+ $path = new PropertyPath($violationPath);
+ $elements = $path->getElements();
+ $data = false;
+
+ for ($i = 0, $l = count($elements); $i < $l; ++$i) {
+ if (!$data) {
+ // The element "data" has not yet been passed
+ if ('children' === $elements[$i] && $path->isProperty($i)) {
+ // Skip element "children"
+ ++$i;
+
+ // Next element must exist and must be an index
+ // Otherwise consider this the end of the path
+ if ($i >= $l || !$path->isIndex($i)) {
+ break;
+ }
+
+ $this->elements[] = $elements[$i];
+ $this->isIndex[] = true;
+ $this->mapsForm[] = true;
+ } elseif ('data' === $elements[$i] && $path->isProperty($i)) {
+ // Skip element "data"
+ ++$i;
+
+ // End of path
+ if ($i >= $l) {
+ break;
+ }
+
+ $this->elements[] = $elements[$i];
+ $this->isIndex[] = $path->isIndex($i);
+ $this->mapsForm[] = false;
+ $data = true;
+ } else {
+ // Neither "children" nor "data" property found
+ // Consider this the end of the path
+ break;
+ }
+ } else {
+ // Already after the "data" element
+ // Pick everything as is
+ $this->elements[] = $elements[$i];
+ $this->isIndex[] = $path->isIndex($i);
+ $this->mapsForm[] = false;
+ }
+ }
+
+ $this->length = count($this->elements);
+
+ $this->buildString();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->pathAsString;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLength()
+ {
+ return $this->length;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ if ($this->length <= 1) {
+ return null;
+ }
+
+ $parent = clone $this;
+
+ --$parent->length;
+ array_pop($parent->elements);
+ array_pop($parent->isIndex);
+ array_pop($parent->mapsForm);
+
+ $parent->buildString();
+
+ return $parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElements()
+ {
+ return $this->elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElement($index)
+ {
+ if (!isset($this->elements[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
+ }
+
+ return $this->elements[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isProperty($index)
+ {
+ if (!isset($this->isIndex[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
+ }
+
+ return !$this->isIndex[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isIndex($index)
+ {
+ if (!isset($this->isIndex[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
+ }
+
+ return $this->isIndex[$index];
+ }
+
+ /**
+ * Returns whether an element maps directly to a form.
+ *
+ * Consider the following violation path:
+ *
+ * <code>
+ * children[address].children[office].data.street
+ * </code>
+ *
+ * In this example, "address" and "office" map to forms, while
+ * "street does not.
+ *
+ * @param integer $index The element index.
+ *
+ * @return Boolean Whether the element maps to a form.
+ *
+ * @throws OutOfBoundsException If the offset is invalid.
+ */
+ public function mapsForm($index)
+ {
+ if (!isset($this->mapsForm[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the violation path', $index));
+ }
+
+ return $this->mapsForm[$index];
+ }
+
+ /**
+ * Returns a new iterator for this path
+ *
+ * @return ViolationPathIterator
+ */
+ public function getIterator()
+ {
+ return new ViolationPathIterator($this);
+ }
+
+ /**
+ * Builds the string representation from the elements.
+ */
+ private function buildString()
+ {
+ $this->pathAsString = '';
+ $data = false;
+
+ foreach ($this->elements as $index => $element) {
+ if ($this->mapsForm[$index]) {
+ $this->pathAsString .= ".children[$element]";
+ } elseif (!$data) {
+ $this->pathAsString .= '.data'.($this->isIndex[$index] ? "[$element]" : ".$element");
+ $data = true;
+ } else {
+ $this->pathAsString .= $this->isIndex[$index] ? "[$element]" : ".$element";
+ }
+ }
+
+ if ('' !== $this->pathAsString) {
+ // remove leading dot
+ $this->pathAsString = substr($this->pathAsString, 1);
+ }
+ }
+}
--- /dev/null
+<?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\Form\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\PropertyAccess\PropertyPathIterator;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPathIterator extends PropertyPathIterator
+{
+ public function __construct(ViolationPath $violationPath)
+ {
+ parent::__construct($violationPath);
+ }
+
+ public function mapsForm()
+ {
+ return $this->path->mapsForm($this->key());
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\RuntimeException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Exception\AlreadySubmittedException;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\Form\Exception\OutOfBoundsException;
+use Symfony\Component\Form\Util\FormUtil;
+use Symfony\Component\Form\Util\InheritDataAwareIterator;
+use Symfony\Component\PropertyAccess\PropertyPath;
+
+/**
+ * Form represents a form.
+ *
+ * To implement your own form fields, you need to have a thorough understanding
+ * of the data flow within a form. A form stores its data in three different
+ * representations:
+ *
+ * (1) the "model" format required by the form's object
+ * (2) the "normalized" format for internal processing
+ * (3) the "view" format used for display
+ *
+ * A date field, for example, may store a date as "Y-m-d" string (1) in the
+ * object. To facilitate processing in the field, this value is normalized
+ * to a DateTime object (2). In the HTML representation of your form, a
+ * localized string (3) is presented to and modified by the user.
+ *
+ * In most cases, format (1) and format (2) will be the same. For example,
+ * a checkbox field uses a Boolean value for both internal processing and
+ * storage in the object. In these cases you simply need to set a value
+ * transformer to convert between formats (2) and (3). You can do this by
+ * calling addViewTransformer().
+ *
+ * In some cases though it makes sense to make format (1) configurable. To
+ * demonstrate this, let's extend our above date field to store the value
+ * either as "Y-m-d" string or as timestamp. Internally we still want to
+ * use a DateTime object for processing. To convert the data from string/integer
+ * to DateTime you can set a normalization transformer by calling
+ * addNormTransformer(). The normalized data is then converted to the displayed
+ * data as described before.
+ *
+ * The conversions (1) -> (2) -> (3) use the transform methods of the transformers.
+ * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Form implements \IteratorAggregate, FormInterface
+{
+ /**
+ * The form's configuration
+ * @var FormConfigInterface
+ */
+ private $config;
+
+ /**
+ * The parent of this form
+ * @var FormInterface
+ */
+ private $parent;
+
+ /**
+ * The children of this form
+ * @var FormInterface[] An array of FormInterface instances
+ */
+ private $children = array();
+
+ /**
+ * The errors of this form
+ * @var FormError[] An array of FormError instances
+ */
+ private $errors = array();
+
+ /**
+ * Whether this form was submitted
+ * @var Boolean
+ */
+ private $submitted = false;
+
+ /**
+ * The form data in model format
+ * @var mixed
+ */
+ private $modelData;
+
+ /**
+ * The form data in normalized format
+ * @var mixed
+ */
+ private $normData;
+
+ /**
+ * The form data in view format
+ * @var mixed
+ */
+ private $viewData;
+
+ /**
+ * The submitted values that don't belong to any children
+ * @var array
+ */
+ private $extraData = array();
+
+ /**
+ * Whether the data in model, normalized and view format is
+ * synchronized. Data may not be synchronized if transformation errors
+ * occur.
+ * @var Boolean
+ */
+ private $synchronized = true;
+
+ /**
+ * Whether the form's data has been initialized.
+ *
+ * When the data is initialized with its default value, that default value
+ * is passed through the transformer chain in order to synchronize the
+ * model, normalized and view format for the first time. This is done
+ * lazily in order to save performance when {@link setData()} is called
+ * manually, making the initialization with the configured default value
+ * superfluous.
+ *
+ * @var Boolean
+ */
+ private $defaultDataSet = false;
+
+ /**
+ * Whether setData() is currently being called.
+ * @var Boolean
+ */
+ private $lockSetData = false;
+
+ /**
+ * Creates a new form based on the given configuration.
+ *
+ * @param FormConfigInterface $config The form configuration.
+ *
+ * @throws LogicException if a data mapper is not provided for a compound form
+ */
+ public function __construct(FormConfigInterface $config)
+ {
+ // Compound forms always need a data mapper, otherwise calls to
+ // `setData` and `add` will not lead to the correct population of
+ // the child forms.
+ if ($config->getCompound() && !$config->getDataMapper()) {
+ throw new LogicException('Compound forms need a data mapper');
+ }
+
+ // If the form inherits the data from its parent, it is not necessary
+ // to call setData() with the default data.
+ if ($config->getInheritData()) {
+ $this->defaultDataSet = true;
+ }
+
+ $this->config = $config;
+ }
+
+ public function __clone()
+ {
+ foreach ($this->children as $key => $child) {
+ $this->children[$key] = clone $child;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->config->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyPath()
+ {
+ if (null !== $this->config->getPropertyPath()) {
+ return $this->config->getPropertyPath();
+ }
+
+ if (null === $this->getName() || '' === $this->getName()) {
+ return null;
+ }
+
+ $parent = $this->parent;
+
+ while ($parent && $parent->getConfig()->getInheritData()) {
+ $parent = $parent->getParent();
+ }
+
+ if ($parent && null === $parent->getConfig()->getDataClass()) {
+ return new PropertyPath('['.$this->getName().']');
+ }
+
+ return new PropertyPath($this->getName());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired()
+ {
+ if (null === $this->parent || $this->parent->isRequired()) {
+ return $this->config->getRequired();
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isDisabled()
+ {
+ if (null === $this->parent || !$this->parent->isDisabled()) {
+ return $this->config->getDisabled();
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setParent(FormInterface $parent = null)
+ {
+ if ($this->submitted) {
+ throw new AlreadySubmittedException('You cannot set the parent of a submitted form');
+ }
+
+ if (null !== $parent && '' === $this->config->getName()) {
+ throw new LogicException('A form with an empty name cannot have a parent form.');
+ }
+
+ $this->parent = $parent;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoot()
+ {
+ return $this->parent ? $this->parent->getRoot() : $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRoot()
+ {
+ return null === $this->parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setData($modelData)
+ {
+ // If the form is submitted while disabled, it is set to submitted, but the data is not
+ // changed. In such cases (i.e. when the form is not initialized yet) don't
+ // abort this method.
+ if ($this->submitted && $this->defaultDataSet) {
+ throw new AlreadySubmittedException('You cannot change the data of a submitted form.');
+ }
+
+ // If the form inherits its parent's data, disallow data setting to
+ // prevent merge conflicts
+ if ($this->config->getInheritData()) {
+ throw new RuntimeException('You cannot change the data of a form inheriting its parent data.');
+ }
+
+ // Don't allow modifications of the configured data if the data is locked
+ if ($this->config->getDataLocked() && $modelData !== $this->config->getData()) {
+ return $this;
+ }
+
+ if (is_object($modelData) && !$this->config->getByReference()) {
+ $modelData = clone $modelData;
+ }
+
+ if ($this->lockSetData) {
+ throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.');
+ }
+
+ $this->lockSetData = true;
+ $dispatcher = $this->config->getEventDispatcher();
+
+ // Hook to change content of the data
+ if ($dispatcher->hasListeners(FormEvents::PRE_SET_DATA)) {
+ $event = new FormEvent($this, $modelData);
+ $dispatcher->dispatch(FormEvents::PRE_SET_DATA, $event);
+ $modelData = $event->getData();
+ }
+
+ // Treat data as strings unless a value transformer exists
+ if (!$this->config->getViewTransformers() && !$this->config->getModelTransformers() && is_scalar($modelData)) {
+ $modelData = (string) $modelData;
+ }
+
+ // Synchronize representations - must not change the content!
+ $normData = $this->modelToNorm($modelData);
+ $viewData = $this->normToView($normData);
+
+ // Validate if view data matches data class (unless empty)
+ if (!FormUtil::isEmpty($viewData)) {
+ $dataClass = $this->config->getDataClass();
+
+ $actualType = is_object($viewData) ? 'an instance of class '.get_class($viewData) : ' a(n) '.gettype($viewData);
+
+ if (null === $dataClass && is_object($viewData) && !$viewData instanceof \ArrayAccess) {
+ $expectedType = 'scalar, array or an instance of \ArrayAccess';
+
+ throw new LogicException(
+ 'The form\'s view data is expected to be of type '.$expectedType.', ' .
+ 'but is '.$actualType.'. You ' .
+ 'can avoid this error by setting the "data_class" option to ' .
+ '"'.get_class($viewData).'" or by adding a view transformer ' .
+ 'that transforms '.$actualType.' to '.$expectedType.'.'
+ );
+ }
+
+ if (null !== $dataClass && !$viewData instanceof $dataClass) {
+ throw new LogicException(
+ 'The form\'s view data is expected to be an instance of class ' .
+ $dataClass.', but is '. $actualType.'. You can avoid this error ' .
+ 'by setting the "data_class" option to null or by adding a view ' .
+ 'transformer that transforms '.$actualType.' to an instance of ' .
+ $dataClass.'.'
+ );
+ }
+ }
+
+ $this->modelData = $modelData;
+ $this->normData = $normData;
+ $this->viewData = $viewData;
+ $this->defaultDataSet = true;
+ $this->lockSetData = false;
+
+ // It is not necessary to invoke this method if the form doesn't have children,
+ // even if the form is compound.
+ if (count($this->children) > 0) {
+ // Update child forms from the data
+ $childrenIterator = new InheritDataAwareIterator($this->children);
+ $childrenIterator = new \RecursiveIteratorIterator($childrenIterator);
+ $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator);
+ }
+
+ if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) {
+ $event = new FormEvent($this, $modelData);
+ $dispatcher->dispatch(FormEvents::POST_SET_DATA, $event);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData()
+ {
+ if ($this->config->getInheritData()) {
+ if (!$this->parent) {
+ throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
+ }
+
+ return $this->parent->getData();
+ }
+
+ if (!$this->defaultDataSet) {
+ $this->setData($this->config->getData());
+ }
+
+ return $this->modelData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNormData()
+ {
+ if ($this->config->getInheritData()) {
+ if (!$this->parent) {
+ throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
+ }
+
+ return $this->parent->getNormData();
+ }
+
+ if (!$this->defaultDataSet) {
+ $this->setData($this->config->getData());
+ }
+
+ return $this->normData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViewData()
+ {
+ if ($this->config->getInheritData()) {
+ if (!$this->parent) {
+ throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
+ }
+
+ return $this->parent->getViewData();
+ }
+
+ if (!$this->defaultDataSet) {
+ $this->setData($this->config->getData());
+ }
+
+ return $this->viewData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtraData()
+ {
+ return $this->extraData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize()
+ {
+ if (null !== $this->parent) {
+ throw new RuntimeException('Only root forms should be initialized.');
+ }
+
+ // Guarantee that the *_SET_DATA events have been triggered once the
+ // form is initialized. This makes sure that dynamically added or
+ // removed fields are already visible after initialization.
+ if (!$this->defaultDataSet) {
+ $this->setData($this->config->getData());
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handleRequest($request = null)
+ {
+ $this->config->getRequestHandler()->handleRequest($this, $request);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submit($submittedData, $clearMissing = true)
+ {
+ if ($this->submitted) {
+ throw new AlreadySubmittedException('A form can only be submitted once');
+ }
+
+ // Initialize errors in the very beginning so that we don't lose any
+ // errors added during listeners
+ $this->errors = array();
+
+ // Obviously, a disabled form should not change its data upon submission.
+ if ($this->isDisabled()) {
+ $this->submitted = true;
+
+ return $this;
+ }
+
+ // The data must be initialized if it was not initialized yet.
+ // This is necessary to guarantee that the *_SET_DATA listeners
+ // are always invoked before submit() takes place.
+ if (!$this->defaultDataSet) {
+ $this->setData($this->config->getData());
+ }
+
+ // Treat false as NULL to support binding false to checkboxes.
+ // Don't convert NULL to a string here in order to determine later
+ // whether an empty value has been submitted or whether no value has
+ // been submitted at all. This is important for processing checkboxes
+ // and radio buttons with empty values.
+ if (false === $submittedData) {
+ $submittedData = null;
+ } elseif (is_scalar($submittedData)) {
+ $submittedData = (string) $submittedData;
+ }
+
+ $dispatcher = $this->config->getEventDispatcher();
+
+ // Hook to change content of the data submitted by the browser
+ if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
+ $event = new FormEvent($this, $submittedData);
+ $dispatcher->dispatch(FormEvents::PRE_SUBMIT, $event);
+ $submittedData = $event->getData();
+ }
+
+ // Check whether the form is compound.
+ // This check is preferable over checking the number of children,
+ // since forms without children may also be compound.
+ // (think of empty collection forms)
+ if ($this->config->getCompound()) {
+ if (!is_array($submittedData)) {
+ $submittedData = array();
+ }
+
+ foreach ($this->children as $name => $child) {
+ if (isset($submittedData[$name]) || $clearMissing) {
+ $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing);
+ unset($submittedData[$name]);
+ }
+ }
+
+ $this->extraData = $submittedData;
+ }
+
+ // Forms that inherit their parents' data also are not processed,
+ // because then it would be too difficult to merge the changes in
+ // the child and the parent form. Instead, the parent form also takes
+ // changes in the grandchildren (i.e. children of the form that inherits
+ // its parent's data) into account.
+ // (see InheritDataAwareIterator below)
+ if ($this->config->getInheritData()) {
+ $this->submitted = true;
+
+ // When POST_SUBMIT is reached, the data is not yet updated, so pass
+ // NULL to prevent hard-to-debug bugs.
+ $dataForPostSubmit = null;
+ } else {
+ // If the form is compound, the default data in view format
+ // is reused. The data of the children is merged into this
+ // default data using the data mapper.
+ // If the form is not compound, the submitted data is also the data in view format.
+ $viewData = $this->config->getCompound() ? $this->viewData : $submittedData;
+
+ if (FormUtil::isEmpty($viewData)) {
+ $emptyData = $this->config->getEmptyData();
+
+ if ($emptyData instanceof \Closure) {
+ /* @var \Closure $emptyData */
+ $emptyData = $emptyData($this, $viewData);
+ }
+
+ $viewData = $emptyData;
+ }
+
+ // Merge form data from children into existing view data
+ // It is not necessary to invoke this method if the form has no children,
+ // even if it is compound.
+ if (count($this->children) > 0) {
+ // Use InheritDataAwareIterator to process children of
+ // descendants that inherit this form's data.
+ // These descendants will not be submitted normally (see the check
+ // for $this->config->getInheritData() above)
+ $childrenIterator = new InheritDataAwareIterator($this->children);
+ $childrenIterator = new \RecursiveIteratorIterator($childrenIterator);
+ $this->config->getDataMapper()->mapFormsToData($childrenIterator, $viewData);
+ }
+
+ $modelData = null;
+ $normData = null;
+
+ try {
+ // Normalize data to unified representation
+ $normData = $this->viewToNorm($viewData);
+
+ // Hook to change content of the data into the normalized
+ // representation
+ if ($dispatcher->hasListeners(FormEvents::SUBMIT)) {
+ $event = new FormEvent($this, $normData);
+ $dispatcher->dispatch(FormEvents::SUBMIT, $event);
+ $normData = $event->getData();
+ }
+
+ // Synchronize representations - must not change the content!
+ $modelData = $this->normToModel($normData);
+ $viewData = $this->normToView($normData);
+ } catch (TransformationFailedException $e) {
+ $this->synchronized = false;
+ }
+
+ $this->submitted = true;
+ $this->modelData = $modelData;
+ $this->normData = $normData;
+ $this->viewData = $viewData;
+
+ $dataForPostSubmit = $viewData;
+ }
+
+ if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) {
+ $event = new FormEvent($this, $dataForPostSubmit);
+ $dispatcher->dispatch(FormEvents::POST_SUBMIT, $event);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Alias of {@link submit()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link submit()} instead.
+ */
+ public function bind($submittedData)
+ {
+ return $this->submit($submittedData);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addError(FormError $error)
+ {
+ if ($this->parent && $this->config->getErrorBubbling()) {
+ $this->parent->addError($error);
+ } else {
+ $this->errors[] = $error;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSubmitted()
+ {
+ return $this->submitted;
+ }
+
+ /**
+ * Alias of {@link isSubmitted()}.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link isSubmitted()} instead.
+ */
+ public function isBound()
+ {
+ return $this->submitted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSynchronized()
+ {
+ return $this->synchronized;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEmpty()
+ {
+ foreach ($this->children as $child) {
+ if (!$child->isEmpty()) {
+ return false;
+ }
+ }
+
+ return FormUtil::isEmpty($this->modelData) ||
+ // arrays, countables
+ 0 === count($this->modelData) ||
+ // traversables that are not countable
+ ($this->modelData instanceof \Traversable && 0 === iterator_count($this->modelData));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid()
+ {
+ if (!$this->submitted) {
+ return false;
+ }
+
+ if (count($this->errors) > 0) {
+ return false;
+ }
+
+ if (!$this->isDisabled()) {
+ foreach ($this->children as $child) {
+ if (!$child->isValid()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ /**
+ * Returns a string representation of all form errors (including children errors).
+ *
+ * This method should only be used to help debug a form.
+ *
+ * @param integer $level The indentation level (used internally)
+ *
+ * @return string A string representation of all errors
+ */
+ public function getErrorsAsString($level = 0)
+ {
+ $errors = '';
+ foreach ($this->errors as $error) {
+ $errors .= str_repeat(' ', $level).'ERROR: '.$error->getMessage()."\n";
+ }
+
+ foreach ($this->children as $key => $child) {
+ $errors .= str_repeat(' ', $level).$key.":\n";
+ if ($err = $child->getErrorsAsString($level + 4)) {
+ $errors .= $err;
+ } else {
+ $errors .= str_repeat(' ', $level + 4)."No errors\n";
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ return $this->children;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ if ($this->submitted) {
+ throw new AlreadySubmittedException('You cannot add children to a submitted form');
+ }
+
+ if (!$this->config->getCompound()) {
+ throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
+ }
+
+ // Obtain the view data
+ $viewData = null;
+
+ // If setData() is currently being called, there is no need to call
+ // mapDataToForms() here, as mapDataToForms() is called at the end
+ // of setData() anyway. Not doing this check leads to an endless
+ // recursion when initializing the form lazily and an event listener
+ // (such as ResizeFormListener) adds fields depending on the data:
+ //
+ // * setData() is called, the form is not initialized yet
+ // * add() is called by the listener (setData() is not complete, so
+ // the form is still not initialized)
+ // * getViewData() is called
+ // * setData() is called since the form is not initialized yet
+ // * ... endless recursion ...
+ //
+ // Also skip data mapping if setData() has not been called yet.
+ // setData() will be called upon form initialization and data mapping
+ // will take place by then.
+ if (!$this->lockSetData && $this->defaultDataSet && !$this->config->getInheritData()) {
+ $viewData = $this->getViewData();
+ }
+
+ if (!$child instanceof FormInterface) {
+ if (!is_string($child) && !is_int($child)) {
+ throw new UnexpectedTypeException($child, 'string, integer or Symfony\Component\Form\FormInterface');
+ }
+
+ if (null !== $type && !is_string($type) && !$type instanceof FormTypeInterface) {
+ throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
+ }
+
+ // Never initialize child forms automatically
+ $options['auto_initialize'] = false;
+
+ if (null === $type) {
+ $child = $this->config->getFormFactory()->createForProperty($this->config->getDataClass(), $child, null, $options);
+ } else {
+ $child = $this->config->getFormFactory()->createNamed($child, $type, null, $options);
+ }
+ } elseif ($child->getConfig()->getAutoInitialize()) {
+ throw new RuntimeException(sprintf(
+ 'Automatic initialization is only supported on root forms. You '.
+ 'should set the "auto_initialize" option to false on the field "%s".',
+ $child->getName()
+ ));
+ }
+
+ $this->children[$child->getName()] = $child;
+
+ $child->setParent($this);
+
+ if (!$this->lockSetData && $this->defaultDataSet && !$this->config->getInheritData()) {
+ $childrenIterator = new InheritDataAwareIterator(array($child));
+ $childrenIterator = new \RecursiveIteratorIterator($childrenIterator);
+ $this->config->getDataMapper()->mapDataToForms($viewData, $childrenIterator);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($name)
+ {
+ if ($this->submitted) {
+ throw new AlreadySubmittedException('You cannot remove children from a submitted form');
+ }
+
+ if (isset($this->children[$name])) {
+ $this->children[$name]->setParent(null);
+
+ unset($this->children[$name]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has($name)
+ {
+ return isset($this->children[$name]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($name)
+ {
+ if (isset($this->children[$name])) {
+ return $this->children[$name];
+ }
+
+ throw new OutOfBoundsException(sprintf('Child "%s" does not exist.', $name));
+ }
+
+ /**
+ * Returns whether a child with the given name exists (implements the \ArrayAccess interface).
+ *
+ * @param string $name The name of the child
+ *
+ * @return Boolean
+ */
+ public function offsetExists($name)
+ {
+ return $this->has($name);
+ }
+
+ /**
+ * Returns the child with the given name (implements the \ArrayAccess interface).
+ *
+ * @param string $name The name of the child
+ *
+ * @return FormInterface The child form
+ *
+ * @throws \OutOfBoundsException If the named child does not exist.
+ */
+ public function offsetGet($name)
+ {
+ return $this->get($name);
+ }
+
+ /**
+ * Adds a child to the form (implements the \ArrayAccess interface).
+ *
+ * @param string $name Ignored. The name of the child is used.
+ * @param FormInterface $child The child to be added.
+ *
+ * @throws AlreadySubmittedException If the form has already been submitted.
+ * @throws LogicException When trying to add a child to a non-compound form.
+ *
+ * @see self::add()
+ */
+ public function offsetSet($name, $child)
+ {
+ $this->add($child);
+ }
+
+ /**
+ * Removes the child with the given name from the form (implements the \ArrayAccess interface).
+ *
+ * @param string $name The name of the child to remove
+ *
+ * @throws AlreadySubmittedException If the form has already been submitted.
+ */
+ public function offsetUnset($name)
+ {
+ $this->remove($name);
+ }
+
+ /**
+ * Returns the iterator for this group.
+ *
+ * @return \ArrayIterator
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->children);
+ }
+
+ /**
+ * Returns the number of form children (implements the \Countable interface).
+ *
+ * @return integer The number of embedded form children
+ */
+ public function count()
+ {
+ return count($this->children);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createView(FormView $parent = null)
+ {
+ if (null === $parent && $this->parent) {
+ $parent = $this->parent->createView();
+ }
+
+ return $this->config->getType()->createView($this, $parent);
+ }
+
+ /**
+ * Normalizes the value if a normalization transformer is set.
+ *
+ * @param mixed $value The value to transform
+ *
+ * @return mixed
+ */
+ private function modelToNorm($value)
+ {
+ foreach ($this->config->getModelTransformers() as $transformer) {
+ $value = $transformer->transform($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reverse transforms a value if a normalization transformer is set.
+ *
+ * @param string $value The value to reverse transform
+ *
+ * @return mixed
+ */
+ private function normToModel($value)
+ {
+ $transformers = $this->config->getModelTransformers();
+
+ for ($i = count($transformers) - 1; $i >= 0; --$i) {
+ $value = $transformers[$i]->reverseTransform($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Transforms the value if a value transformer is set.
+ *
+ * @param mixed $value The value to transform
+ *
+ * @return mixed
+ */
+ private function normToView($value)
+ {
+ // Scalar values should be converted to strings to
+ // facilitate differentiation between empty ("") and zero (0).
+ // Only do this for simple forms, as the resulting value in
+ // compound forms is passed to the data mapper and thus should
+ // not be converted to a string before.
+ if (!$this->config->getViewTransformers() && !$this->config->getCompound()) {
+ return null === $value || is_scalar($value) ? (string) $value : $value;
+ }
+
+ foreach ($this->config->getViewTransformers() as $transformer) {
+ $value = $transformer->transform($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Reverse transforms a value if a value transformer is set.
+ *
+ * @param string $value The value to reverse transform
+ *
+ * @return mixed
+ */
+ private function viewToNorm($value)
+ {
+ $transformers = $this->config->getViewTransformers();
+
+ if (!$transformers) {
+ return '' === $value ? null : $value;
+ }
+
+ for ($i = count($transformers) - 1; $i >= 0; --$i) {
+ $value = $transformers[$i]->reverseTransform($value);
+ }
+
+ return $value;
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\BadMethodCallException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * A builder for creating {@link Form} instances.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormBuilder extends FormConfigBuilder implements \IteratorAggregate, FormBuilderInterface
+{
+ /**
+ * The children of the form builder.
+ *
+ * @var FormBuilderInterface[]
+ */
+ private $children = array();
+
+ /**
+ * The data of children who haven't been converted to form builders yet.
+ *
+ * @var array
+ */
+ private $unresolvedChildren = array();
+
+ /**
+ * Creates a new form builder.
+ *
+ * @param string $name
+ * @param string $dataClass
+ * @param EventDispatcherInterface $dispatcher
+ * @param FormFactoryInterface $factory
+ * @param array $options
+ */
+ public function __construct($name, $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory, array $options = array())
+ {
+ parent::__construct($name, $dataClass, $dispatcher, $options);
+
+ $this->setFormFactory($factory);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function add($child, $type = null, array $options = array())
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if ($child instanceof self) {
+ $this->children[$child->getName()] = $child;
+
+ // In case an unresolved child with the same name exists
+ unset($this->unresolvedChildren[$child->getName()]);
+
+ return $this;
+ }
+
+ if (!is_string($child) && !is_int($child)) {
+ throw new UnexpectedTypeException($child, 'string, integer or Symfony\Component\Form\FormBuilder');
+ }
+
+ if (null !== $type && !is_string($type) && !$type instanceof FormTypeInterface) {
+ throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
+ }
+
+ // Add to "children" to maintain order
+ $this->children[$child] = null;
+ $this->unresolvedChildren[$child] = array(
+ 'type' => $type,
+ 'options' => $options,
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create($name, $type = null, array $options = array())
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if (null === $type && null === $this->getDataClass()) {
+ $type = 'text';
+ }
+
+ if (null !== $type) {
+ return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options);
+ }
+
+ return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($name)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if (isset($this->unresolvedChildren[$name])) {
+ return $this->resolveChild($name);
+ }
+
+ if (isset($this->children[$name])) {
+ return $this->children[$name];
+ }
+
+ throw new InvalidArgumentException(sprintf('The child with the name "%s" does not exist.', $name));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function remove($name)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ unset($this->unresolvedChildren[$name]);
+
+ if (array_key_exists($name, $this->children)) {
+ unset($this->children[$name]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function has($name)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if (isset($this->unresolvedChildren[$name])) {
+ return true;
+ }
+
+ if (isset($this->children[$name])) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function all()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->resolveChildren();
+
+ return $this->children;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ return count($this->children);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormConfig()
+ {
+ $config = parent::getFormConfig();
+
+ $config->children = array();
+ $config->unresolvedChildren = array();
+
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getForm()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->resolveChildren();
+
+ $form = new Form($this->getFormConfig());
+
+ foreach ($this->children as $child) {
+ // Automatic initialization is only supported on root forms
+ $form->add($child->setAutoInitialize(false)->getForm());
+ }
+
+ if ($this->getAutoInitialize()) {
+ // Automatically initialize the form if it is configured so
+ $form->initialize();
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ return new \ArrayIterator($this->children);
+ }
+
+ /**
+ * Converts an unresolved child into a {@link FormBuilder} instance.
+ *
+ * @param string $name The name of the unresolved child.
+ *
+ * @return FormBuilder The created instance.
+ */
+ private function resolveChild($name)
+ {
+ $info = $this->unresolvedChildren[$name];
+ $child = $this->create($name, $info['type'], $info['options']);
+ $this->children[$name] = $child;
+ unset($this->unresolvedChildren[$name]);
+
+ return $child;
+ }
+
+ /**
+ * Converts all unresolved children into {@link FormBuilder} instances.
+ */
+ private function resolveChildren()
+ {
+ foreach ($this->unresolvedChildren as $name => $info) {
+ $this->children[$name] = $this->create($name, $info['type'], $info['options']);
+ }
+
+ $this->unresolvedChildren = array();
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormBuilderInterface extends \Traversable, \Countable, FormConfigBuilderInterface
+{
+ /**
+ * Adds a new field to this group. A field must have a unique name within
+ * the group. Otherwise the existing field is overwritten.
+ *
+ * If you add a nested group, this group should also be represented in the
+ * object hierarchy.
+ *
+ * @param string|integer|FormBuilderInterface $child
+ * @param string|FormTypeInterface $type
+ * @param array $options
+ *
+ * @return FormBuilderInterface The builder object.
+ */
+ public function add($child, $type = null, array $options = array());
+
+ /**
+ * Creates a form builder.
+ *
+ * @param string $name The name of the form or the name of the property
+ * @param string|FormTypeInterface $type The type of the form or null if name is a property
+ * @param array $options The options
+ *
+ * @return FormBuilderInterface The created builder.
+ */
+ public function create($name, $type = null, array $options = array());
+
+ /**
+ * Returns a child by name.
+ *
+ * @param string $name The name of the child
+ *
+ * @return FormBuilderInterface The builder for the child
+ *
+ * @throws Exception\InvalidArgumentException if the given child does not exist
+ */
+ public function get($name);
+
+ /**
+ * Removes the field with the given name.
+ *
+ * @param string $name
+ *
+ * @return FormBuilderInterface The builder object.
+ */
+ public function remove($name);
+
+ /**
+ * Returns whether a field with the given name exists.
+ *
+ * @param string $name
+ *
+ * @return Boolean
+ */
+ public function has($name);
+
+ /**
+ * Returns the children.
+ *
+ * @return array
+ */
+ public function all();
+
+ /**
+ * Creates the form.
+ *
+ * @return Form The form
+ */
+ public function getForm();
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\BadMethodCallException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\PropertyAccess\PropertyPathInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
+
+/**
+ * A basic form configuration.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormConfigBuilder implements FormConfigBuilderInterface
+{
+ /**
+ * Caches a globally unique {@link NativeRequestHandler} instance.
+ *
+ * @var NativeRequestHandler
+ */
+ private static $nativeRequestProcessor;
+
+ /**
+ * The accepted request methods.
+ *
+ * @var array
+ */
+ private static $allowedMethods = array(
+ 'GET',
+ 'PUT',
+ 'POST',
+ 'DELETE',
+ 'PATCH'
+ );
+
+ /**
+ * @var Boolean
+ */
+ protected $locked = false;
+
+ /**
+ * @var EventDispatcherInterface
+ */
+ private $dispatcher;
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var PropertyPathInterface
+ */
+ private $propertyPath;
+
+ /**
+ * @var Boolean
+ */
+ private $mapped = true;
+
+ /**
+ * @var Boolean
+ */
+ private $byReference = true;
+
+ /**
+ * @var Boolean
+ */
+ private $inheritData = false;
+
+ /**
+ * @var Boolean
+ */
+ private $compound = false;
+
+ /**
+ * @var ResolvedFormTypeInterface
+ */
+ private $type;
+
+ /**
+ * @var array
+ */
+ private $viewTransformers = array();
+
+ /**
+ * @var array
+ */
+ private $modelTransformers = array();
+
+ /**
+ * @var DataMapperInterface
+ */
+ private $dataMapper;
+
+ /**
+ * @var Boolean
+ */
+ private $required = true;
+
+ /**
+ * @var Boolean
+ */
+ private $disabled = false;
+
+ /**
+ * @var Boolean
+ */
+ private $errorBubbling = false;
+
+ /**
+ * @var mixed
+ */
+ private $emptyData;
+
+ /**
+ * @var array
+ */
+ private $attributes = array();
+
+ /**
+ * @var mixed
+ */
+ private $data;
+
+ /**
+ * @var string
+ */
+ private $dataClass;
+
+ /**
+ * @var Boolean
+ */
+ private $dataLocked;
+
+ /**
+ * @var FormFactoryInterface
+ */
+ private $formFactory;
+
+ /**
+ * @var string
+ */
+ private $action;
+
+ /**
+ * @var string
+ */
+ private $method = 'POST';
+
+ /**
+ * @var RequestHandlerInterface
+ */
+ private $requestHandler;
+
+ /**
+ * @var Boolean
+ */
+ private $autoInitialize = false;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Creates an empty form configuration.
+ *
+ * @param string|integer $name The form name
+ * @param string $dataClass The class of the form's data
+ * @param EventDispatcherInterface $dispatcher The event dispatcher
+ * @param array $options The form options
+ *
+ * @throws InvalidArgumentException If the data class is not a valid class or if
+ * the name contains invalid characters.
+ */
+ public function __construct($name, $dataClass, EventDispatcherInterface $dispatcher, array $options = array())
+ {
+ self::validateName($name);
+
+ if (null !== $dataClass && !class_exists($dataClass)) {
+ throw new InvalidArgumentException(sprintf('The data class "%s" is not a valid class.', $dataClass));
+ }
+
+ $this->name = (string) $name;
+ $this->dataClass = $dataClass;
+ $this->dispatcher = $dispatcher;
+ $this->options = $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addEventListener($eventName, $listener, $priority = 0)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->dispatcher->addListener($eventName, $listener, $priority);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addEventSubscriber(EventSubscriberInterface $subscriber)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->dispatcher->addSubscriber($subscriber);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addViewTransformer(DataTransformerInterface $viewTransformer, $forcePrepend = false)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if ($forcePrepend) {
+ array_unshift($this->viewTransformers, $viewTransformer);
+ } else {
+ $this->viewTransformers[] = $viewTransformer;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resetViewTransformers()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->viewTransformers = array();
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addModelTransformer(DataTransformerInterface $modelTransformer, $forceAppend = false)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if ($forceAppend) {
+ $this->modelTransformers[] = $modelTransformer;
+ } else {
+ array_unshift($this->modelTransformers, $modelTransformer);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resetModelTransformers()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->modelTransformers = array();
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEventDispatcher()
+ {
+ if ($this->locked && !$this->dispatcher instanceof ImmutableEventDispatcher) {
+ $this->dispatcher = new ImmutableEventDispatcher($this->dispatcher);
+ }
+
+ return $this->dispatcher;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyPath()
+ {
+ return $this->propertyPath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMapped()
+ {
+ return $this->mapped;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getByReference()
+ {
+ return $this->byReference;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInheritData()
+ {
+ return $this->inheritData;
+ }
+
+ /**
+ * Alias of {@link getInheritData()}.
+ *
+ * @return FormConfigBuilder The configuration object.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link getInheritData()} instead.
+ */
+ public function getVirtual()
+ {
+ // Uncomment this as soon as the deprecation note should be shown
+ // trigger_error('getVirtual() is deprecated since version 2.3 and will be removed in 3.0. Use getInheritData() instead.', E_USER_DEPRECATED);
+ return $this->getInheritData();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCompound()
+ {
+ return $this->compound;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViewTransformers()
+ {
+ return $this->viewTransformers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getModelTransformers()
+ {
+ return $this->modelTransformers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataMapper()
+ {
+ return $this->dataMapper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequired()
+ {
+ return $this->required;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getErrorBubbling()
+ {
+ return $this->errorBubbling;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEmptyData()
+ {
+ return $this->emptyData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasAttribute($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttribute($name, $default = null)
+ {
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataClass()
+ {
+ return $this->dataClass;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataLocked()
+ {
+ return $this->dataLocked;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormFactory()
+ {
+ return $this->formFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequestHandler()
+ {
+ if (null === $this->requestHandler) {
+ if (null === self::$nativeRequestProcessor) {
+ self::$nativeRequestProcessor = new NativeRequestHandler();
+ }
+ $this->requestHandler = self::$nativeRequestProcessor;
+ }
+
+ return $this->requestHandler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAutoInitialize()
+ {
+ return $this->autoInitialize;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasOption($name)
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOption($name, $default = null)
+ {
+ return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttribute($name, $value)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->attributes[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAttributes(array $attributes)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->attributes = $attributes;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDataMapper(DataMapperInterface $dataMapper = null)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->dataMapper = $dataMapper;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDisabled($disabled)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->disabled = (Boolean) $disabled;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEmptyData($emptyData)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->emptyData = $emptyData;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setErrorBubbling($errorBubbling)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->errorBubbling = null === $errorBubbling ? null : (Boolean) $errorBubbling;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRequired($required)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->required = (Boolean) $required;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setPropertyPath($propertyPath)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ if (null !== $propertyPath && !$propertyPath instanceof PropertyPathInterface) {
+ $propertyPath = new PropertyPath($propertyPath);
+ }
+
+ $this->propertyPath = $propertyPath;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMapped($mapped)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->mapped = $mapped;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setByReference($byReference)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->byReference = $byReference;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setInheritData($inheritData)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->inheritData = $inheritData;
+
+ return $this;
+ }
+
+ /**
+ * Alias of {@link setInheritData()}.
+ *
+ * @param Boolean $inheritData Whether the form should inherit its parent's data.
+ *
+ * @return FormConfigBuilder The configuration object.
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link setInheritData()} instead.
+ */
+ public function setVirtual($inheritData)
+ {
+ // Uncomment this as soon as the deprecation note should be shown
+ // trigger_error('setVirtual() is deprecated since version 2.3 and will be removed in 3.0. Use setInheritData() instead.', E_USER_DEPRECATED);
+
+ $this->setInheritData($inheritData);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCompound($compound)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->compound = $compound;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setType(ResolvedFormTypeInterface $type)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setData($data)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDataLocked($locked)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->dataLocked = $locked;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFormFactory(FormFactoryInterface $formFactory)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ $this->formFactory = $formFactory;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAction($action)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('The config builder cannot be modified anymore.');
+ }
+
+ $this->action = $action;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMethod($method)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('The config builder cannot be modified anymore.');
+ }
+
+ $upperCaseMethod = strtoupper($method);
+
+ if (!in_array($upperCaseMethod, self::$allowedMethods)) {
+ throw new InvalidArgumentException(sprintf(
+ 'The form method is "%s", but should be one of "%s".',
+ $method,
+ implode('", "', self::$allowedMethods)
+ ));
+ }
+
+ $this->method = $upperCaseMethod;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRequestHandler(RequestHandlerInterface $requestHandler)
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('The config builder cannot be modified anymore.');
+ }
+
+ $this->requestHandler = $requestHandler;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAutoInitialize($initialize)
+ {
+ $this->autoInitialize = (Boolean) $initialize;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormConfig()
+ {
+ if ($this->locked) {
+ throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.');
+ }
+
+ // This method should be idempotent, so clone the builder
+ $config = clone $this;
+ $config->locked = true;
+
+ return $config;
+ }
+
+ /**
+ * Validates whether the given variable is a valid form name.
+ *
+ * @param string|integer $name The tested form name.
+ *
+ * @throws UnexpectedTypeException If the name is not a string or an integer.
+ * @throws InvalidArgumentException If the name contains invalid characters.
+ */
+ public static function validateName($name)
+ {
+ if (null !== $name && !is_string($name) && !is_int($name)) {
+ throw new UnexpectedTypeException($name, 'string, integer or null');
+ }
+
+ if (!self::isValidName($name)) {
+ throw new InvalidArgumentException(sprintf(
+ 'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").',
+ $name
+ ));
+ }
+ }
+
+ /**
+ * Returns whether the given variable contains a valid form name.
+ *
+ * A name is accepted if it
+ *
+ * * is empty
+ * * starts with a letter, digit or underscore
+ * * contains only letters, digits, numbers, underscores ("_"),
+ * hyphens ("-") and colons (":")
+ *
+ * @param string $name The tested form name.
+ *
+ * @return Boolean Whether the name is valid.
+ */
+ public static function isValidName($name)
+ {
+ return '' === $name || null === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormConfigBuilderInterface extends FormConfigInterface
+{
+ /**
+ * Adds an event listener to an event on this form.
+ *
+ * @param string $eventName The name of the event to listen to.
+ * @param callable $listener The listener to execute.
+ * @param integer $priority The priority of the listener. Listeners
+ * with a higher priority are called before
+ * listeners with a lower priority.
+ *
+ * @return self The configuration object.
+ */
+ public function addEventListener($eventName, $listener, $priority = 0);
+
+ /**
+ * Adds an event subscriber for events on this form.
+ *
+ * @param EventSubscriberInterface $subscriber The subscriber to attach.
+ *
+ * @return self The configuration object.
+ */
+ public function addEventSubscriber(EventSubscriberInterface $subscriber);
+
+ /**
+ * Appends / prepends a transformer to the view transformer chain.
+ *
+ * The transform method of the transformer is used to convert data from the
+ * normalized to the view format.
+ * The reverseTransform method of the transformer is used to convert from the
+ * view to the normalized format.
+ *
+ * @param DataTransformerInterface $viewTransformer
+ * @param Boolean $forcePrepend if set to true, prepend instead of appending
+ *
+ * @return self The configuration object.
+ */
+ public function addViewTransformer(DataTransformerInterface $viewTransformer, $forcePrepend = false);
+
+ /**
+ * Clears the view transformers.
+ *
+ * @return self The configuration object.
+ */
+ public function resetViewTransformers();
+
+ /**
+ * Prepends / appends a transformer to the normalization transformer chain.
+ *
+ * The transform method of the transformer is used to convert data from the
+ * model to the normalized format.
+ * The reverseTransform method of the transformer is used to convert from the
+ * normalized to the model format.
+ *
+ * @param DataTransformerInterface $modelTransformer
+ * @param Boolean $forceAppend if set to true, append instead of prepending
+ *
+ * @return self The configuration object.
+ */
+ public function addModelTransformer(DataTransformerInterface $modelTransformer, $forceAppend = false);
+
+ /**
+ * Clears the normalization transformers.
+ *
+ * @return self The configuration object.
+ */
+ public function resetModelTransformers();
+
+ /**
+ * Sets the value for an attribute.
+ *
+ * @param string $name The name of the attribute
+ * @param string $value The value of the attribute
+ *
+ * @return self The configuration object.
+ */
+ public function setAttribute($name, $value);
+
+ /**
+ * Sets the attributes.
+ *
+ * @param array $attributes The attributes.
+ *
+ * @return self The configuration object.
+ */
+ public function setAttributes(array $attributes);
+
+ /**
+ * Sets the data mapper used by the form.
+ *
+ * @param DataMapperInterface $dataMapper
+ *
+ * @return self The configuration object.
+ */
+ public function setDataMapper(DataMapperInterface $dataMapper = null);
+
+ /**
+ * Set whether the form is disabled.
+ *
+ * @param Boolean $disabled Whether the form is disabled
+ *
+ * @return self The configuration object.
+ */
+ public function setDisabled($disabled);
+
+ /**
+ * Sets the data used for the client data when no value is submitted.
+ *
+ * @param mixed $emptyData The empty data.
+ *
+ * @return self The configuration object.
+ */
+ public function setEmptyData($emptyData);
+
+ /**
+ * Sets whether errors bubble up to the parent.
+ *
+ * @param Boolean $errorBubbling
+ *
+ * @return self The configuration object.
+ */
+ public function setErrorBubbling($errorBubbling);
+
+ /**
+ * Sets whether this field is required to be filled out when submitted.
+ *
+ * @param Boolean $required
+ *
+ * @return self The configuration object.
+ */
+ public function setRequired($required);
+
+ /**
+ * Sets the property path that the form should be mapped to.
+ *
+ * @param null|string|\Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath
+ * The property path or null if the path should be set
+ * automatically based on the form's name.
+ *
+ * @return self The configuration object.
+ */
+ public function setPropertyPath($propertyPath);
+
+ /**
+ * Sets whether the form should be mapped to an element of its
+ * parent's data.
+ *
+ * @param Boolean $mapped Whether the form should be mapped.
+ *
+ * @return self The configuration object.
+ */
+ public function setMapped($mapped);
+
+ /**
+ * Sets whether the form's data should be modified by reference.
+ *
+ * @param Boolean $byReference Whether the data should be
+ * modified by reference.
+ *
+ * @return self The configuration object.
+ */
+ public function setByReference($byReference);
+
+ /**
+ * Sets whether the form should read and write the data of its parent.
+ *
+ * @param Boolean $inheritData Whether the form should inherit its parent's data.
+ *
+ * @return self The configuration object.
+ */
+ public function setInheritData($inheritData);
+
+ /**
+ * Sets whether the form should be compound.
+ *
+ * @param Boolean $compound Whether the form should be compound.
+ *
+ * @return self The configuration object.
+ *
+ * @see FormConfigInterface::getCompound()
+ */
+ public function setCompound($compound);
+
+ /**
+ * Set the types.
+ *
+ * @param ResolvedFormTypeInterface $type The type of the form.
+ *
+ * @return self The configuration object.
+ */
+ public function setType(ResolvedFormTypeInterface $type);
+
+ /**
+ * Sets the initial data of the form.
+ *
+ * @param array $data The data of the form in application format.
+ *
+ * @return self The configuration object.
+ */
+ public function setData($data);
+
+ /**
+ * Locks the form's data to the data passed in the configuration.
+ *
+ * A form with locked data is restricted to the data passed in
+ * this configuration. The data can only be modified then by
+ * submitting the form.
+ *
+ * @param Boolean $locked Whether to lock the default data.
+ *
+ * @return self The configuration object.
+ */
+ public function setDataLocked($locked);
+
+ /**
+ * Sets the form factory used for creating new forms.
+ *
+ * @param FormFactoryInterface $formFactory The form factory.
+ */
+ public function setFormFactory(FormFactoryInterface $formFactory);
+
+ /**
+ * Sets the target URL of the form.
+ *
+ * @param string $action The target URL of the form.
+ *
+ * @return self The configuration object.
+ */
+ public function setAction($action);
+
+ /**
+ * Sets the HTTP method used by the form.
+ *
+ * @param string $method The HTTP method of the form.
+ *
+ * @return self The configuration object.
+ */
+ public function setMethod($method);
+
+ /**
+ * Sets the request handler used by the form.
+ *
+ * @param RequestHandlerInterface $requestHandler
+ *
+ * @return self The configuration object.
+ */
+ public function setRequestHandler(RequestHandlerInterface $requestHandler);
+
+ /**
+ * Sets whether the form should be initialized automatically.
+ *
+ * Should be set to true only for root forms.
+ *
+ * @param Boolean $initialize True to initialize the form automatically,
+ * false to suppress automatic initialization.
+ * In the second case, you need to call
+ * {@link FormInterface::initialize()} manually.
+ *
+ * @return self The configuration object.
+ */
+ public function setAutoInitialize($initialize);
+
+ /**
+ * Builds and returns the form configuration.
+ *
+ * @return FormConfigInterface
+ */
+ public function getFormConfig();
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * The configuration of a {@link Form} object.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormConfigInterface
+{
+ /**
+ * Returns the event dispatcher used to dispatch form events.
+ *
+ * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface The dispatcher.
+ */
+ public function getEventDispatcher();
+
+ /**
+ * Returns the name of the form used as HTTP parameter.
+ *
+ * @return string The form name.
+ */
+ public function getName();
+
+ /**
+ * Returns the property path that the form should be mapped to.
+ *
+ * @return null|\Symfony\Component\PropertyAccess\PropertyPathInterface The property path.
+ */
+ public function getPropertyPath();
+
+ /**
+ * Returns whether the form should be mapped to an element of its
+ * parent's data.
+ *
+ * @return Boolean Whether the form is mapped.
+ */
+ public function getMapped();
+
+ /**
+ * Returns whether the form's data should be modified by reference.
+ *
+ * @return Boolean Whether to modify the form's data by reference.
+ */
+ public function getByReference();
+
+ /**
+ * Returns whether the form should read and write the data of its parent.
+ *
+ * @return Boolean Whether the form should inherit its parent's data.
+ */
+ public function getInheritData();
+
+ /**
+ * Returns whether the form is compound.
+ *
+ * This property is independent of whether the form actually has
+ * children. A form can be compound and have no children at all, like
+ * for example an empty collection form.
+ *
+ * @return Boolean Whether the form is compound.
+ */
+ public function getCompound();
+
+ /**
+ * Returns the form types used to construct the form.
+ *
+ * @return ResolvedFormTypeInterface The form's type.
+ */
+ public function getType();
+
+ /**
+ * Returns the view transformers of the form.
+ *
+ * @return DataTransformerInterface[] An array of {@link DataTransformerInterface} instances.
+ */
+ public function getViewTransformers();
+
+ /**
+ * Returns the model transformers of the form.
+ *
+ * @return DataTransformerInterface[] An array of {@link DataTransformerInterface} instances.
+ */
+ public function getModelTransformers();
+
+ /**
+ * Returns the data mapper of the form.
+ *
+ * @return DataMapperInterface The data mapper.
+ */
+ public function getDataMapper();
+
+ /**
+ * Returns whether the form is required.
+ *
+ * @return Boolean Whether the form is required.
+ */
+ public function getRequired();
+
+ /**
+ * Returns whether the form is disabled.
+ *
+ * @return Boolean Whether the form is disabled.
+ */
+ public function getDisabled();
+
+ /**
+ * Returns whether errors attached to the form will bubble to its parent.
+ *
+ * @return Boolean Whether errors will bubble up.
+ */
+ public function getErrorBubbling();
+
+ /**
+ * Returns the data that should be returned when the form is empty.
+ *
+ * @return mixed The data returned if the form is empty.
+ */
+ public function getEmptyData();
+
+ /**
+ * Returns additional attributes of the form.
+ *
+ * @return array An array of key-value combinations.
+ */
+ public function getAttributes();
+
+ /**
+ * Returns whether the attribute with the given name exists.
+ *
+ * @param string $name The attribute name.
+ *
+ * @return Boolean Whether the attribute exists.
+ */
+ public function hasAttribute($name);
+
+ /**
+ * Returns the value of the given attribute.
+ *
+ * @param string $name The attribute name.
+ * @param mixed $default The value returned if the attribute does not exist.
+ *
+ * @return mixed The attribute value.
+ */
+ public function getAttribute($name, $default = null);
+
+ /**
+ * Returns the initial data of the form.
+ *
+ * @return mixed The initial form data.
+ */
+ public function getData();
+
+ /**
+ * Returns the class of the form data or null if the data is scalar or an array.
+ *
+ * @return string The data class or null.
+ */
+ public function getDataClass();
+
+ /**
+ * Returns whether the form's data is locked.
+ *
+ * A form with locked data is restricted to the data passed in
+ * this configuration. The data can only be modified then by
+ * submitting the form.
+ *
+ * @return Boolean Whether the data is locked.
+ */
+ public function getDataLocked();
+
+ /**
+ * Returns the form factory used for creating new forms.
+ *
+ * @return FormFactoryInterface The form factory.
+ */
+ public function getFormFactory();
+
+ /**
+ * Returns the target URL of the form.
+ *
+ * @return string The target URL of the form.
+ */
+ public function getAction();
+
+ /**
+ * Returns the HTTP method used by the form.
+ *
+ * @return string The HTTP method of the form.
+ */
+ public function getMethod();
+
+ /**
+ * Returns the request handler used by the form.
+ *
+ * @return RequestHandlerInterface The request handler.
+ */
+ public function getRequestHandler();
+
+ /**
+ * Returns whether the form should be initialized upon creation.
+ *
+ * @return Boolean Returns true if the form should be initialized
+ * when created, false otherwise.
+ */
+ public function getAutoInitialize();
+
+ /**
+ * Returns all options passed during the construction of the form.
+ *
+ * @return array The passed options.
+ */
+ public function getOptions();
+
+ /**
+ * Returns whether a specific option exists.
+ *
+ * @param string $name The option name,
+ *
+ * @return Boolean Whether the option exists.
+ */
+ public function hasOption($name);
+
+ /**
+ * Returns the value of a specific option.
+ *
+ * @param string $name The option name.
+ * @param mixed $default The value returned if the option does not exist.
+ *
+ * @return mixed The option value.
+ */
+ public function getOption($name, $default = null);
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Wraps errors in forms
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormError
+{
+ /**
+ * @var string
+ */
+ private $message;
+
+ /**
+ * The template for the error message
+ * @var string
+ */
+ protected $messageTemplate;
+
+ /**
+ * The parameters that should be substituted in the message template
+ * @var array
+ */
+ protected $messageParameters;
+
+ /**
+ * The value for error message pluralization
+ * @var integer|null
+ */
+ protected $messagePluralization;
+
+ /**
+ * Constructor
+ *
+ * Any array key in $messageParameters will be used as a placeholder in
+ * $messageTemplate.
+ *
+ * @param string $message The translated error message
+ * @param string|null $messageTemplate The template for the error message
+ * @param array $messageParameters The parameters that should be
+ * substituted in the message template.
+ * @param integer|null $messagePluralization The value for error message pluralization
+ *
+ * @see \Symfony\Component\Translation\Translator
+ */
+ public function __construct($message, $messageTemplate = null, array $messageParameters = array(), $messagePluralization = null)
+ {
+ $this->message = $message;
+ $this->messageTemplate = $messageTemplate ?: $message;
+ $this->messageParameters = $messageParameters;
+ $this->messagePluralization = $messagePluralization;
+ }
+
+ /**
+ * Returns the error message
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Returns the error message template
+ *
+ * @return string
+ */
+ public function getMessageTemplate()
+ {
+ return $this->messageTemplate;
+ }
+
+ /**
+ * Returns the parameters to be inserted in the message template
+ *
+ * @return array
+ */
+ public function getMessageParameters()
+ {
+ return $this->messageParameters;
+ }
+
+ /**
+ * Returns the value for error message pluralization.
+ *
+ * @return integer|null
+ */
+ public function getMessagePluralization()
+ {
+ return $this->messagePluralization;
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormEvent extends Event
+{
+ private $form;
+ protected $data;
+
+ /**
+ * Constructs an event.
+ *
+ * @param FormInterface $form The associated form
+ * @param mixed $data The data
+ */
+ public function __construct(FormInterface $form, $data)
+ {
+ $this->form = $form;
+ $this->data = $data;
+ }
+
+ /**
+ * Returns the form at the source of the event.
+ *
+ * @return FormInterface
+ */
+ public function getForm()
+ {
+ return $this->form;
+ }
+
+ /**
+ * Returns the data associated with this event.
+ *
+ * @return mixed
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Allows updating with some filtered data.
+ *
+ * @param mixed $data
+ */
+ public function setData($data)
+ {
+ $this->data = $data;
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+final class FormEvents
+{
+ const PRE_SUBMIT = 'form.pre_bind';
+
+ const SUBMIT = 'form.bind';
+
+ const POST_SUBMIT = 'form.post_bind';
+
+ const PRE_SET_DATA = 'form.pre_set_data';
+
+ const POST_SET_DATA = 'form.post_set_data';
+
+ /**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link PRE_SUBMIT} instead.
+ */
+ const PRE_BIND = 'form.pre_bind';
+
+ /**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link SUBMIT} instead.
+ */
+ const BIND = 'form.bind';
+
+ /**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link POST_SUBMIT} instead.
+ */
+ const POST_BIND = 'form.post_bind';
+
+ private function __construct()
+ {
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Interface for extensions which provide types, type extensions and a guesser.
+ */
+interface FormExtensionInterface
+{
+ /**
+ * Returns a type by name.
+ *
+ * @param string $name The name of the type
+ *
+ * @return FormTypeInterface The type
+ *
+ * @throws Exception\InvalidArgumentException if the given type is not supported by this extension
+ */
+ public function getType($name);
+
+ /**
+ * Returns whether the given type is supported.
+ *
+ * @param string $name The name of the type
+ *
+ * @return Boolean Whether the type is supported by this extension
+ */
+ public function hasType($name);
+
+ /**
+ * Returns the extensions for the given type.
+ *
+ * @param string $name The name of the type
+ *
+ * @return FormTypeExtensionInterface[] An array of extensions as FormTypeExtensionInterface instances
+ */
+ public function getTypeExtensions($name);
+
+ /**
+ * Returns whether this extension provides type extensions for the given type.
+ *
+ * @param string $name The name of the type
+ *
+ * @return Boolean Whether the given type has extensions
+ */
+ public function hasTypeExtensions($name);
+
+ /**
+ * Returns the type guesser provided by this extension.
+ *
+ * @return FormTypeGuesserInterface|null The type guesser
+ */
+ public function getTypeGuesser();
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+class FormFactory implements FormFactoryInterface
+{
+ /**
+ * @var FormRegistryInterface
+ */
+ private $registry;
+
+ /**
+ * @var ResolvedFormTypeFactoryInterface
+ */
+ private $resolvedTypeFactory;
+
+ public function __construct(FormRegistryInterface $registry, ResolvedFormTypeFactoryInterface $resolvedTypeFactory)
+ {
+ $this->registry = $registry;
+ $this->resolvedTypeFactory = $resolvedTypeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function create($type = 'form', $data = null, array $options = array())
+ {
+ return $this->createBuilder($type, $data, $options)->getForm();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createNamed($name, $type = 'form', $data = null, array $options = array())
+ {
+ return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createForProperty($class, $property, $data = null, array $options = array())
+ {
+ return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createBuilder($type = 'form', $data = null, array $options = array())
+ {
+ $name = $type instanceof FormTypeInterface || $type instanceof ResolvedFormTypeInterface
+ ? $type->getName()
+ : $type;
+
+ return $this->createNamedBuilder($name, $type, $data, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array())
+ {
+ if (null !== $data && !array_key_exists('data', $options)) {
+ $options['data'] = $data;
+ }
+
+ if ($type instanceof FormTypeInterface) {
+ $type = $this->resolveType($type);
+ } elseif (is_string($type)) {
+ $type = $this->registry->getType($type);
+ } elseif (!$type instanceof ResolvedFormTypeInterface) {
+ throw new UnexpectedTypeException($type, 'string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface');
+ }
+
+ return $type->createBuilder($this, $name, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createBuilderForProperty($class, $property, $data = null, array $options = array())
+ {
+ if (null === $guesser = $this->registry->getTypeGuesser()) {
+ return $this->createNamedBuilder($property, 'text', $data, $options);
+ }
+
+ $typeGuess = $guesser->guessType($class, $property);
+ $maxLengthGuess = $guesser->guessMaxLength($class, $property);
+ $requiredGuess = $guesser->guessRequired($class, $property);
+ $patternGuess = $guesser->guessPattern($class, $property);
+
+ $type = $typeGuess ? $typeGuess->getType() : 'text';
+
+ $maxLength = $maxLengthGuess ? $maxLengthGuess->getValue() : null;
+ $pattern = $patternGuess ? $patternGuess->getValue() : null;
+
+ if (null !== $pattern) {
+ $options = array_merge(array('pattern' => $pattern), $options);
+ }
+
+ if (null !== $maxLength) {
+ $options = array_merge(array('max_length' => $maxLength), $options);
+ }
+
+ if ($requiredGuess) {
+ $options = array_merge(array('required' => $requiredGuess->getValue()), $options);
+ }
+
+ // user options may override guessed options
+ if ($typeGuess) {
+ $options = array_merge($typeGuess->getOptions(), $options);
+ }
+
+ return $this->createNamedBuilder($property, $type, $data, $options);
+ }
+
+ /**
+ * Wraps a type into a ResolvedFormTypeInterface implementation and connects
+ * it with its parent type.
+ *
+ * @param FormTypeInterface $type The type to resolve.
+ *
+ * @return ResolvedFormTypeInterface The resolved type.
+ */
+ private function resolveType(FormTypeInterface $type)
+ {
+ $parentType = $type->getParent();
+
+ if ($parentType instanceof FormTypeInterface) {
+ $parentType = $this->resolveType($parentType);
+ } elseif (null !== $parentType) {
+ $parentType = $this->registry->getType($parentType);
+ }
+
+ return $this->resolvedTypeFactory->createResolvedType(
+ $type,
+ // Type extensions are not supported for unregistered type instances,
+ // i.e. type instances that are passed to the FormFactory directly,
+ // nor for their parents, if getParent() also returns a type instance.
+ array(),
+ $parentType
+ );
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * The default implementation of FormFactoryBuilderInterface.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormFactoryBuilder implements FormFactoryBuilderInterface
+{
+ /**
+ * @var ResolvedFormTypeFactoryInterface
+ */
+ private $resolvedTypeFactory;
+
+ /**
+ * @var array
+ */
+ private $extensions = array();
+
+ /**
+ * @var array
+ */
+ private $types = array();
+
+ /**
+ * @var array
+ */
+ private $typeExtensions = array();
+
+ /**
+ * @var array
+ */
+ private $typeGuessers = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory)
+ {
+ $this->resolvedTypeFactory = $resolvedTypeFactory;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addExtension(FormExtensionInterface $extension)
+ {
+ $this->extensions[] = $extension;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addExtensions(array $extensions)
+ {
+ $this->extensions = array_merge($this->extensions, $extensions);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addType(FormTypeInterface $type)
+ {
+ $this->types[$type->getName()] = $type;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTypes(array $types)
+ {
+ foreach ($types as $type) {
+ $this->types[$type->getName()] = $type;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTypeExtension(FormTypeExtensionInterface $typeExtension)
+ {
+ $this->typeExtensions[$typeExtension->getExtendedType()][] = $typeExtension;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTypeExtensions(array $typeExtensions)
+ {
+ foreach ($typeExtensions as $typeExtension) {
+ $this->typeExtensions[$typeExtension->getExtendedType()][] = $typeExtension;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser)
+ {
+ $this->typeGuessers[] = $typeGuesser;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTypeGuessers(array $typeGuessers)
+ {
+ $this->typeGuessers = array_merge($this->typeGuessers, $typeGuessers);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormFactory()
+ {
+ $extensions = $this->extensions;
+
+ if (count($this->types) > 0 || count($this->typeExtensions) > 0 || count($this->typeGuessers) > 0) {
+ if (count($this->typeGuessers) > 1) {
+ $typeGuesser = new FormTypeGuesserChain($this->typeGuessers);
+ } else {
+ $typeGuesser = isset($this->typeGuessers[0]) ? $this->typeGuessers[0] : null;
+ }
+
+ $extensions[] = new PreloadedExtension($this->types, $this->typeExtensions, $typeGuesser);
+ }
+
+ $resolvedTypeFactory = $this->resolvedTypeFactory ?: new ResolvedFormTypeFactory();
+ $registry = new FormRegistry($extensions, $resolvedTypeFactory);
+
+ return new FormFactory($registry, $resolvedTypeFactory);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A builder for FormFactoryInterface objects.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormFactoryBuilderInterface
+{
+ /**
+ * Sets the factory for creating ResolvedFormTypeInterface instances.
+ *
+ * @param ResolvedFormTypeFactoryInterface $resolvedTypeFactory
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function setResolvedTypeFactory(ResolvedFormTypeFactoryInterface $resolvedTypeFactory);
+
+ /**
+ * Adds an extension to be loaded by the factory.
+ *
+ * @param FormExtensionInterface $extension The extension.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addExtension(FormExtensionInterface $extension);
+
+ /**
+ * Adds a list of extensions to be loaded by the factory.
+ *
+ * @param array $extensions The extensions.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addExtensions(array $extensions);
+
+ /**
+ * Adds a form type to the factory.
+ *
+ * @param FormTypeInterface $type The form type.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addType(FormTypeInterface $type);
+
+ /**
+ * Adds a list of form types to the factory.
+ *
+ * @param array $types The form types.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addTypes(array $types);
+
+ /**
+ * Adds a form type extension to the factory.
+ *
+ * @param FormTypeExtensionInterface $typeExtension The form type extension.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addTypeExtension(FormTypeExtensionInterface $typeExtension);
+
+ /**
+ * Adds a list of form type extensions to the factory.
+ *
+ * @param array $typeExtensions The form type extensions.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addTypeExtensions(array $typeExtensions);
+
+ /**
+ * Adds a type guesser to the factory.
+ *
+ * @param FormTypeGuesserInterface $typeGuesser The type guesser.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addTypeGuesser(FormTypeGuesserInterface $typeGuesser);
+
+ /**
+ * Adds a list of type guessers to the factory.
+ *
+ * @param array $typeGuessers The type guessers.
+ *
+ * @return FormFactoryBuilderInterface The builder.
+ */
+ public function addTypeGuessers(array $typeGuessers);
+
+ /**
+ * Builds and returns the factory.
+ *
+ * @return FormFactoryInterface The form factory.
+ */
+ public function getFormFactory();
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormFactoryInterface
+{
+ /**
+ * Returns a form.
+ *
+ * @see createBuilder()
+ *
+ * @param string|FormTypeInterface $type The type of the form
+ * @param mixed $data The initial data
+ * @param array $options The options
+ *
+ * @return FormInterface The form named after the type
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the given type
+ */
+ public function create($type = 'form', $data = null, array $options = array());
+
+ /**
+ * Returns a form.
+ *
+ * @see createNamedBuilder()
+ *
+ * @param string|integer $name The name of the form
+ * @param string|FormTypeInterface $type The type of the form
+ * @param mixed $data The initial data
+ * @param array $options The options
+ *
+ * @return FormInterface The form
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the given type
+ */
+ public function createNamed($name, $type = 'form', $data = null, array $options = array());
+
+ /**
+ * Returns a form for a property of a class.
+ *
+ * @see createBuilderForProperty()
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ * @param mixed $data The initial data
+ * @param array $options The options for the builder
+ *
+ * @return FormInterface The form named after the property
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the form type
+ */
+ public function createForProperty($class, $property, $data = null, array $options = array());
+
+ /**
+ * Returns a form builder.
+ *
+ * @param string|FormTypeInterface $type The type of the form
+ * @param mixed $data The initial data
+ * @param array $options The options
+ *
+ * @return FormBuilderInterface The form builder
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the given type
+ */
+ public function createBuilder($type = 'form', $data = null, array $options = array());
+
+ /**
+ * Returns a form builder.
+ *
+ * @param string|integer $name The name of the form
+ * @param string|FormTypeInterface $type The type of the form
+ * @param mixed $data The initial data
+ * @param array $options The options
+ *
+ * @return FormBuilderInterface The form builder
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the given type
+ */
+ public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array());
+
+ /**
+ * Returns a form builder for a property of a class.
+ *
+ * If any of the 'max_length', 'required' and type options can be guessed,
+ * and are not provided in the options argument, the guessed value is used.
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ * @param mixed $data The initial data
+ * @param array $options The options for the builder
+ *
+ * @return FormBuilderInterface The form builder named after the property
+ *
+ * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException if any given option is not applicable to the form type
+ */
+ public function createBuilderForProperty($class, $property, $data = null, array $options = array());
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A form group bundling multiple forms in a hierarchical structure.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormInterface extends \ArrayAccess, \Traversable, \Countable
+{
+ /**
+ * Sets the parent form.
+ *
+ * @param FormInterface|null $parent The parent form or null if it's the root.
+ *
+ * @return FormInterface The form instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ * @throws Exception\LogicException When trying to set a parent for a form with
+ * an empty name.
+ */
+ public function setParent(FormInterface $parent = null);
+
+ /**
+ * Returns the parent form.
+ *
+ * @return FormInterface|null The parent form or null if there is none.
+ */
+ public function getParent();
+
+ /**
+ * Adds a child to the form.
+ *
+ * @param FormInterface|string|integer $child The FormInterface instance or the name of the child.
+ * @param string|null $type The child's type, if a name was passed.
+ * @param array $options The child's options, if a name was passed.
+ *
+ * @return FormInterface The form instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ * @throws Exception\LogicException When trying to add a child to a non-compound form.
+ * @throws Exception\UnexpectedTypeException If $child or $type has an unexpected type.
+ */
+ public function add($child, $type = null, array $options = array());
+
+ /**
+ * Returns the child with the given name.
+ *
+ * @param string $name The name of the child
+ *
+ * @return FormInterface The child form
+ *
+ * @throws \OutOfBoundsException If the named child does not exist.
+ */
+ public function get($name);
+
+ /**
+ * Returns whether a child with the given name exists.
+ *
+ * @param string $name The name of the child
+ *
+ * @return Boolean
+ */
+ public function has($name);
+
+ /**
+ * Removes a child from the form.
+ *
+ * @param string $name The name of the child to remove
+ *
+ * @return FormInterface The form instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ */
+ public function remove($name);
+
+ /**
+ * Returns all children in this group.
+ *
+ * @return FormInterface[] An array of FormInterface instances
+ */
+ public function all();
+
+ /**
+ * Returns all errors.
+ *
+ * @return FormError[] An array of FormError instances that occurred during validation
+ */
+ public function getErrors();
+
+ /**
+ * Updates the form with default data.
+ *
+ * @param mixed $modelData The data formatted as expected for the underlying object
+ *
+ * @return FormInterface The form instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ * @throws Exception\LogicException If listeners try to call setData in a cycle. Or if
+ * the view data does not match the expected type
+ * according to {@link FormConfigInterface::getDataClass}.
+ */
+ public function setData($modelData);
+
+ /**
+ * Returns the data in the format needed for the underlying object.
+ *
+ * @return mixed
+ */
+ public function getData();
+
+ /**
+ * Returns the normalized data of the field.
+ *
+ * @return mixed When the field is not submitted, the default data is returned.
+ * When the field is submitted, the normalized submitted data is
+ * returned if the field is valid, null otherwise.
+ */
+ public function getNormData();
+
+ /**
+ * Returns the data transformed by the value transformer.
+ *
+ * @return mixed
+ */
+ public function getViewData();
+
+ /**
+ * Returns the extra data.
+ *
+ * @return array The submitted data which do not belong to a child
+ */
+ public function getExtraData();
+
+ /**
+ * Returns the form's configuration.
+ *
+ * @return FormConfigInterface The configuration.
+ */
+ public function getConfig();
+
+ /**
+ * Returns whether the form is submitted.
+ *
+ * @return Boolean true if the form is submitted, false otherwise
+ */
+ public function isSubmitted();
+
+ /**
+ * Returns the name by which the form is identified in forms.
+ *
+ * @return string The name of the form.
+ */
+ public function getName();
+
+ /**
+ * Returns the property path that the form is mapped to.
+ *
+ * @return \Symfony\Component\PropertyAccess\PropertyPathInterface The property path.
+ */
+ public function getPropertyPath();
+
+ /**
+ * Adds an error to this form.
+ *
+ * @param FormError $error
+ *
+ * @return FormInterface The form instance
+ */
+ public function addError(FormError $error);
+
+ /**
+ * Returns whether the form and all children are valid.
+ *
+ * If the form is not submitted, this method always returns false.
+ *
+ * @return Boolean
+ */
+ public function isValid();
+
+ /**
+ * Returns whether the form is required to be filled out.
+ *
+ * If the form has a parent and the parent is not required, this method
+ * will always return false. Otherwise the value set with setRequired()
+ * is returned.
+ *
+ * @return Boolean
+ */
+ public function isRequired();
+
+ /**
+ * Returns whether this form is disabled.
+ *
+ * The content of a disabled form is displayed, but not allowed to be
+ * modified. The validation of modified disabled forms should fail.
+ *
+ * Forms whose parents are disabled are considered disabled regardless of
+ * their own state.
+ *
+ * @return Boolean
+ */
+ public function isDisabled();
+
+ /**
+ * Returns whether the form is empty.
+ *
+ * @return Boolean
+ */
+ public function isEmpty();
+
+ /**
+ * Returns whether the data in the different formats is synchronized.
+ *
+ * @return Boolean
+ */
+ public function isSynchronized();
+
+ /**
+ * Initializes the form tree.
+ *
+ * Should be called on the root form after constructing the tree.
+ *
+ * @return FormInterface The form instance.
+ */
+ public function initialize();
+
+ /**
+ * Inspects the given request and calls {@link submit()} if the form was
+ * submitted.
+ *
+ * Internally, the request is forwarded to the configured
+ * {@link RequestHandlerInterface} instance, which determines whether to
+ * submit the form or not.
+ *
+ * @param mixed $request The request to handle.
+ *
+ * @return FormInterface The form instance.
+ */
+ public function handleRequest($request = null);
+
+ /**
+ * Submits data to the form, transforms and validates it.
+ *
+ * @param null|string|array $submittedData The submitted data.
+ * @param Boolean $clearMissing Whether to set fields to NULL
+ * when they are missing in the
+ * submitted data.
+ *
+ * @return FormInterface The form instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ */
+ public function submit($submittedData, $clearMissing = true);
+
+ /**
+ * Returns the root of the form tree.
+ *
+ * @return FormInterface The root of the tree
+ */
+ public function getRoot();
+
+ /**
+ * Returns whether the field is the root of the form tree.
+ *
+ * @return Boolean
+ */
+ public function isRoot();
+
+ /**
+ * Creates a view.
+ *
+ * @param FormView $parent The parent view
+ *
+ * @return FormView The view
+ */
+ public function createView(FormView $parent = null);
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\ExceptionInterface;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * The central registry of the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormRegistry implements FormRegistryInterface
+{
+ /**
+ * Extensions
+ *
+ * @var FormExtensionInterface[] An array of FormExtensionInterface
+ */
+ private $extensions = array();
+
+ /**
+ * @var array
+ */
+ private $types = array();
+
+ /**
+ * @var FormTypeGuesserInterface|false|null
+ */
+ private $guesser = false;
+
+ /**
+ * @var ResolvedFormTypeFactoryInterface
+ */
+ private $resolvedTypeFactory;
+
+ /**
+ * Constructor.
+ *
+ * @param FormExtensionInterface[] $extensions An array of FormExtensionInterface
+ * @param ResolvedFormTypeFactoryInterface $resolvedTypeFactory The factory for resolved form types.
+ *
+ * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface
+ */
+ public function __construct(array $extensions, ResolvedFormTypeFactoryInterface $resolvedTypeFactory)
+ {
+ foreach ($extensions as $extension) {
+ if (!$extension instanceof FormExtensionInterface) {
+ throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormExtensionInterface');
+ }
+ }
+
+ $this->extensions = $extensions;
+ $this->resolvedTypeFactory = $resolvedTypeFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType($name)
+ {
+ if (!is_string($name)) {
+ throw new UnexpectedTypeException($name, 'string');
+ }
+
+ if (!isset($this->types[$name])) {
+ /** @var FormTypeInterface $type */
+ $type = null;
+
+ foreach ($this->extensions as $extension) {
+ /* @var FormExtensionInterface $extension */
+ if ($extension->hasType($name)) {
+ $type = $extension->getType($name);
+ break;
+ }
+ }
+
+ if (!$type) {
+ throw new InvalidArgumentException(sprintf('Could not load type "%s"', $name));
+ }
+
+ $this->resolveAndAddType($type);
+ }
+
+ return $this->types[$name];
+ }
+
+ /**
+ * Wraps a type into a ResolvedFormTypeInterface implementation and connects
+ * it with its parent type.
+ *
+ * @param FormTypeInterface $type The type to resolve.
+ *
+ * @return ResolvedFormTypeInterface The resolved type.
+ */
+ private function resolveAndAddType(FormTypeInterface $type)
+ {
+ $parentType = $type->getParent();
+
+ if ($parentType instanceof FormTypeInterface) {
+ $this->resolveAndAddType($parentType);
+ $parentType = $parentType->getName();
+ }
+
+ $typeExtensions = array();
+
+ foreach ($this->extensions as $extension) {
+ /* @var FormExtensionInterface $extension */
+ $typeExtensions = array_merge(
+ $typeExtensions,
+ $extension->getTypeExtensions($type->getName())
+ );
+ }
+
+ $this->types[$type->getName()] = $this->resolvedTypeFactory->createResolvedType(
+ $type,
+ $typeExtensions,
+ $parentType ? $this->getType($parentType) : null
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasType($name)
+ {
+ if (isset($this->types[$name])) {
+ return true;
+ }
+
+ try {
+ $this->getType($name);
+ } catch (ExceptionInterface $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeGuesser()
+ {
+ if (false === $this->guesser) {
+ $guessers = array();
+
+ foreach ($this->extensions as $extension) {
+ /* @var FormExtensionInterface $extension */
+ $guesser = $extension->getTypeGuesser();
+
+ if ($guesser) {
+ $guessers[] = $guesser;
+ }
+ }
+
+ $this->guesser = !empty($guessers) ? new FormTypeGuesserChain($guessers) : null;
+ }
+
+ return $this->guesser;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtensions()
+ {
+ return $this->extensions;
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * The central registry of the Form component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormRegistryInterface
+{
+ /**
+ * Returns a form type by name.
+ *
+ * This methods registers the type extensions from the form extensions.
+ *
+ * @param string $name The name of the type
+ *
+ * @return ResolvedFormTypeInterface The type
+ *
+ * @throws Exception\UnexpectedTypeException if the passed name is not a string
+ * @throws Exception\InvalidArgumentException if the type can not be retrieved from any extension
+ */
+ public function getType($name);
+
+ /**
+ * Returns whether the given form type is supported.
+ *
+ * @param string $name The name of the type
+ *
+ * @return Boolean Whether the type is supported
+ */
+ public function hasType($name);
+
+ /**
+ * Returns the guesser responsible for guessing types.
+ *
+ * @return FormTypeGuesserInterface|null
+ */
+ public function getTypeGuesser();
+
+ /**
+ * Returns the extensions loaded by the framework.
+ *
+ * @return array
+ */
+ public function getExtensions();
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\LogicException;
+use Symfony\Component\Form\Exception\BadMethodCallException;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+
+/**
+ * Renders a form into HTML using a rendering engine.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormRenderer implements FormRendererInterface
+{
+ const CACHE_KEY_VAR = 'unique_block_prefix';
+
+ /**
+ * @var FormRendererEngineInterface
+ */
+ private $engine;
+
+ /**
+ * @var CsrfProviderInterface
+ */
+ private $csrfProvider;
+
+ /**
+ * @var array
+ */
+ private $blockNameHierarchyMap = array();
+
+ /**
+ * @var array
+ */
+ private $hierarchyLevelMap = array();
+
+ /**
+ * @var array
+ */
+ private $variableStack = array();
+
+ public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
+ {
+ $this->engine = $engine;
+ $this->csrfProvider = $csrfProvider;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEngine()
+ {
+ return $this->engine;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTheme(FormView $view, $themes)
+ {
+ $this->engine->setTheme($view, $themes);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderCsrfToken($intention)
+ {
+ if (null === $this->csrfProvider) {
+ throw new BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
+ }
+
+ return $this->csrfProvider->generateCsrfToken($intention);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderBlock(FormView $view, $blockName, array $variables = array())
+ {
+ $resource = $this->engine->getResourceForBlockName($view, $blockName);
+
+ if (!$resource) {
+ throw new LogicException(sprintf('No block "%s" found while rendering the form.', $blockName));
+ }
+
+ $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
+
+ // The variables are cached globally for a view (instead of for the
+ // current suffix)
+ if (!isset($this->variableStack[$viewCacheKey])) {
+ $this->variableStack[$viewCacheKey] = array();
+
+ // The default variable scope contains all view variables, merged with
+ // the variables passed explicitly to the helper
+ $scopeVariables = $view->vars;
+
+ $varInit = true;
+ } else {
+ // Reuse the current scope and merge it with the explicitly passed variables
+ $scopeVariables = end($this->variableStack[$viewCacheKey]);
+
+ $varInit = false;
+ }
+
+ // Merge the passed with the existing attributes
+ if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
+ $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
+ }
+
+ // Merge the passed with the exist *label* attributes
+ if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
+ $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
+ }
+
+ // Do not use array_replace_recursive(), otherwise array variables
+ // cannot be overwritten
+ $variables = array_replace($scopeVariables, $variables);
+
+ $this->variableStack[$viewCacheKey][] = $variables;
+
+ // Do the rendering
+ $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
+
+ // Clear the stack
+ array_pop($this->variableStack[$viewCacheKey]);
+
+ if ($varInit) {
+ unset($this->variableStack[$viewCacheKey]);
+ }
+
+ return $html;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $variables = array())
+ {
+ $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
+
+ if ($renderOnlyOnce && $view->isRendered()) {
+ return '';
+ }
+
+ // The cache key for storing the variables and types
+ $viewCacheKey = $view->vars[self::CACHE_KEY_VAR];
+ $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix;
+
+ // In templates, we have to deal with two kinds of block hierarchies:
+ //
+ // +---------+ +---------+
+ // | Theme B | -------> | Theme A |
+ // +---------+ +---------+
+ //
+ // form_widget -------> form_widget
+ // ^
+ // |
+ // choice_widget -----> choice_widget
+ //
+ // The first kind of hierarchy is the theme hierarchy. This allows to
+ // override the block "choice_widget" from Theme A in the extending
+ // Theme B. This kind of inheritance needs to be supported by the
+ // template engine and, for example, offers "parent()" or similar
+ // functions to fall back from the custom to the parent implementation.
+ //
+ // The second kind of hierarchy is the form type hierarchy. This allows
+ // to implement a custom "choice_widget" block (no matter in which theme),
+ // or to fallback to the block of the parent type, which would be
+ // "form_widget" in this example (again, no matter in which theme).
+ // If the designer wants to explicitly fallback to "form_widget" in his
+ // custom "choice_widget", for example because he only wants to wrap
+ // a <div> around the original implementation, he can simply call the
+ // widget() function again to render the block for the parent type.
+ //
+ // The second kind is implemented in the following blocks.
+ if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
+ // INITIAL CALL
+ // Calculate the hierarchy of template blocks and start on
+ // the bottom level of the hierarchy (= "_<id>_<section>" block)
+ $blockNameHierarchy = array();
+ foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
+ $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
+ }
+ $hierarchyLevel = count($blockNameHierarchy) - 1;
+
+ $hierarchyInit = true;
+ } else {
+ // RECURSIVE CALL
+ // If a block recursively calls searchAndRenderBlock() again, resume rendering
+ // using the parent type in the hierarchy.
+ $blockNameHierarchy = $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
+ $hierarchyLevel = $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
+
+ $hierarchyInit = false;
+ }
+
+ // The variables are cached globally for a view (instead of for the
+ // current suffix)
+ if (!isset($this->variableStack[$viewCacheKey])) {
+ $this->variableStack[$viewCacheKey] = array();
+
+ // The default variable scope contains all view variables, merged with
+ // the variables passed explicitly to the helper
+ $scopeVariables = $view->vars;
+
+ $varInit = true;
+ } else {
+ // Reuse the current scope and merge it with the explicitly passed variables
+ $scopeVariables = end($this->variableStack[$viewCacheKey]);
+
+ $varInit = false;
+ }
+
+ // Load the resource where this block can be found
+ $resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel);
+
+ // Update the current hierarchy level to the one at which the resource was
+ // found. For example, if looking for "choice_widget", but only a resource
+ // is found for its parent "form_widget", then the level is updated here
+ // to the parent level.
+ $hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockNameHierarchy, $hierarchyLevel);
+
+ // The actually existing block name in $resource
+ $blockName = $blockNameHierarchy[$hierarchyLevel];
+
+ // Escape if no resource exists for this block
+ if (!$resource) {
+ throw new LogicException(sprintf(
+ 'Unable to render the form as none of the following blocks exist: "%s".',
+ implode('", "', array_reverse($blockNameHierarchy))
+ ));
+ }
+
+ // Merge the passed with the existing attributes
+ if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
+ $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
+ }
+
+ // Merge the passed with the exist *label* attributes
+ if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
+ $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
+ }
+
+ // Do not use array_replace_recursive(), otherwise array variables
+ // cannot be overwritten
+ $variables = array_replace($scopeVariables, $variables);
+
+ // In order to make recursive calls possible, we need to store the block hierarchy,
+ // the current level of the hierarchy and the variables so that this method can
+ // resume rendering one level higher of the hierarchy when it is called recursively.
+ //
+ // We need to store these values in maps (associative arrays) because within a
+ // call to widget() another call to widget() can be made, but for a different view
+ // object. These nested calls should not override each other.
+ $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
+ $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
+
+ // We also need to store the variables for the view so that we can render other
+ // blocks for the same view using the same variables as in the outer block.
+ $this->variableStack[$viewCacheKey][] = $variables;
+
+ // Do the rendering
+ $html = $this->engine->renderBlock($view, $resource, $blockName, $variables);
+
+ // Clear the stack
+ array_pop($this->variableStack[$viewCacheKey]);
+
+ // Clear the caches if they were filled for the first time within
+ // this function call
+ if ($hierarchyInit) {
+ unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey]);
+ unset($this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
+ }
+
+ if ($varInit) {
+ unset($this->variableStack[$viewCacheKey]);
+ }
+
+ if ($renderOnlyOnce) {
+ $view->setRendered();
+ }
+
+ return $html;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function humanize($text)
+ {
+ return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text))));
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Adapter for rendering form templates with a specific templating engine.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormRendererEngineInterface
+{
+ /**
+ * Sets the theme(s) to be used for rendering a view and its children.
+ *
+ * @param FormView $view The view to assign the theme(s) to.
+ * @param mixed $themes The theme(s). The type of these themes
+ * is open to the implementation.
+ */
+ public function setTheme(FormView $view, $themes);
+
+ /**
+ * Returns the resource for a block name.
+ *
+ * The resource is first searched in the themes attached to $view, then
+ * in the themes of its parent view and so on, until a resource was found.
+ *
+ * The type of the resource is decided by the implementation. The resource
+ * is later passed to {@link renderBlock()} by the rendering algorithm.
+ *
+ * @param FormView $view The view for determining the used themes.
+ * First the themes attached directly to the
+ * view with {@link setTheme()} are considered,
+ * then the ones of its parent etc.
+ * @param string $blockName The name of the block to render.
+ *
+ * @return mixed The renderer resource or false, if none was found.
+ */
+ public function getResourceForBlockName(FormView $view, $blockName);
+
+ /**
+ * Returns the resource for a block hierarchy.
+ *
+ * A block hierarchy is an array which starts with the root of the hierarchy
+ * and continues with the child of that root, the child of that child etc.
+ * The following is an example for a block hierarchy:
+ *
+ * <code>
+ * form_widget
+ * text_widget
+ * url_widget
+ * </code>
+ *
+ * In this example, "url_widget" is the most specific block, while the other
+ * blocks are its ancestors in the hierarchy.
+ *
+ * The second parameter $hierarchyLevel determines the level of the hierarchy
+ * that should be rendered. For example, if $hierarchyLevel is 2 for the
+ * above hierarchy, the engine will first look for the block "url_widget",
+ * then, if that does not exist, for the block "text_widget" etc.
+ *
+ * The type of the resource is decided by the implementation. The resource
+ * is later passed to {@link renderBlock()} by the rendering algorithm.
+ *
+ * @param FormView $view The view for determining the
+ * used themes. First the themes
+ * attached directly to the view
+ * with {@link setTheme()} are
+ * considered, then the ones of
+ * its parent etc.
+ * @param array $blockNameHierarchy The block name hierarchy, with
+ * the root block at the beginning.
+ * @param integer $hierarchyLevel The level in the hierarchy at
+ * which to start looking. Level 0
+ * indicates the root block, i.e.
+ * the first element of
+ * $blockNameHierarchy.
+ *
+ * @return mixed The renderer resource or false, if none was found.
+ */
+ public function getResourceForBlockNameHierarchy(FormView $view, array $blockNameHierarchy, $hierarchyLevel);
+
+ /**
+ * Returns the hierarchy level at which a resource can be found.
+ *
+ * A block hierarchy is an array which starts with the root of the hierarchy
+ * and continues with the child of that root, the child of that child etc.
+ * The following is an example for a block hierarchy:
+ *
+ * <code>
+ * form_widget
+ * text_widget
+ * url_widget
+ * </code>
+ *
+ * The second parameter $hierarchyLevel determines the level of the hierarchy
+ * that should be rendered.
+ *
+ * If we call this method with the hierarchy level 2, the engine will first
+ * look for a resource for block "url_widget". If such a resource exists,
+ * the method returns 2. Otherwise it tries to find a resource for block
+ * "text_widget" (at level 1) and, again, returns 1 if a resource was found.
+ * The method continues to look for resources until the root level was
+ * reached and nothing was found. In this case false is returned.
+ *
+ * The type of the resource is decided by the implementation. The resource
+ * is later passed to {@link renderBlock()} by the rendering algorithm.
+ *
+ * @param FormView $view The view for determining the
+ * used themes. First the themes
+ * attached directly to the view
+ * with {@link setTheme()} are
+ * considered, then the ones of
+ * its parent etc.
+ * @param array $blockNameHierarchy The block name hierarchy, with
+ * the root block at the beginning.
+ * @param integer $hierarchyLevel The level in the hierarchy at
+ * which to start looking. Level 0
+ * indicates the root block, i.e.
+ * the first element of
+ * $blockNameHierarchy.
+ *
+ * @return integer|Boolean The hierarchy level or false, if no resource was found.
+ */
+ public function getResourceHierarchyLevel(FormView $view, array $blockNameHierarchy, $hierarchyLevel);
+
+ /**
+ * Renders a block in the given renderer resource.
+ *
+ * The resource can be obtained by calling {@link getResourceForBlock()}
+ * or {@link getResourceForBlockHierarchy()}. The type of the resource is
+ * decided by the implementation.
+ *
+ * @param FormView $view The view to render.
+ * @param mixed $resource The renderer resource.
+ * @param string $blockName The name of the block to render.
+ * @param array $variables The variables to pass to the template.
+ *
+ * @return string The HTML markup.
+ */
+ public function renderBlock(FormView $view, $resource, $blockName, array $variables = array());
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Renders a form into HTML.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormRendererInterface
+{
+ /**
+ * Returns the engine used by this renderer.
+ *
+ * @return FormRendererEngineInterface The renderer engine.
+ */
+ public function getEngine();
+
+ /**
+ * Sets the theme(s) to be used for rendering a view and its children.
+ *
+ * @param FormView $view The view to assign the theme(s) to.
+ * @param mixed $themes The theme(s). The type of these themes
+ * is open to the implementation.
+ */
+ public function setTheme(FormView $view, $themes);
+
+ /**
+ * Renders a named block of the form theme.
+ *
+ * @param FormView $view The view for which to render the block.
+ * @param string $blockName The name of the block.
+ * @param array $variables The variables to pass to the template.
+ *
+ * @return string The HTML markup
+ */
+ public function renderBlock(FormView $view, $blockName, array $variables = array());
+
+ /**
+ * Searches and renders a block for a given name suffix.
+ *
+ * The block is searched by combining the block names stored in the
+ * form view with the given suffix. If a block name is found, that
+ * block is rendered.
+ *
+ * If this method is called recursively, the block search is continued
+ * where a block was found before.
+ *
+ * @param FormView $view The view for which to render the block.
+ * @param string $blockNameSuffix The suffix of the block name.
+ * @param array $variables The variables to pass to the template.
+ *
+ * @return string The HTML markup
+ */
+ public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $variables = array());
+
+ /**
+ * Renders a CSRF token.
+ *
+ * Use this helper for CSRF protection without the overhead of creating a
+ * form.
+ *
+ * <code>
+ * <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
+ * </code>
+ *
+ * Check the token in your action using the same intention.
+ *
+ * <code>
+ * $csrfProvider = $this->get('form.csrf_provider');
+ * if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
+ * throw new \RuntimeException('CSRF attack detected.');
+ * }
+ * </code>
+ *
+ * @param string $intention The intention of the protected action
+ *
+ * @return string A CSRF token
+ */
+ public function renderCsrfToken($intention);
+
+ /**
+ * Makes a technical name human readable.
+ *
+ * Sequences of underscores are replaced by single spaces. The first letter
+ * of the resulting string is capitalized, while all other letters are
+ * turned to lowercase.
+ *
+ * @param string $text The text to humanize.
+ *
+ * @return string The humanized text.
+ */
+ public function humanize($text);
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormTypeExtensionInterface
+{
+ /**
+ * Builds the form.
+ *
+ * This method is called after the extended type has built the form to
+ * further modify it.
+ *
+ * @see FormTypeInterface::buildForm()
+ *
+ * @param FormBuilderInterface $builder The form builder
+ * @param array $options The options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options);
+
+ /**
+ * Builds the view.
+ *
+ * This method is called after the extended type has built the view to
+ * further modify it.
+ *
+ * @see FormTypeInterface::buildView()
+ *
+ * @param FormView $view The view
+ * @param FormInterface $form The form
+ * @param array $options The options
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Finishes the view.
+ *
+ * This method is called after the extended type has finished the view to
+ * further modify it.
+ *
+ * @see FormTypeInterface::finishView()
+ *
+ * @param FormView $view The view
+ * @param FormInterface $form The form
+ * @param array $options The options
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Overrides the default options from the extended type.
+ *
+ * @param OptionsResolverInterface $resolver The resolver for the options.
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver);
+
+ /**
+ * Returns the name of the type being extended.
+ *
+ * @return string The name of the type being extended
+ */
+ public function getExtendedType();
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Guess\Guess;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+class FormTypeGuesserChain implements FormTypeGuesserInterface
+{
+ private $guessers = array();
+
+ /**
+ * Constructor.
+ *
+ * @param array $guessers Guessers as instances of FormTypeGuesserInterface
+ *
+ * @throws UnexpectedTypeException if any guesser does not implement FormTypeGuesserInterface
+ */
+ public function __construct(array $guessers)
+ {
+ foreach ($guessers as $guesser) {
+ if (!$guesser instanceof FormTypeGuesserInterface) {
+ throw new UnexpectedTypeException($guesser, 'Symfony\Component\Form\FormTypeGuesserInterface');
+ }
+
+ if ($guesser instanceof self) {
+ $this->guessers = array_merge($this->guessers, $guesser->guessers);
+ } else {
+ $this->guessers[] = $guesser;
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessType($class, $property)
+ {
+ return $this->guess(function ($guesser) use ($class, $property) {
+ return $guesser->guessType($class, $property);
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessRequired($class, $property)
+ {
+ return $this->guess(function ($guesser) use ($class, $property) {
+ return $guesser->guessRequired($class, $property);
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessMaxLength($class, $property)
+ {
+ return $this->guess(function ($guesser) use ($class, $property) {
+ return $guesser->guessMaxLength($class, $property);
+ });
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function guessPattern($class, $property)
+ {
+ return $this->guess(function ($guesser) use ($class, $property) {
+ return $guesser->guessPattern($class, $property);
+ });
+ }
+
+ /**
+ * Executes a closure for each guesser and returns the best guess from the
+ * return values
+ *
+ * @param \Closure $closure The closure to execute. Accepts a guesser
+ * as argument and should return a Guess instance
+ *
+ * @return Guess The guess with the highest confidence
+ */
+ private function guess(\Closure $closure)
+ {
+ $guesses = array();
+
+ foreach ($this->guessers as $guesser) {
+ if ($guess = $closure($guesser)) {
+ $guesses[] = $guess;
+ }
+ }
+
+ return Guess::getBestGuess($guesses);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormTypeGuesserInterface
+{
+ /**
+ * Returns a field guess for a property name of a class
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ *
+ * @return Guess\TypeGuess A guess for the field's type and options
+ */
+ public function guessType($class, $property);
+
+ /**
+ * Returns a guess whether a property of a class is required
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ *
+ * @return Guess\Guess A guess for the field's required setting
+ */
+ public function guessRequired($class, $property);
+
+ /**
+ * Returns a guess about the field's maximum length
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ *
+ * @return Guess\Guess A guess for the field's maximum length
+ */
+ public function guessMaxLength($class, $property);
+
+ /**
+ * Returns a guess about the field's pattern
+ *
+ * - When you have a min value, you guess a min length of this min (LOW_CONFIDENCE) , lines below
+ * - If this value is a float type, this is wrong so you guess null with MEDIUM_CONFIDENCE to override the previous guess.
+ * Example:
+ * You want a float greater than 5, 4.512313 is not valid but length(4.512314) > length(5)
+ * @link https://github.com/symfony/symfony/pull/3927
+ *
+ * @param string $class The fully qualified class name
+ * @param string $property The name of the property to guess for
+ *
+ * @return Guess\Guess A guess for the field's required pattern
+ */
+ public function guessPattern($class, $property);
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface FormTypeInterface
+{
+ /**
+ * Builds the form.
+ *
+ * This method is called for each type in the hierarchy starting form the
+ * top most type. Type extensions can further modify the form.
+ *
+ * @see FormTypeExtensionInterface::buildForm()
+ *
+ * @param FormBuilderInterface $builder The form builder
+ * @param array $options The options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options);
+
+ /**
+ * Builds the form view.
+ *
+ * This method is called for each type in the hierarchy starting form the
+ * top most type. Type extensions can further modify the view.
+ *
+ * A view of a form is built before the views of the child forms are built.
+ * This means that you cannot access child views in this method. If you need
+ * to do so, move your logic to {@link finishView()} instead.
+ *
+ * @see FormTypeExtensionInterface::buildView()
+ *
+ * @param FormView $view The view
+ * @param FormInterface $form The form
+ * @param array $options The options
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Finishes the form view.
+ *
+ * This method gets called for each type in the hierarchy starting form the
+ * top most type. Type extensions can further modify the view.
+ *
+ * When this method is called, views of the form's children have already
+ * been built and finished and can be accessed. You should only implement
+ * such logic in this method that actually accesses child views. For everything
+ * else you are recommended to implement {@link buildView()} instead.
+ *
+ * @see FormTypeExtensionInterface::finishView()
+ *
+ * @param FormView $view The view
+ * @param FormInterface $form The form
+ * @param array $options The options
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Sets the default options for this type.
+ *
+ * @param OptionsResolverInterface $resolver The resolver for the options.
+ */
+ public function setDefaultOptions(OptionsResolverInterface $resolver);
+
+ /**
+ * Returns the name of the parent type.
+ *
+ * You can also return a type instance from this method, although doing so
+ * is discouraged because it leads to a performance penalty. The support
+ * for returning type instances may be dropped from future releases.
+ *
+ * @return string|null|FormTypeInterface The name of the parent type if any, null otherwise.
+ */
+ public function getParent();
+
+ /**
+ * Returns the name of this type.
+ *
+ * @return string The name of this type
+ */
+ public function getName();
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\BadMethodCallException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
+{
+ /**
+ * The variables assigned to this view.
+ * @var array
+ */
+ public $vars = array(
+ 'value' => null,
+ 'attr' => array(),
+ );
+
+ /**
+ * The parent view.
+ * @var FormView
+ */
+ public $parent;
+
+ /**
+ * The child views.
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * Is the form attached to this renderer rendered?
+ *
+ * Rendering happens when either the widget or the row method was called.
+ * Row implicitly includes widget, however certain rendering mechanisms
+ * have to skip widget rendering when a row is rendered.
+ *
+ * @var Boolean
+ */
+ private $rendered = false;
+
+ public function __construct(FormView $parent = null)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Returns whether the view was already rendered.
+ *
+ * @return Boolean Whether this view's widget is rendered.
+ */
+ public function isRendered()
+ {
+ $hasChildren = 0 < count($this->children);
+
+ if (true === $this->rendered || !$hasChildren) {
+ return $this->rendered;
+ }
+
+ if ($hasChildren) {
+ foreach ($this->children as $child) {
+ if (!$child->isRendered()) {
+ return false;
+ }
+ }
+
+ return $this->rendered = true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Marks the view as rendered.
+ *
+ * @return FormView The view object.
+ */
+ public function setRendered()
+ {
+ $this->rendered = true;
+
+ return $this;
+ }
+
+ /**
+ * Returns a child by name (implements \ArrayAccess).
+ *
+ * @param string $name The child name
+ *
+ * @return FormView The child view
+ */
+ public function offsetGet($name)
+ {
+ return $this->children[$name];
+ }
+
+ /**
+ * Returns whether the given child exists (implements \ArrayAccess).
+ *
+ * @param string $name The child name
+ *
+ * @return Boolean Whether the child view exists
+ */
+ public function offsetExists($name)
+ {
+ return isset($this->children[$name]);
+ }
+
+ /**
+ * Implements \ArrayAccess.
+ *
+ * @throws BadMethodCallException always as setting a child by name is not allowed
+ */
+ public function offsetSet($name, $value)
+ {
+ throw new BadMethodCallException('Not supported');
+ }
+
+ /**
+ * Removes a child (implements \ArrayAccess).
+ *
+ * @param string $name The child name
+ */
+ public function offsetUnset($name)
+ {
+ unset($this->children[$name]);
+ }
+
+ /**
+ * Returns an iterator to iterate over children (implements \IteratorAggregate)
+ *
+ * @return \ArrayIterator The iterator
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->children);
+ }
+
+ /**
+ * Implements \Countable.
+ *
+ * @return integer The number of children views
+ */
+ public function count()
+ {
+ return count($this->children);
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Extension\Core\CoreExtension;
+
+/**
+ * Entry point of the Form component.
+ *
+ * Use this class to conveniently create new form factories:
+ *
+ * <code>
+ * use Symfony\Component\Form\Forms;
+ *
+ * $formFactory = Forms::createFormFactory();
+ *
+ * $form = $formFactory->createBuilder()
+ * ->add('firstName', 'text')
+ * ->add('lastName', 'text')
+ * ->add('age', 'integer')
+ * ->add('gender', 'choice', array(
+ * 'choices' => array('m' => 'Male', 'f' => 'Female'),
+ * ))
+ * ->getForm();
+ * </code>
+ *
+ * You can also add custom extensions to the form factory:
+ *
+ * <code>
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new AcmeExtension())
+ * ->getFormFactory();
+ * </code>
+ *
+ * If you create custom form types or type extensions, it is
+ * generally recommended to create your own extensions that lazily
+ * load these types and type extensions. In projects where performance
+ * does not matter that much, you can also pass them directly to the
+ * form factory:
+ *
+ * <code>
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addType(new PersonType())
+ * ->addType(new PhoneNumberType())
+ * ->addTypeExtension(new FormTypeHelpTextExtension())
+ * ->getFormFactory();
+ * </code>
+ *
+ * Support for CSRF protection is provided by the CsrfExtension.
+ * This extension needs a CSRF provider with a strong secret
+ * (e.g. a 20 character long random string). The default
+ * implementation for this is DefaultCsrfProvider:
+ *
+ * <code>
+ * use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
+ * use Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider;
+ *
+ * $secret = 'V8a5Z97e...';
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new CsrfExtension(new DefaultCsrfProvider($secret)))
+ * ->getFormFactory();
+ * </code>
+ *
+ * Support for the HttpFoundation is provided by the
+ * HttpFoundationExtension. You are also advised to load the CSRF
+ * extension with the driver for HttpFoundation's Session class:
+ *
+ * <code>
+ * use Symfony\Component\HttpFoundation\Session\Session;
+ * use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
+ * use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
+ * use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider;
+ *
+ * $session = new Session();
+ * $secret = 'V8a5Z97e...';
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new HttpFoundationExtension())
+ * ->addExtension(new CsrfExtension(new SessionCsrfProvider($session, $secret)))
+ * ->getFormFactory();
+ * </code>
+ *
+ * Support for the Validator component is provided by ValidatorExtension.
+ * This extension needs a validator object to function properly:
+ *
+ * <code>
+ * use Symfony\Component\Validator\Validation;
+ * use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
+ *
+ * $validator = Validation::createValidator();
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new ValidatorExtension($validator))
+ * ->getFormFactory();
+ * </code>
+ *
+ * Support for the Templating component is provided by TemplatingExtension.
+ * This extension needs a PhpEngine object for rendering forms. As second
+ * argument you should pass the names of the default themes. Here is an
+ * example for using the default layout with "<div>" tags:
+ *
+ * <code>
+ * use Symfony\Component\Form\Extension\Templating\TemplatingExtension;
+ *
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new TemplatingExtension($engine, null, array(
+ * 'FrameworkBundle:Form',
+ * )))
+ * ->getFormFactory();
+ * </code>
+ *
+ * The next example shows how to include the "<table>" layout:
+ *
+ * <code>
+ * use Symfony\Component\Form\Extension\Templating\TemplatingExtension;
+ *
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new TemplatingExtension($engine, null, array(
+ * 'FrameworkBundle:Form',
+ * 'FrameworkBundle:FormTable',
+ * )))
+ * ->getFormFactory();
+ * </code>
+ *
+ * If you also loaded the CsrfExtension, you should pass the CSRF provider
+ * to the extension so that you can render CSRF tokens in your templates
+ * more easily:
+ *
+ * <code>
+ * use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
+ * use Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider;
+ * use Symfony\Component\Form\Extension\Templating\TemplatingExtension;
+ *
+ *
+ * $secret = 'V8a5Z97e...';
+ * $csrfProvider = new DefaultCsrfProvider($secret);
+ * $formFactory = Forms::createFormFactoryBuilder()
+ * ->addExtension(new CsrfExtension($csrfProvider))
+ * ->addExtension(new TemplatingExtension($engine, $csrfProvider, array(
+ * 'FrameworkBundle:Form',
+ * )))
+ * ->getFormFactory();
+ * </code>
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+final class Forms
+{
+ /**
+ * Creates a form factory with the default configuration.
+ *
+ * @return FormFactoryInterface The form factory.
+ */
+ public static function createFormFactory()
+ {
+ return self::createFormFactoryBuilder()->getFormFactory();
+ }
+
+ /**
+ * Creates a form factory builder with the default configuration.
+ *
+ * @return FormFactoryBuilderInterface The form factory builder.
+ */
+ public static function createFormFactoryBuilder()
+ {
+ $builder = new FormFactoryBuilder();
+ $builder->addExtension(new CoreExtension());
+
+ return $builder;
+ }
+
+ /**
+ * This class cannot be instantiated.
+ */
+ private function __construct()
+ {
+ }
+}
--- /dev/null
+<?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\Form\Guess;
+
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * Base class for guesses made by TypeGuesserInterface implementation
+ *
+ * Each instance contains a confidence value about the correctness of the guess.
+ * Thus an instance with confidence HIGH_CONFIDENCE is more likely to be
+ * correct than an instance with confidence LOW_CONFIDENCE.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class Guess
+{
+ /**
+ * Marks an instance with a value that is extremely likely to be correct
+ * @var integer
+ */
+ const VERY_HIGH_CONFIDENCE = 3;
+
+ /**
+ * Marks an instance with a value that is very likely to be correct
+ * @var integer
+ */
+ const HIGH_CONFIDENCE = 2;
+
+ /**
+ * Marks an instance with a value that is likely to be correct
+ * @var integer
+ */
+ const MEDIUM_CONFIDENCE = 1;
+
+ /**
+ * Marks an instance with a value that may be correct
+ * @var integer
+ */
+ const LOW_CONFIDENCE = 0;
+
+ /**
+ * The confidence about the correctness of the value
+ *
+ * One of VERY_HIGH_CONFIDENCE, HIGH_CONFIDENCE, MEDIUM_CONFIDENCE
+ * and LOW_CONFIDENCE.
+ *
+ * @var integer
+ */
+ private $confidence;
+
+ /**
+ * Returns the guess most likely to be correct from a list of guesses
+ *
+ * If there are multiple guesses with the same, highest confidence, the
+ * returned guess is any of them.
+ *
+ * @param array $guesses A list of guesses
+ *
+ * @return Guess The guess with the highest confidence
+ */
+ public static function getBestGuess(array $guesses)
+ {
+ $result = null;
+ $maxConfidence = -1;
+
+ foreach ($guesses as $guess) {
+ if ($maxConfidence < $confidence = $guess->getConfidence()) {
+ $maxConfidence = $confidence;
+ $result = $guess;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param integer $confidence The confidence
+ *
+ * @throws InvalidArgumentException if the given value of confidence is unknown
+ */
+ public function __construct($confidence)
+ {
+ if (self::VERY_HIGH_CONFIDENCE !== $confidence && self::HIGH_CONFIDENCE !== $confidence &&
+ self::MEDIUM_CONFIDENCE !== $confidence && self::LOW_CONFIDENCE !== $confidence) {
+ throw new InvalidArgumentException('The confidence should be one of the constants defined in Guess.');
+ }
+
+ $this->confidence = $confidence;
+ }
+
+ /**
+ * Returns the confidence that the guessed value is correct
+ *
+ * @return integer One of the constants VERY_HIGH_CONFIDENCE,
+ * HIGH_CONFIDENCE, MEDIUM_CONFIDENCE and LOW_CONFIDENCE
+ */
+ public function getConfidence()
+ {
+ return $this->confidence;
+ }
+}
--- /dev/null
+<?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\Form\Guess;
+
+/**
+ * Contains a guessed class name and a list of options for creating an instance
+ * of that class
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TypeGuess extends Guess
+{
+ /**
+ * The guessed field type
+ * @var string
+ */
+ private $type;
+
+ /**
+ * The guessed options for creating an instance of the guessed class
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Constructor
+ *
+ * @param string $type The guessed field type
+ * @param array $options The options for creating instances of the
+ * guessed class
+ * @param integer $confidence The confidence that the guessed class name
+ * is correct
+ */
+ public function __construct($type, array $options, $confidence)
+ {
+ parent::__construct($confidence);
+
+ $this->type = $type;
+ $this->options = $options;
+ }
+
+ /**
+ * Returns the guessed field type
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Returns the guessed options for creating instances of the guessed type
+ *
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+}
--- /dev/null
+<?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\Form\Guess;
+
+/**
+ * Contains a guessed value
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ValueGuess extends Guess
+{
+ /**
+ * The guessed value
+ * @var array
+ */
+ private $value;
+
+ /**
+ * Constructor
+ *
+ * @param string $value The guessed value
+ * @param integer $confidence The confidence that the guessed class name
+ * is correct
+ */
+ public function __construct($value, $confidence)
+ {
+ parent::__construct($confidence);
+
+ $this->value = $value;
+ }
+
+ /**
+ * Returns the guessed value
+ *
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+}
--- /dev/null
+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.
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\RequestHandlerInterface;
+
+/**
+ * A request handler using PHP's super globals $_GET, $_POST and $_SERVER.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class NativeRequestHandler implements RequestHandlerInterface
+{
+ /**
+ * The allowed keys of the $_FILES array.
+ *
+ * @var array
+ */
+ private static $fileKeys = array(
+ 'error',
+ 'name',
+ 'size',
+ 'tmp_name',
+ 'type',
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handleRequest(FormInterface $form, $request = null)
+ {
+ if (null !== $request) {
+ throw new UnexpectedTypeException($request, 'null');
+ }
+
+ $name = $form->getName();
+ $method = $form->getConfig()->getMethod();
+
+ if ($method !== self::getRequestMethod()) {
+ return;
+ }
+
+ if ('GET' === $method) {
+ if ('' === $name) {
+ $data = $_GET;
+ } else {
+ // Don't submit GET requests if the form's name does not exist
+ // in the request
+ if (!isset($_GET[$name])) {
+ return;
+ }
+
+ $data = $_GET[$name];
+ }
+ } else {
+ $fixedFiles = array();
+ foreach ($_FILES as $name => $file) {
+ $fixedFiles[$name] = self::stripEmptyFiles(self::fixPhpFilesArray($file));
+ }
+
+ if ('' === $name) {
+ $params = $_POST;
+ $files = $fixedFiles;
+ } else {
+ $default = $form->getConfig()->getCompound() ? array() : null;
+ $params = isset($_POST[$name]) ? $_POST[$name] : $default;
+ $files = isset($fixedFiles[$name]) ? $fixedFiles[$name] : $default;
+ }
+
+ if (is_array($params) && is_array($files)) {
+ $data = array_replace_recursive($params, $files);
+ } else {
+ $data = $params ?: $files;
+ }
+ }
+
+ // Don't auto-submit the form unless at least one field is present.
+ if ('' === $name && count(array_intersect_key($data, $form->all())) <= 0) {
+ return;
+ }
+
+ $form->submit($data, 'PATCH' !== $method);
+ }
+
+ /**
+ * Returns the method used to submit the request to the server.
+ *
+ * @return string The request method.
+ */
+ private static function getRequestMethod()
+ {
+ $method = isset($_SERVER['REQUEST_METHOD'])
+ ? strtoupper($_SERVER['REQUEST_METHOD'])
+ : 'GET';
+
+ if ('POST' === $method && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
+ $method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
+ }
+
+ return $method;
+ }
+
+ /**
+ * Fixes a malformed PHP $_FILES array.
+ *
+ * PHP has a bug that the format of the $_FILES array differs, depending on
+ * whether the uploaded file fields had normal field names or array-like
+ * field names ("normal" vs. "parent[child]").
+ *
+ * This method fixes the array to look like the "normal" $_FILES array.
+ *
+ * It's safe to pass an already converted array, in which case this method
+ * just returns the original array unmodified.
+ *
+ * This method is identical to {@link Symfony\Component\HttpFoundation\FileBag::fixPhpFilesArray}
+ * and should be kept as such in order to port fixes quickly and easily.
+ *
+ * @param array $data
+ *
+ * @return array
+ */
+ private static function fixPhpFilesArray($data)
+ {
+ if (!is_array($data)) {
+ return $data;
+ }
+
+ $keys = array_keys($data);
+ sort($keys);
+
+ if (self::$fileKeys !== $keys || !isset($data['name']) || !is_array($data['name'])) {
+ return $data;
+ }
+
+ $files = $data;
+ foreach (self::$fileKeys as $k) {
+ unset($files[$k]);
+ }
+
+ foreach (array_keys($data['name']) as $key) {
+ $files[$key] = self::fixPhpFilesArray(array(
+ 'error' => $data['error'][$key],
+ 'name' => $data['name'][$key],
+ 'type' => $data['type'][$key],
+ 'tmp_name' => $data['tmp_name'][$key],
+ 'size' => $data['size'][$key]
+ ));
+ }
+
+ return $files;
+ }
+
+ /**
+ * Sets empty uploaded files to NULL in the given uploaded files array.
+ *
+ * @param mixed $data The file upload data.
+ *
+ * @return array|null Returns the stripped upload data.
+ */
+ private static function stripEmptyFiles($data)
+ {
+ if (!is_array($data)) {
+ return $data;
+ }
+
+ $keys = array_keys($data);
+ sort($keys);
+
+ if (self::$fileKeys === $keys) {
+ if (UPLOAD_ERR_NO_FILE === $data['error']) {
+ return null;
+ }
+
+ return $data;
+ }
+
+ foreach ($data as $key => $value) {
+ $data[$key] = self::stripEmptyFiles($value);
+ }
+
+ return $data;
+ }
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * A form extension with preloaded types, type exceptions and type guessers.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PreloadedExtension implements FormExtensionInterface
+{
+ /**
+ * @var array
+ */
+ private $types = array();
+
+ /**
+ * @var array
+ */
+ private $typeExtensions = array();
+
+ /**
+ * @var FormTypeGuesserInterface
+ */
+ private $typeGuesser;
+
+ /**
+ * Creates a new preloaded extension.
+ *
+ * @param array $types The types that the extension should support.
+ * @param array $typeExtensions The type extensions that the extension should support.
+ * @param FormTypeGuesserInterface|null $typeGuesser The guesser that the extension should support.
+ */
+ public function __construct(array $types, array $typeExtensions, FormTypeGuesserInterface $typeGuesser = null)
+ {
+ $this->types = $types;
+ $this->typeExtensions = $typeExtensions;
+ $this->typeGuesser = $typeGuesser;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType($name)
+ {
+ if (!isset($this->types[$name])) {
+ throw new InvalidArgumentException(sprintf('The type "%s" can not be loaded by this extension', $name));
+ }
+
+ return $this->types[$name];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasType($name)
+ {
+ return isset($this->types[$name]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeExtensions($name)
+ {
+ return isset($this->typeExtensions[$name])
+ ? $this->typeExtensions[$name]
+ : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTypeExtensions($name)
+ {
+ return !empty($this->typeExtensions[$name]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeGuesser()
+ {
+ return $this->typeGuesser;
+ }
+}
--- /dev/null
+Form Component
+==============
+
+Form provides tools for defining forms, rendering and mapping request data to
+related models. Furthermore it provides integration with the Validation
+component.
+
+Resources
+---------
+
+Silex integration:
+
+https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/FormServiceProvider.php
+
+Documentation:
+
+http://symfony.com/doc/2.3/book/forms.html
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Form/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\Form;
+
+/**
+ * Submits forms if they were submitted.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface RequestHandlerInterface
+{
+ /**
+ * Submits a form if it was submitted.
+ *
+ * @param FormInterface $form The form to submit.
+ * @param mixed $request The current request.
+ */
+ public function handleRequest(FormInterface $form, $request = null);
+}
--- /dev/null
+<?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\Form;
+
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+/**
+ * A wrapper for a form type and its extensions.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ResolvedFormType implements ResolvedFormTypeInterface
+{
+ /**
+ * @var FormTypeInterface
+ */
+ private $innerType;
+
+ /**
+ * @var array
+ */
+ private $typeExtensions;
+
+ /**
+ * @var ResolvedFormTypeInterface
+ */
+ private $parent;
+
+ /**
+ * @var OptionsResolver
+ */
+ private $optionsResolver;
+
+ public function __construct(FormTypeInterface $innerType, array $typeExtensions = array(), ResolvedFormTypeInterface $parent = null)
+ {
+ if (!preg_match('/^[a-z0-9_]*$/i', $innerType->getName())) {
+ throw new InvalidArgumentException(sprintf(
+ 'The "%s" form type name ("%s") is not valid. Names must only contain letters, numbers, and "_".',
+ get_class($innerType),
+ $innerType->getName()
+ ));
+ }
+
+ foreach ($typeExtensions as $extension) {
+ if (!$extension instanceof FormTypeExtensionInterface) {
+ throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
+ }
+ }
+
+ $this->innerType = $innerType;
+ $this->typeExtensions = $typeExtensions;
+ $this->parent = $parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->innerType->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInnerType()
+ {
+ return $this->innerType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeExtensions()
+ {
+ // BC
+ if ($this->innerType instanceof AbstractType) {
+ return $this->innerType->getExtensions();
+ }
+
+ return $this->typeExtensions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createBuilder(FormFactoryInterface $factory, $name, array $options = array())
+ {
+ $options = $this->getOptionsResolver()->resolve($options);
+
+ // Should be decoupled from the specific option at some point
+ $dataClass = isset($options['data_class']) ? $options['data_class'] : null;
+
+ $builder = $this->newBuilder($name, $dataClass, $factory, $options);
+ $builder->setType($this);
+
+ $this->buildForm($builder, $options);
+
+ return $builder;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createView(FormInterface $form, FormView $parent = null)
+ {
+ $options = $form->getConfig()->getOptions();
+
+ $view = $this->newView($parent);
+
+ $this->buildView($view, $form, $options);
+
+ foreach ($form as $name => $child) {
+ /* @var FormInterface $child */
+ $view->children[$name] = $child->createView($view);
+ }
+
+ $this->finishView($view, $form, $options);
+
+ return $view;
+ }
+
+ /**
+ * Configures a form builder for the type hierarchy.
+ *
+ * This method is protected in order to allow implementing classes
+ * to change or call it in re-implementations of {@link createBuilder()}.
+ *
+ * @param FormBuilderInterface $builder The builder to configure.
+ * @param array $options The options used for the configuration.
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ if (null !== $this->parent) {
+ $this->parent->buildForm($builder, $options);
+ }
+
+ $this->innerType->buildForm($builder, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ /* @var FormTypeExtensionInterface $extension */
+ $extension->buildForm($builder, $options);
+ }
+ }
+
+ /**
+ * Configures a form view for the type hierarchy.
+ *
+ * This method is protected in order to allow implementing classes
+ * to change or call it in re-implementations of {@link createView()}.
+ *
+ * It is called before the children of the view are built.
+ *
+ * @param FormView $view The form view to configure.
+ * @param FormInterface $form The form corresponding to the view.
+ * @param array $options The options used for the configuration.
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ if (null !== $this->parent) {
+ $this->parent->buildView($view, $form, $options);
+ }
+
+ $this->innerType->buildView($view, $form, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ /* @var FormTypeExtensionInterface $extension */
+ $extension->buildView($view, $form, $options);
+ }
+ }
+
+ /**
+ * Finishes a form view for the type hierarchy.
+ *
+ * This method is protected in order to allow implementing classes
+ * to change or call it in re-implementations of {@link createView()}.
+ *
+ * It is called after the children of the view have been built.
+ *
+ * @param FormView $view The form view to configure.
+ * @param FormInterface $form The form corresponding to the view.
+ * @param array $options The options used for the configuration.
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options)
+ {
+ if (null !== $this->parent) {
+ $this->parent->finishView($view, $form, $options);
+ }
+
+ $this->innerType->finishView($view, $form, $options);
+
+ foreach ($this->typeExtensions as $extension) {
+ /* @var FormTypeExtensionInterface $extension */
+ $extension->finishView($view, $form, $options);
+ }
+ }
+
+ /**
+ * Returns the configured options resolver used for this type.
+ *
+ * This method is protected in order to allow implementing classes
+ * to change or call it in re-implementations of {@link createBuilder()}.
+ *
+ * @return \Symfony\Component\OptionsResolver\OptionsResolverInterface The options resolver.
+ */
+ public function getOptionsResolver()
+ {
+ if (null === $this->optionsResolver) {
+ if (null !== $this->parent) {
+ $this->optionsResolver = clone $this->parent->getOptionsResolver();
+ } else {
+ $this->optionsResolver = new OptionsResolver();
+ }
+
+ $this->innerType->setDefaultOptions($this->optionsResolver);
+
+ foreach ($this->typeExtensions as $extension) {
+ /* @var FormTypeExtensionInterface $extension */
+ $extension->setDefaultOptions($this->optionsResolver);
+ }
+ }
+
+ return $this->optionsResolver;
+ }
+
+ /**
+ * Creates a new builder instance.
+ *
+ * Override this method if you want to customize the builder class.
+ *
+ * @param string $name The name of the builder.
+ * @param string $dataClass The data class.
+ * @param FormFactoryInterface $factory The current form factory.
+ * @param array $options The builder options.
+ *
+ * @return FormBuilderInterface The new builder instance.
+ */
+ protected function newBuilder($name, $dataClass, FormFactoryInterface $factory, array $options)
+ {
+ if ($this->innerType instanceof ButtonTypeInterface) {
+ return new ButtonBuilder($name, $options);
+ }
+
+ if ($this->innerType instanceof SubmitButtonTypeInterface) {
+ return new SubmitButtonBuilder($name, $options);
+ }
+
+ return new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);
+ }
+
+ /**
+ * Creates a new view instance.
+ *
+ * Override this method if you want to customize the view class.
+ *
+ * @param FormView|null $parent The parent view, if available.
+ *
+ * @return FormView A new view instance.
+ */
+ protected function newView(FormView $parent = null)
+ {
+ return new FormView($parent);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ResolvedFormTypeFactory implements ResolvedFormTypeFactoryInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null)
+ {
+ return new ResolvedFormType($type, $typeExtensions, $parent);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * Creates ResolvedFormTypeInterface instances.
+ *
+ * This interface allows you to use your custom ResolvedFormTypeInterface
+ * implementation, within which you can customize the concrete FormBuilderInterface
+ * implementations or FormView subclasses that are used by the framework.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ResolvedFormTypeFactoryInterface
+{
+ /**
+ * Resolves a form type.
+ *
+ * @param FormTypeInterface $type
+ * @param array $typeExtensions
+ * @param ResolvedFormTypeInterface $parent
+ *
+ * @return ResolvedFormTypeInterface
+ *
+ * @throws Exception\UnexpectedTypeException if the types parent {@link FormTypeInterface::getParent()} is not a string
+ * @throws Exception\InvalidArgumentException if the types parent can not be retrieved from any extension
+ */
+ public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null);
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A wrapper for a form type and its extensions.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ResolvedFormTypeInterface
+{
+ /**
+ * Returns the name of the type.
+ *
+ * @return string The type name.
+ */
+ public function getName();
+
+ /**
+ * Returns the parent type.
+ *
+ * @return ResolvedFormTypeInterface The parent type or null.
+ */
+ public function getParent();
+
+ /**
+ * Returns the wrapped form type.
+ *
+ * @return FormTypeInterface The wrapped form type.
+ */
+ public function getInnerType();
+
+ /**
+ * Returns the extensions of the wrapped form type.
+ *
+ * @return FormTypeExtensionInterface[] An array of {@link FormTypeExtensionInterface} instances.
+ */
+ public function getTypeExtensions();
+
+ /**
+ * Creates a new form builder for this type.
+ *
+ * @param FormFactoryInterface $factory The form factory.
+ * @param string $name The name for the builder.
+ * @param array $options The builder options.
+ *
+ * @return FormBuilderInterface The created form builder.
+ */
+ public function createBuilder(FormFactoryInterface $factory, $name, array $options = array());
+
+ /**
+ * Creates a new form view for a form of this type.
+ *
+ * @param FormInterface $form The form to create a view for.
+ * @param FormView $parent The parent view or null.
+ *
+ * @return FormView The created form view.
+ */
+ public function createView(FormInterface $form, FormView $parent = null);
+
+ /**
+ * Configures a form builder for the type hierarchy.
+ *
+ * @param FormBuilderInterface $builder The builder to configure.
+ * @param array $options The options used for the configuration.
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options);
+
+ /**
+ * Configures a form view for the type hierarchy.
+ *
+ * It is called before the children of the view are built.
+ *
+ * @param FormView $view The form view to configure.
+ * @param FormInterface $form The form corresponding to the view.
+ * @param array $options The options used for the configuration.
+ */
+ public function buildView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Finishes a form view for the type hierarchy.
+ *
+ * It is called after the children of the view have been built.
+ *
+ * @param FormView $view The form view to configure.
+ * @param FormInterface $form The form corresponding to the view.
+ * @param array $options The options used for the configuration.
+ */
+ public function finishView(FormView $view, FormInterface $form, array $options);
+
+ /**
+ * Returns the configured options resolver used for this type.
+ *
+ * @return \Symfony\Component\OptionsResolver\OptionsResolverInterface The options resolver.
+ */
+ public function getOptionsResolver();
+}
--- /dev/null
+<?xml version="1.0" ?>
+
+<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
+
+ <class name="Symfony\Component\Form\Form">
+ <constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
+ <property name="children">
+ <constraint name="Valid" />
+ </property>
+ </class>
+</constraint-mapping>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>هذا النموذج يجب الا يحتوى على اى حقول اضافية.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>مساحة الملف المرسل كبيرة. من فضلك حاول ارسال ملف اصغر.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>قيمة رمز الموقع غير صحيحة. من فضلك اعد ارسال النموذج.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>\r
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">\r
+ <file source-language="en" datatype="plaintext" original="file.ext">\r
+ <body>\r
+ <trans-unit id="28">\r
+ <source>This form should not contain extra fields.</source>\r
+ <target>Тази форма не трябва да съдържа допълнителни полета.</target>\r
+ </trans-unit>\r
+ <trans-unit id="29">\r
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>\r
+ <target>Каченият файл е твърде голям. Моля, опитайте да качите по-малък файл.</target>\r
+ </trans-unit>\r
+ <trans-unit id="30">\r
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>\r
+ <target>Невалиден CSRF токен. Моля, опитайте да изпратите формата отново.</target>\r
+ </trans-unit>\r
+ </body>\r
+ </file>\r
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Aquest formulari no hauria de contenir camps addicionals.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>L'arxiu pujat és massa gran. Per favor, pugi un arxiu més petit.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>El token CSRF no és vàlid. Per favor, provi d'enviar novament el formulari.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Tato skupina polí nesmí obsahovat další pole.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Nahraný soubor je příliš velký. Nahrajte prosím menší soubor.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF token je neplatný. Zkuste prosím znovu odeslat formulář.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Feltgruppen må ikke indeholde ekstra felter.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Den oploadede fil var for stor. Opload venligst en mindre fil.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF nøglen er ugyldig.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Dieses Formular sollte keine zusätzlichen Felder enthalten.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Die hochgeladene Datei ist zu groß. Versuchen Sie bitte eine kleinere Datei hochzuladen.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Das CSRF-Token ist ungültig. Versuchen Sie bitte das Formular erneut zu senden.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Αυτή η φόρμα δεν πρέπει να περιέχει επιπλέον πεδία.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Το αρχείο είναι πολύ μεγάλο. Παρακαλούμε προσπαθήστε να ανεβάσετε ένα μικρότερο αρχείο.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Το CSRF token δεν είναι έγκυρο. Παρακαλούμε δοκιμάστε να υποβάλετε τη φόρμα ξανά.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>This form should not contain extra fields.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>The uploaded file was too large. Please try to upload a smaller file.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>The CSRF token is invalid. Please try to resubmit the form.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Este formulario no debería contener campos adicionales.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>El archivo subido es demasiado grande. Por favor, suba un archivo más pequeño.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>El token CSRF no es válido. Por favor, pruebe de enviar nuevamente el formulario</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version='1.0'?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Väljade grupp ei tohiks sisalda lisaväljasid.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Üleslaaditud fail oli liiga suur. Palun proovi uuesti väiksema failiga.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF-märgis on vigane. Palun proovi vormi uuesti esitada.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Formulario honek ez luke aparteko eremurik eduki behar.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Igotako fitxategia handiegia da. Mesedez saiatu fitxategi txikiago bat igotzen.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid.</source>
+ <target>CSFR tokena ez da egokia.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>این فرم نباید فیلد اضافی داشته باشد.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>فایل بارگذاری شده بسیار بزرگ است. لطفا فایل کوچکتری را بارگزاری کنید.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>مقدار CSRF نامعتبر است. لطفا فرم را مجددا ارسال فرمایید..</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This field group should not contain extra fields.</source>
+ <target>Tämä kenttäryhmä ei voi sisältää ylimääräisiä kenttiä.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Ladattu tiedosto on liian iso. Ole hyvä ja lataa pienempi tiedosto.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF tarkiste on virheellinen. Olen hyvä ja yritä lähettää lomake uudestaan.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ce formulaire ne doit pas contenir des champs supplémentaires.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Le fichier téléchargé est trop volumineux. Merci d'essayer d'envoyer un fichier plus petit.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Le jeton CSRF est invalide. Veuillez renvoyer le formulaire.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Este formulario non debería conter campos adicionais.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>O arquivo subido é demasiado grande. Por favor, suba un arquivo máis pequeno.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>O token CSRF non é válido. Por favor, probe a enviar novamente o formulario</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>הטופס לא צריך להכיל שדות נוספים.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>הקובץ שהועלה גדול מדי. נסה להעלות קובץ קטן יותר.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid.</source>
+ <target>אסימון CSRF אינו חוקי.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ovaj obrazac ne smije sadržavati dodatna polja.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Prenesena datoteka je prevelika. Molim pokušajte prenijeti manju datoteku.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF vrijednost nije ispravna. Pokušajte ponovo poslati obrazac.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ez a mezőcsoport nem tartalmazhat extra mezőket.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>A feltöltött fájl túl nagy. Kérem próbáljon egy kisebb fájlt feltölteni.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Érvénytelen CSRF token. Kérem próbálja újra elküldeni az űrlapot.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Այս ձևը չպետք է պարունակի լրացուցիչ տողեր.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Վերբեռնված ֆայլը չափազանց մեծ է: Խնդրվում է վերբեռնել ավելի փոքր չափսի ֆայլ.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF արժեքը անթույլատրելի է: Փորձեք նորից ուղարկել ձևը.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Gabungan kolom tidak boleh mengandung kolom tambahan.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Berkas yang di unggah terlalu besar. Silahkan coba unggah berkas yang lebih kecil.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF-Token tidak sah. Silahkan coba kirim ulang formulir.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Questo form non dovrebbe contenere nessun campo extra.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Il file caricato è troppo grande. Per favore caricare un file più piccolo.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Il token CSRF non è valido. Provare a reinviare il form.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>フィールドグループに追加のフィールドを含んではなりません.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>アップロードされたファイルが大きすぎます。小さなファイルで再度アップロードしてください.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid.</source>
+ <target>CSRFトークンが無効です.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Dës Feldergrupp sollt keng zousätzlech Felder enthalen.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>De geschécktene Fichier ass ze grouss. Versicht wann ech gelift ee méi klenge Fichier eropzelueden.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Den CSRF-Token ass ongëlteg. Versicht wann ech gelift de Formulaire nach eng Kéier ze schécken.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Forma negali turėti papildomų laukų.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Įkelta byla yra per didelė. bandykite įkelti mažesnę.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF kodas nepriimtinas. Bandykite siųsti formos užklausą dar kartą.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Šajā veidlapā nevajadzētu būt papildus ievades laukiem.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Augšupielādētā faila izmērs bija par lielu. Lūdzu mēģiniet augšupielādēt mazāka izmēra failu.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Dotais CSRF talons nav derīgs. Lūdzu mēģiniet vēlreiz iesniegt veidlapu.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Форм нэмэлт талбар багтаах боломжгүй.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Upload хийсэн файл хэтэрхий том байна. Бага хэмжээтэй файл оруулна уу.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF token буруу байна. Формоо дахин илгээнэ үү.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Feltgruppen må ikke inneholde ekstra felter.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Den opplastede file var for stor. Vennligst last opp en mindre fil.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid.</source>
+ <target>CSRF nøkkelen er ugyldig.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Dit formulier mag geen extra velden bevatten.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Het geüploade bestand is te groot. Probeer een kleiner bestand te uploaden.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>De CSRF-token is ongeldig. Probeer het formulier opnieuw te versturen.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ten formularz nie powinien zawierać dodatkowych pól.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Wgrany plik był za duży. Proszę spróbować wgrać mniejszy plik.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Token CSRF jest nieprawidłowy. Proszę spróbować wysłać formularz ponownie.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Este formulário não deveria conter campos extra.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>O arquivo enviado é muito grande. Por favor, tente enviar um ficheiro mais pequeno.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>O token CSRF é inválido. Por favor submeta o formulário novamente.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Este formulário não deve conter campos adicionais.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>O arquivo enviado é muito grande. Por favor, tente enviar um arquivo menor.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>O token CSRF é inválido. Por favor, tente reenviar o formulário.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Aceast formular nu ar trebui să conțină câmpuri suplimentare.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Fișierul încărcat a fost prea mare. Vă rugăm sa încărcați un fișier mai mic.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>Token-ul CSRF este invalid. Vă rugăm să trimiteți formularul incă o dată.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Эта форма не должна содержать дополнительных полей.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Загруженный файл слишком большой. Пожалуйста, попробуйте загрузить файл меньшего размера.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF значение недопустимо. Пожалуйста, попробуйте повторить отправку формы.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Polia by nemali obsahovať ďalšie prvky.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Odoslaný súbor je príliš veľký. Prosím odošlite súbor s menšou veľkosťou.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF token je neplatný. Prosím skúste znovu odoslať formulár.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>To področje ne sme vsebovati dodatnih polj.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Naložena datoteka je prevelika. Prosim, poizkusite naložiti manjšo.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF vrednost je napačna. Prosimo, ponovno pošljite obrazec.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Овај формулар не треба да садржи додатна поља.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Отпремљена датотека је била превелика. Молим покушајте отпремање мање датотеке.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF вредност је невалидна. Покушајте поново.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ovaj formular ne treba da sadrži dodatna polja.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Otpremljena datoteka je bila prevelika. Molim pokušajte otpremanje manje datoteke.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF vrednost je nevalidna. Pokušajte ponovo.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Formuläret kan inte innehålla extra fält.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Den uppladdade filen var för stor. Försök ladda upp en mindre fil.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid.</source>
+ <target>CSRF-symbolen är inte giltig.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>Ця форма не повинна містити додаткових полів.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>Завантажений файл занадто великий. Будь-ласка, спробуйте завантажити файл меншого розміру.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF значення недопустиме. Будь-ласка, спробуйте відправити форму знову.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="28">
+ <source>This form should not contain extra fields.</source>
+ <target>该表单中不可有额外字段.</target>
+ </trans-unit>
+ <trans-unit id="29">
+ <source>The uploaded file was too large. Please try to upload a smaller file.</source>
+ <target>上传文件太大, 请重新尝试上传一个较小的文件.</target>
+ </trans-unit>
+ <trans-unit id="30">
+ <source>The CSRF token is invalid. Please try to resubmit the form.</source>
+ <target>CSRF 验证符无效, 请重新提交.</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?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\Form;
+
+/**
+ * Reverses a transformer
+ *
+ * When the transform() method is called, the reversed transformer's
+ * reverseTransform() method is called and vice versa.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ReversedTransformer implements DataTransformerInterface
+{
+ /**
+ * The reversed transformer
+ * @var DataTransformerInterface
+ */
+ protected $reversedTransformer;
+
+ /**
+ * Reverses this transformer
+ *
+ * @param DataTransformerInterface $reversedTransformer
+ */
+ public function __construct(DataTransformerInterface $reversedTransformer)
+ {
+ $this->reversedTransformer = $reversedTransformer;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function transform($value)
+ {
+ return $this->reversedTransformer->reverseTransform($value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function reverseTransform($value)
+ {
+ return $this->reversedTransformer->transform($value);
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A button that submits the form.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SubmitButton extends Button implements ClickableInterface
+{
+ /**
+ * @var Boolean
+ */
+ private $clicked = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isClicked()
+ {
+ return $this->clicked;
+ }
+
+ /**
+ * Submits data to the button.
+ *
+ * @param null|string $submittedData The data.
+ * @param Boolean $clearMissing Not used.
+ *
+ * @return SubmitButton The button instance
+ *
+ * @throws Exception\AlreadySubmittedException If the form has already been submitted.
+ */
+ public function submit($submittedData, $clearMissing = true)
+ {
+ parent::submit($submittedData, $clearMissing);
+
+ $this->clicked = null !== $submittedData;
+
+ return $this;
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A builder for {@link SubmitButton} instances.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SubmitButtonBuilder extends ButtonBuilder
+{
+ /**
+ * Creates the button.
+ *
+ * @return Button The button
+ */
+ public function getForm()
+ {
+ return new SubmitButton($this->getFormConfig());
+ }
+}
--- /dev/null
+<?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\Form;
+
+/**
+ * A type that should be converted into a {@link SubmitButton} instance.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface SubmitButtonTypeInterface extends FormTypeInterface
+{
+}
--- /dev/null
+<?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\Form\Test;
+
+use Symfony\Component\Form\FormEvent;
+
+class DeprecationErrorHandler
+{
+ public static function handle($errorNumber, $message, $file, $line, $context)
+ {
+ if ($errorNumber & E_USER_DEPRECATED) {
+ return true;
+ }
+
+ return \PHPUnit_Util_ErrorHandler::handleError($errorNumber, $message, $file, $line);
+ }
+
+ public static function handleBC($errorNumber, $message, $file, $line, $context)
+ {
+ if ($errorNumber & E_USER_DEPRECATED) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function preBind($listener, FormEvent $event)
+ {
+ set_error_handler(array('Symfony\Component\Form\Test\DeprecationErrorHandler', 'handle'));
+ $listener->preBind($event);
+ restore_error_handler();
+ }
+}
--- /dev/null
+<?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\Form\Test;
+
+interface FormBuilderInterface extends \Iterator, \Symfony\Component\Form\FormBuilderInterface
+{
+}
--- /dev/null
+<?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\Form\Test;
+
+use Symfony\Component\Form\Forms;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \Symfony\Component\Form\FormFactoryInterface
+ */
+ protected $factory;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->factory = Forms::createFormFactoryBuilder()
+ ->addExtensions($this->getExtensions())
+ ->getFormFactory();
+ }
+
+ protected function getExtensions()
+ {
+ return array();
+ }
+}
--- /dev/null
+<?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\Form\Test;
+
+interface FormInterface extends \Iterator, \Symfony\Component\Form\FormInterface
+{
+}
--- /dev/null
+<?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\Form\Test;
+
+/**
+ * Base class for performance tests.
+ *
+ * Copied from Doctrine 2's OrmPerformanceTestCase.
+ *
+ * @author robo
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class FormPerformanceTestCase extends FormIntegrationTestCase
+{
+ /**
+ * @var integer
+ */
+ protected $maxRunningTime = 0;
+
+ /**
+ */
+ protected function runTest()
+ {
+ $s = microtime(true);
+ parent::runTest();
+ $time = microtime(true) - $s;
+
+ if ($this->maxRunningTime != 0 && $time > $this->maxRunningTime) {
+ $this->fail(
+ sprintf(
+ 'expected running time: <= %s but was: %s',
+
+ $this->maxRunningTime,
+ $time
+ )
+ );
+ }
+ }
+
+ /**
+ * @param integer $maxRunningTime
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxRunningTime($maxRunningTime)
+ {
+ if (is_integer($maxRunningTime) && $maxRunningTime >= 0) {
+ $this->maxRunningTime = $maxRunningTime;
+ } else {
+ throw new \InvalidArgumentException;
+ }
+ }
+
+ /**
+ * @return integer
+ * @since Method available since Release 2.3.0
+ */
+ public function getMaxRunningTime()
+ {
+ return $this->maxRunningTime;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Test;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+abstract class TypeTestCase extends FormIntegrationTestCase
+{
+ /**
+ * @var FormBuilder
+ */
+ protected $builder;
+
+ /**
+ * @var EventDispatcher
+ */
+ protected $dispatcher;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory);
+ }
+
+ public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual)
+ {
+ self::assertEquals($expected->format('c'), $actual->format('c'));
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\Tests\Fixtures\AlternatingRowType;
+
+abstract class AbstractDivLayoutTest extends AbstractLayoutTest
+{
+ public function testRow()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $form->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name"]
+ /following-sibling::ul
+ [./li[.="[trans]Error![/trans]"]]
+ [count(./li)=1]
+ /following-sibling::input[@id="name"]
+ ]
+'
+ );
+ }
+
+ public function testRowOverrideVariables()
+ {
+ $view = $this->factory->createNamed('name', 'text')->createView();
+ $html = $this->renderRow($view, array(
+ 'attr' => array('class' => 'my&class'),
+ 'label' => 'foo&bar',
+ 'label_attr' => array('class' => 'my&label&class'),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name"][@class="my&label&class required"][.="[trans]foo&bar[/trans]"]
+ /following-sibling::input[@id="name"][@class="my&class"]
+ ]
+'
+ );
+ }
+
+ public function testRepeatedRow()
+ {
+ $form = $this->factory->createNamed('name', 'repeated');
+ $form->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ // The errors of the form are not rendered by intention!
+ // In practice, repeated fields cannot have errors as all errors
+ // on them are mapped to the first child.
+ // (see RepeatedTypeValidatorExtension)
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name_first"]
+ /following-sibling::input[@id="name_first"]
+ ]
+/following-sibling::div
+ [
+ ./label[@for="name_second"]
+ /following-sibling::input[@id="name_second"]
+ ]
+'
+ );
+ }
+
+ public function testButtonRow()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./button[@type="button"][@name="name"]
+ ]
+ [count(//label)=0]
+'
+ );
+ }
+
+ public function testRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'repeated')
+ ->add('field3', 'text')
+ ->add('field4', 'text')
+ ->getForm()
+ ->createView();
+
+ // Render field2 row -> does not implicitly call renderWidget because
+ // it is a repeated field!
+ $this->renderRow($view['field2']);
+
+ // Render field3 widget
+ $this->renderWidget($view['field3']);
+
+ // Rest should only contain field1 and field4
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name_field1"]
+ /following-sibling::input[@type="text"][@id="name_field1"]
+ ]
+/following-sibling::div
+ [
+ ./label[@for="name_field4"]
+ /following-sibling::input[@type="text"][@id="name_field4"]
+ ]
+ [count(../div)=2]
+ [count(..//label)=2]
+ [count(..//input)=3]
+/following-sibling::input
+ [@type="hidden"]
+ [@id="name__token"]
+'
+ );
+ }
+
+ public function testRestWithChildrenForms()
+ {
+ $child1 = $this->factory->createNamedBuilder('child1', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text');
+
+ $child2 = $this->factory->createNamedBuilder('child2', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text');
+
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add($child1)
+ ->add($child2)
+ ->getForm()
+ ->createView();
+
+ // Render child1.field1 row
+ $this->renderRow($view['child1']['field1']);
+
+ // Render child2.field2 widget (remember that widget don't render label)
+ $this->renderWidget($view['child2']['field2']);
+
+ // Rest should only contain child1.field2 and child2.field1
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[not(@for)]
+ /following-sibling::div[@id="parent_child1"]
+ [
+ ./div
+ [
+ ./label[@for="parent_child1_field2"]
+ /following-sibling::input[@id="parent_child1_field2"]
+ ]
+ ]
+ ]
+
+/following-sibling::div
+ [
+ ./label[not(@for)]
+ /following-sibling::div[@id="parent_child2"]
+ [
+ ./div
+ [
+ ./label[@for="parent_child2_field1"]
+ /following-sibling::input[@id="parent_child2_field1"]
+ ]
+ ]
+ ]
+ [count(//label)=4]
+ [count(//input[@type="text"])=2]
+/following-sibling::input[@type="hidden"][@id="parent__token"]
+'
+ );
+ }
+
+ public function testRestAndRepeatedWithRow()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('first', 'text')
+ ->add('password', 'repeated')
+ ->getForm()
+ ->createView();
+
+ $this->renderRow($view['password']);
+
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name_first"]
+ /following-sibling::input[@type="text"][@id="name_first"]
+ ]
+ [count(.//input)=1]
+/following-sibling::input
+ [@type="hidden"]
+ [@id="name__token"]
+'
+ );
+ }
+
+ public function testRestAndRepeatedWithRowPerChild()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('first', 'text')
+ ->add('password', 'repeated')
+ ->getForm()
+ ->createView();
+
+ $this->renderRow($view['password']['first']);
+ $this->renderRow($view['password']['second']);
+
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name_first"]
+ /following-sibling::input[@type="text"][@id="name_first"]
+ ]
+ [count(.//input)=1]
+ [count(.//label)=1]
+/following-sibling::input
+ [@type="hidden"]
+ [@id="name__token"]
+'
+ );
+ }
+
+ public function testRestAndRepeatedWithWidgetPerChild()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('first', 'text')
+ ->add('password', 'repeated')
+ ->getForm()
+ ->createView();
+
+ // The password form is considered as rendered as all its children
+ // are rendered
+ $this->renderWidget($view['password']['first']);
+ $this->renderWidget($view['password']['second']);
+
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name_first"]
+ /following-sibling::input[@type="text"][@id="name_first"]
+ ]
+ [count(//input)=2]
+ [count(//label)=1]
+/following-sibling::input
+ [@type="hidden"]
+ [@id="name__token"]
+'
+ );
+ }
+
+ public function testCollection()
+ {
+ $form = $this->factory->createNamed('name', 'collection', array('a', 'b'), array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div[./input[@type="text"][@value="a"]]
+ /following-sibling::div[./input[@type="text"][@value="b"]]
+ ]
+ [count(./div[./input])=2]
+'
+ );
+ }
+
+ // https://github.com/symfony/symfony/issues/5038
+ public function testCollectionWithAlternatingRowTypes()
+ {
+ $data = array(
+ array('title' => 'a'),
+ array('title' => 'b'),
+ );
+ $form = $this->factory->createNamed('name', 'collection', $data, array(
+ 'type' => new AlternatingRowType(),
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div[./div/div/input[@type="text"][@value="a"]]
+ /following-sibling::div[./div/div/textarea[.="b"]]
+ ]
+ [count(./div[./div/div/input])=1]
+ [count(./div[./div/div/textarea])=1]
+'
+ );
+ }
+
+ public function testEmptyCollection()
+ {
+ $form = $this->factory->createNamed('name', 'collection', array(), array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [./input[@type="hidden"][@id="name__token"]]
+ [count(./div)=0]
+'
+ );
+ }
+
+ public function testCollectionRow()
+ {
+ $collection = $this->factory->createNamedBuilder(
+ 'collection',
+ 'collection',
+ array('a', 'b'),
+ array('type' => 'text')
+ );
+
+ $form = $this->factory->createNamedBuilder('form', 'form')
+ ->add($collection)
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [
+ ./label[not(@for)]
+ /following-sibling::div
+ [
+ ./div
+ [
+ ./label[@for="form_collection_0"]
+ /following-sibling::input[@type="text"][@value="a"]
+ ]
+ /following-sibling::div
+ [
+ ./label[@for="form_collection_1"]
+ /following-sibling::input[@type="text"][@value="b"]
+ ]
+ ]
+ ]
+ /following-sibling::input[@type="hidden"][@id="form__token"]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ public function testForm()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->setMethod('PUT')
+ ->setAction('http://example.com')
+ ->add('firstName', 'text')
+ ->add('lastName', 'text')
+ ->getForm();
+
+ // include ampersands everywhere to validate escaping
+ $html = $this->renderForm($form->createView(), array(
+ 'id' => 'my&id',
+ 'attr' => array('class' => 'my&class'),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/form
+ [
+ ./input[@type="hidden"][@name="_method"][@value="PUT"]
+ /following-sibling::div
+ [
+ ./div
+ [
+ ./label[@for="name_firstName"]
+ /following-sibling::input[@type="text"][@id="name_firstName"]
+ ]
+ /following-sibling::div
+ [
+ ./label[@for="name_lastName"]
+ /following-sibling::input[@type="text"][@id="name_lastName"]
+ ]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(.//input)=3]
+ [@id="my&id"]
+ [@class="my&class"]
+ ]
+ [@method="post"]
+ [@action="http://example.com"]
+ [@class="my&class"]
+'
+ );
+ }
+
+ public function testFormWidget()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add('firstName', 'text')
+ ->add('lastName', 'text')
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [
+ ./label[@for="name_firstName"]
+ /following-sibling::input[@type="text"][@id="name_firstName"]
+ ]
+ /following-sibling::div
+ [
+ ./label[@for="name_lastName"]
+ /following-sibling::input[@type="text"][@id="name_lastName"]
+ ]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ // https://github.com/symfony/symfony/issues/2308
+ public function testNestedFormError()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add($this->factory
+ ->createNamedBuilder('child', 'form', null, array('error_bubbling' => false))
+ ->add('grandChild', 'form')
+ )
+ ->getForm();
+
+ $form->get('child')->addError(new FormError('[trans]Error![/trans]'));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div/label
+ /following-sibling::ul[./li[.="[trans]Error![/trans]"]]
+ ]
+ [count(.//li[.="[trans]Error![/trans]"])=1]
+'
+ );
+ }
+
+ public function testCsrf()
+ {
+ $this->csrfProvider->expects($this->any())
+ ->method('generateCsrfToken')
+ ->will($this->returnValue('foo&bar'));
+
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add($this->factory
+ // No CSRF protection on nested forms
+ ->createNamedBuilder('child', 'form')
+ ->add($this->factory->createNamedBuilder('grandchild', 'text'))
+ )
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ /following-sibling::input[@type="hidden"][@id="name__token"][@value="foo&bar"]
+ ]
+ [count(.//input[@type="hidden"])=1]
+'
+ );
+ }
+
+ public function testRepeated()
+ {
+ $form = $this->factory->createNamed('name', 'repeated', 'foobar', array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [
+ ./label[@for="name_first"]
+ /following-sibling::input[@type="text"][@id="name_first"]
+ ]
+ /following-sibling::div
+ [
+ ./label[@for="name_second"]
+ /following-sibling::input[@type="text"][@id="name_second"]
+ ]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ public function testRepeatedWithCustomOptions()
+ {
+ $form = $this->factory->createNamed('name', 'repeated', null, array(
+ // the global required value cannot be overridden
+ 'first_options' => array('label' => 'Test', 'required' => false),
+ 'second_options' => array('label' => 'Test2')
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [
+ ./label[@for="name_first"][.="[trans]Test[/trans]"]
+ /following-sibling::input[@type="text"][@id="name_first"][@required="required"]
+ ]
+ /following-sibling::div
+ [
+ ./label[@for="name_second"][.="[trans]Test2[/trans]"]
+ /following-sibling::input[@type="text"][@id="name_second"][@required="required"]
+ ]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ public function testSearchInputName()
+ {
+ $form = $this->factory->createNamedBuilder('full', 'form')
+ ->add('name', 'search')
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [
+ ./label[@for="full_name"]
+ /following-sibling::input[@type="search"][@id="full_name"][@name="full[name]"]
+ ]
+ /following-sibling::input[@type="hidden"][@id="full__token"]
+ ]
+ [count(//input)=2]
+'
+ );
+ }
+
+ public function testLabelHasNoId()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $html = $this->renderRow($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./label[@for="name"][not(@id)]
+ /following-sibling::input[@id="name"]
+ ]
+'
+ );
+ }
+
+ public function testLabelIsNotRenderedWhenSetToFalse()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'label' => false
+ ));
+ $html = $this->renderRow($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./input[@id="name"]
+ ]
+ [count(//label)=0]
+'
+ );
+ }
+
+ /**
+ * @dataProvider themeBlockInheritanceProvider
+ */
+ public function testThemeBlockInheritance($theme)
+ {
+ $view = $this->factory
+ ->createNamed('name', 'email')
+ ->createView()
+ ;
+
+ $this->setTheme($view, $theme);
+
+ $this->assertMatchesXpath(
+ $this->renderWidget($view),
+ '/input[@type="email"][@rel="theme"]'
+ );
+ }
+
+ /**
+ * @dataProvider themeInheritanceProvider
+ */
+ public function testThemeInheritance($parentTheme, $childTheme)
+ {
+ $child = $this->factory->createNamedBuilder('child', 'form')
+ ->add('field', 'text');
+
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('field', 'text')
+ ->add($child)
+ ->getForm()
+ ->createView()
+ ;
+
+ $this->setTheme($view, $parentTheme);
+ $this->setTheme($view['child'], $childTheme);
+
+ $this->assertWidgetMatchesXpath($view, array(),
+'/div
+ [
+ ./div
+ [
+ ./label[.="parent"]
+ /following-sibling::input[@type="text"]
+ ]
+ /following-sibling::div
+ [
+ ./label[.="child"]
+ /following-sibling::div
+ [
+ ./div
+ [
+ ./label[.="child"]
+ /following-sibling::input[@type="text"]
+ ]
+ ]
+ ]
+ /following-sibling::input[@type="hidden"]
+ ]
+'
+ );
+ }
+
+ /**
+ * The block "_name_child_label" should be overridden in the theme of the
+ * implemented driver.
+ */
+ public function testCollectionRowWithCustomBlock()
+ {
+ $collection = array('one', 'two', 'three');
+ $form = $this->factory->createNamedBuilder('name', 'collection', $collection)
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div[./label[.="Custom label: [trans]0[/trans]"]]
+ /following-sibling::div[./label[.="Custom label: [trans]1[/trans]"]]
+ /following-sibling::div[./label[.="Custom label: [trans]2[/trans]"]]
+ ]
+'
+ );
+ }
+
+ public function testFormEndWithRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text')
+ ->getForm()
+ ->createView();
+
+ $this->renderWidget($view['field1']);
+
+ // Rest should only contain field2
+ $html = $this->renderEnd($view);
+
+ // Insert the start tag, the end tag should be rendered by the helper
+ $this->assertMatchesXpath('<form>' . $html,
+'/form
+ [
+ ./div
+ [
+ ./label[@for="name_field2"]
+ /following-sibling::input[@type="text"][@id="name_field2"]
+ ]
+ /following-sibling::input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+'
+ );
+ }
+
+ public function testFormEndWithoutRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text')
+ ->getForm()
+ ->createView();
+
+ $this->renderWidget($view['field1']);
+
+ // Rest should only contain field2, but isn't rendered
+ $html = $this->renderEnd($view, array('render_rest' => false));
+
+ $this->assertEquals('</form>', $html);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\AbstractExtension;
+use Symfony\Component\Form\Tests\Fixtures\FooType;
+
+class AbstractExtensionTest extends \PHPUnit_Framework_TestCase
+{
+ public function testHasType()
+ {
+ $loader = new ConcreteExtension();
+ $this->assertTrue($loader->hasType('foo'));
+ $this->assertFalse($loader->hasType('bar'));
+ }
+
+ public function testGetType()
+ {
+ $loader = new ConcreteExtension();
+ $this->assertInstanceOf('Symfony\Component\Form\Tests\Fixtures\FooType', $loader->getType('foo'));
+ }
+}
+
+class ConcreteExtension extends AbstractExtension
+{
+ protected function loadTypes()
+ {
+ return array(new FooType());
+ }
+
+ protected function loadTypeGuesser()
+ {
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+abstract class AbstractFormTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var EventDispatcherInterface
+ */
+ protected $dispatcher;
+
+ /**
+ * @var \Symfony\Component\Form\FormFactoryInterface
+ */
+ protected $factory;
+
+ /**
+ * @var \Symfony\Component\Form\FormInterface
+ */
+ protected $form;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ // We need an actual dispatcher to use the deprecated
+ // bindRequest() method
+ $this->dispatcher = new EventDispatcher();
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->form = $this->createForm();
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->factory = null;
+ $this->form = null;
+ }
+
+ /**
+ * @return \Symfony\Component\Form\FormInterface
+ */
+ abstract protected function createForm();
+
+ /**
+ * @param string $name
+ * @param EventDispatcherInterface $dispatcher
+ * @param string $dataClass
+ *
+ * @return FormBuilder
+ */
+ protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null)
+ {
+ return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getMockForm($name = 'name')
+ {
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $config = $this->getMock('Symfony\Component\Form\FormConfigInterface');
+
+ $form->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue($name));
+ $form->expects($this->any())
+ ->method('getConfig')
+ ->will($this->returnValue($config));
+
+ return $form;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getValidForm($name)
+ {
+ $form = $this->getMockForm($name);
+
+ $form->expects($this->any())
+ ->method('isValid')
+ ->will($this->returnValue(true));
+
+ return $form;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getInvalidForm($name)
+ {
+ $form = $this->getMockForm($name);
+
+ $form->expects($this->any())
+ ->method('isValid')
+ ->will($this->returnValue(false));
+
+ return $form;
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getDataTransformer()
+ {
+ return $this->getMock('Symfony\Component\Form\DataTransformerInterface');
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getFormValidator()
+ {
+ return $this->getMock('Symfony\Component\Form\FormValidatorInterface');
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
+
+abstract class AbstractLayoutTest extends \Symfony\Component\Form\Test\FormIntegrationTestCase
+{
+ protected $csrfProvider;
+
+ protected function setUp()
+ {
+ if (!extension_loaded('intl')) {
+ $this->markTestSkipped('The "intl" extension is not available');
+ }
+
+ \Locale::setDefault('en');
+
+ $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
+
+ parent::setUp();
+ }
+
+ protected function getExtensions()
+ {
+ return array(
+ new CsrfExtension($this->csrfProvider),
+ );
+ }
+
+ protected function tearDown()
+ {
+ $this->csrfProvider = null;
+
+ parent::tearDown();
+ }
+
+ protected function assertXpathNodeValue(\DomElement $element, $expression, $nodeValue)
+ {
+ $xpath = new \DOMXPath($element->ownerDocument);
+ $nodeList = $xpath->evaluate($expression);
+ $this->assertEquals(1, $nodeList->length);
+ $this->assertEquals($nodeValue, $nodeList->item(0)->nodeValue);
+ }
+
+ protected function assertMatchesXpath($html, $expression, $count = 1)
+ {
+ $dom = new \DomDocument('UTF-8');
+ try {
+ // Wrap in <root> node so we can load HTML with multiple tags at
+ // the top level
+ $dom->loadXml('<root>'.$html.'</root>');
+ } catch (\Exception $e) {
+ $this->fail(sprintf(
+ "Failed loading HTML:\n\n%s\n\nError: %s",
+ $html,
+ $e->getMessage()
+ ));
+ }
+ $xpath = new \DOMXPath($dom);
+ $nodeList = $xpath->evaluate('/root'.$expression);
+
+ if ($nodeList->length != $count) {
+ $dom->formatOutput = true;
+ $this->fail(sprintf(
+ "Failed asserting that \n\n%s\n\nmatches exactly %s. Matches %s in \n\n%s",
+ $expression,
+ $count == 1 ? 'once' : $count.' times',
+ $nodeList->length == 1 ? 'once' : $nodeList->length.' times',
+ // strip away <root> and </root>
+ substr($dom->saveHTML(), 6, -8)
+ ));
+ }
+ }
+
+ protected function assertWidgetMatchesXpath(FormView $view, array $vars, $xpath)
+ {
+ // include ampersands everywhere to validate escaping
+ $html = $this->renderWidget($view, array_merge(array(
+ 'id' => 'my&id',
+ 'attr' => array('class' => 'my&class'),
+ ), $vars));
+
+ $xpath = trim($xpath).'
+ [@id="my&id"]
+ [@class="my&class"]';
+
+ $this->assertMatchesXpath($html, $xpath);
+ }
+
+ abstract protected function renderForm(FormView $view, array $vars = array());
+
+ abstract protected function renderEnctype(FormView $view);
+
+ abstract protected function renderLabel(FormView $view, $label = null, array $vars = array());
+
+ abstract protected function renderErrors(FormView $view);
+
+ abstract protected function renderWidget(FormView $view, array $vars = array());
+
+ abstract protected function renderRow(FormView $view, array $vars = array());
+
+ abstract protected function renderRest(FormView $view, array $vars = array());
+
+ abstract protected function renderStart(FormView $view, array $vars = array());
+
+ abstract protected function renderEnd(FormView $view, array $vars = array());
+
+ abstract protected function setTheme(FormView $view, array $themes);
+
+ public function testEnctype()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add('file', 'file')
+ ->getForm();
+
+ $this->assertEquals('enctype="multipart/form-data"', $this->renderEnctype($form->createView()));
+ }
+
+ public function testNoEnctype()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add('text', 'text')
+ ->getForm();
+
+ $this->assertEquals('', $this->renderEnctype($form->createView()));
+ }
+
+ public function testLabel()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $view = $form->createView();
+ $this->renderWidget($view, array('label' => 'foo'));
+ $html = $this->renderLabel($view);
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [.="[trans]Name[/trans]"]
+'
+ );
+ }
+
+ public function testLabelOnForm()
+ {
+ $form = $this->factory->createNamed('name', 'date');
+ $view = $form->createView();
+ $this->renderWidget($view, array('label' => 'foo'));
+ $html = $this->renderLabel($view);
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@class="required"]
+ [.="[trans]Name[/trans]"]
+'
+ );
+ }
+
+ public function testLabelWithCustomTextPassedAsOption()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'label' => 'Custom label',
+ ));
+ $html = $this->renderLabel($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [.="[trans]Custom label[/trans]"]
+'
+ );
+ }
+
+ public function testLabelWithCustomTextPassedDirectly()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $html = $this->renderLabel($form->createView(), 'Custom label');
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [.="[trans]Custom label[/trans]"]
+'
+ );
+ }
+
+ public function testLabelWithCustomTextPassedAsOptionAndDirectly()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'label' => 'Custom label',
+ ));
+ $html = $this->renderLabel($form->createView(), 'Overridden label');
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [.="[trans]Overridden label[/trans]"]
+'
+ );
+ }
+
+ public function testLabelDoesNotRenderFieldAttributes()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $html = $this->renderLabel($form->createView(), null, array(
+ 'attr' => array(
+ 'class' => 'my&class'
+ ),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [@class="required"]
+'
+ );
+ }
+
+ public function testLabelWithCustomAttributesPassedDirectly()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $html = $this->renderLabel($form->createView(), null, array(
+ 'label_attr' => array(
+ 'class' => 'my&class'
+ ),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [@class="my&class required"]
+'
+ );
+ }
+
+ public function testLabelWithCustomTextAndCustomAttributesPassedDirectly()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $html = $this->renderLabel($form->createView(), 'Custom label', array(
+ 'label_attr' => array(
+ 'class' => 'my&class'
+ ),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/label
+ [@for="name"]
+ [@class="my&class required"]
+ [.="[trans]Custom label[/trans]"]
+'
+ );
+ }
+
+ // https://github.com/symfony/symfony/issues/5029
+ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'label' => 'Custom label',
+ ));
+ $html = $this->renderLabel($form->createView(), null, array(
+ 'label_attr' => array(
+ 'class' => 'my&class'
+ ),
+ ));
+
+ $this->assertMatchesXpath($html,
+ '/label
+ [@for="name"]
+ [@class="my&class required"]
+ [.="[trans]Custom label[/trans]"]
+'
+ );
+ }
+
+ public function testErrors()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $form->addError(new FormError('[trans]Error 1[/trans]'));
+ $form->addError(new FormError('[trans]Error 2[/trans]'));
+ $view = $form->createView();
+ $html = $this->renderErrors($view);
+
+ $this->assertMatchesXpath($html,
+'/ul
+ [
+ ./li[.="[trans]Error 1[/trans]"]
+ /following-sibling::li[.="[trans]Error 2[/trans]"]
+ ]
+ [count(./li)=2]
+'
+ );
+ }
+
+ public function testWidgetById()
+ {
+ $form = $this->factory->createNamed('text_id', 'text');
+ $html = $this->renderWidget($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/div
+ [
+ ./input
+ [@type="text"]
+ [@id="text_id"]
+ ]
+ [@id="container"]
+'
+ );
+ }
+
+ public function testCheckedCheckbox()
+ {
+ $form = $this->factory->createNamed('name', 'checkbox', true);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="checkbox"]
+ [@name="name"]
+ [@checked="checked"]
+ [@value="1"]
+'
+ );
+ }
+
+ public function testUncheckedCheckbox()
+ {
+ $form = $this->factory->createNamed('name', 'checkbox', false);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="checkbox"]
+ [@name="name"]
+ [not(@checked)]
+'
+ );
+ }
+
+ public function testCheckboxWithValue()
+ {
+ $form = $this->factory->createNamed('name', 'checkbox', false, array(
+ 'value' => 'foo&bar',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="checkbox"]
+ [@name="name"]
+ [@value="foo&bar"]
+'
+ );
+ }
+
+ public function testSingleChoice()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testSingleChoiceWithPreferred()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'preferred_choices' => array('&b'),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array('separator' => '-- sep --'),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceWithPreferredAndNoSeparator()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'preferred_choices' => array('&b'),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array('separator' => null),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ ]
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testSingleChoiceWithPreferredAndBlankSeparator()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'preferred_choices' => array('&b'),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array('separator' => ''),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ /following-sibling::option[@disabled="disabled"][not(@selected)][.=""]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testChoiceWithOnlyPreferred()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'preferred_choices' => array('&a', '&b'),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testSingleChoiceNonRequired()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'required' => false,
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [not(@required)]
+ [
+ ./option[@value=""][.="[trans][/trans]"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceNonRequiredNoneSelected()
+ {
+ $form = $this->factory->createNamed('name', 'choice', null, array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'required' => false,
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [not(@required)]
+ [
+ ./option[@value=""][.="[trans][/trans]"]
+ /following-sibling::option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceWithNonRequiredEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => false,
+ 'expanded' => false,
+ 'required' => false,
+ 'empty_value' => 'Select&Anything&Not&Me',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [not(@required)]
+ [
+ ./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Anything&Not&Me[/trans]"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceRequiredWithEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'required' => true,
+ 'multiple' => false,
+ 'expanded' => false,
+ 'empty_value' => 'Test&Me'
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[not(@value)][not(@selected)][@disabled][.="[trans]Test&Me[/trans]"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceRequiredWithEmptyValueViaView()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'required' => true,
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array('empty_value' => ''),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [
+ ./option[not(@value)][not(@selected)][@disabled][.="[trans][/trans]"]
+ /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceGrouped()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array(
+ 'Group&1' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'Group&2' => array('&c' => 'Choice&C'),
+ ),
+ 'multiple' => false,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [./optgroup[@label="[trans]Group&1[/trans]"]
+ [
+ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=2]
+ ]
+ [./optgroup[@label="[trans]Group&2[/trans]"]
+ [./option[@value="&c"][not(@selected)][.="[trans]Choice&C[/trans]"]]
+ [count(./option)=1]
+ ]
+ [count(./optgroup)=2]
+'
+ );
+ }
+
+ public function testMultipleChoice()
+ {
+ $form = $this->factory->createNamed('name', 'choice', array('&a'), array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => true,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name[]"]
+ [@multiple="multiple"]
+ [
+ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testMultipleChoiceSkipsEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'choice', array('&a'), array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => true,
+ 'expanded' => false,
+ 'empty_value' => 'Test&Me'
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name[]"]
+ [@multiple="multiple"]
+ [
+ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testMultipleChoiceNonRequired()
+ {
+ $form = $this->factory->createNamed('name', 'choice', array('&a'), array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'required' => false,
+ 'multiple' => true,
+ 'expanded' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name[]"]
+ [@multiple="multiple"]
+ [
+ ./option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"]
+ ]
+ [count(./option)=2]
+'
+ );
+ }
+
+ public function testSingleChoiceExpanded()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => false,
+ 'expanded' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input[@type="radio"][@name="name"][@id="name_0"][@value="&a"][@checked]
+ /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][@value="&b"][not(@checked)]
+ /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(./input)=3]
+'
+ );
+ }
+
+ public function testSingleChoiceExpandedWithEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'choice', '&a', array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B'),
+ 'multiple' => false,
+ 'expanded' => true,
+ 'empty_value' => 'Test&Me'
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input[@type="radio"][@name="name"][@id="name_placeholder"][not(@checked)]
+ /following-sibling::label[@for="name_placeholder"][.="[trans]Test&Me[/trans]"]
+ /following-sibling::input[@type="radio"][@name="name"][@id="name_0"][@checked]
+ /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)]
+ /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(./input)=4]
+'
+ );
+ }
+
+ public function testSingleChoiceExpandedWithBooleanValue()
+ {
+ $form = $this->factory->createNamed('name', 'choice', true, array(
+ 'choices' => array('1' => 'Choice&A', '0' => 'Choice&B'),
+ 'multiple' => false,
+ 'expanded' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input[@type="radio"][@name="name"][@id="name_0"][@checked]
+ /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::input[@type="radio"][@name="name"][@id="name_1"][not(@checked)]
+ /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(./input)=3]
+'
+ );
+ }
+
+ public function testMultipleChoiceExpanded()
+ {
+ $form = $this->factory->createNamed('name', 'choice', array('&a', '&c'), array(
+ 'choices' => array('&a' => 'Choice&A', '&b' => 'Choice&B', '&c' => 'Choice&C'),
+ 'multiple' => true,
+ 'expanded' => true,
+ 'required' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input[@type="checkbox"][@name="name[]"][@id="name_0"][@checked][not(@required)]
+ /following-sibling::label[@for="name_0"][.="[trans]Choice&A[/trans]"]
+ /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_1"][not(@checked)][not(@required)]
+ /following-sibling::label[@for="name_1"][.="[trans]Choice&B[/trans]"]
+ /following-sibling::input[@type="checkbox"][@name="name[]"][@id="name_2"][@checked][not(@required)]
+ /following-sibling::label[@for="name_2"][.="[trans]Choice&C[/trans]"]
+ /following-sibling::input[@type="hidden"][@id="name__token"]
+ ]
+ [count(./input)=4]
+'
+ );
+ }
+
+ public function testCountry()
+ {
+ $form = $this->factory->createNamed('name', 'country', 'AT');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]]
+ [count(./option)>200]
+'
+ );
+ }
+
+ public function testCountryWithEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'country', 'AT', array(
+ 'empty_value' => 'Select&Country',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]]
+ [./option[@value="AT"][@selected="selected"][.="[trans]Austria[/trans]"]]
+ [count(./option)>201]
+'
+ );
+ }
+
+ public function testDateTime()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', '2011-02-03 04:05:06', array(
+ 'input' => 'string',
+ 'with_seconds' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [@id="name_date"]
+ [
+ ./select
+ [@id="name_date_month"]
+ [./option[@value="2"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_day"]
+ [./option[@value="3"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_year"]
+ [./option[@value="2011"][@selected="selected"]]
+ ]
+ /following-sibling::div
+ [@id="name_time"]
+ [
+ ./select
+ [@id="name_time_hour"]
+ [./option[@value="4"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_time_minute"]
+ [./option[@value="5"][@selected="selected"]]
+ ]
+ ]
+ [count(.//select)=5]
+'
+ );
+ }
+
+ public function testDateTimeWithEmptyValueGlobal()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', null, array(
+ 'input' => 'string',
+ 'empty_value' => 'Change&Me',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [@id="name_date"]
+ [
+ ./select
+ [@id="name_date_month"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_date_day"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_date_year"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ ]
+ /following-sibling::div
+ [@id="name_time"]
+ [
+ ./select
+ [@id="name_time_hour"]
+ [./option[@value=""][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_time_minute"]
+ [./option[@value=""][.="[trans]Change&Me[/trans]"]]
+ ]
+ ]
+ [count(.//select)=5]
+'
+ );
+ }
+
+ public function testDateTimeWithEmptyValueOnTime()
+ {
+ $data = array('year' => '2011', 'month' => '2', 'day' => '3', 'hour' => '', 'minute' => '');
+
+ $form = $this->factory->createNamed('name', 'datetime', $data, array(
+ 'input' => 'array',
+ 'empty_value' => array('hour' => 'Change&Me', 'minute' => 'Change&Me'),
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [@id="name_date"]
+ [
+ ./select
+ [@id="name_date_month"]
+ [./option[@value="2"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_day"]
+ [./option[@value="3"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_year"]
+ [./option[@value="2011"][@selected="selected"]]
+ ]
+ /following-sibling::div
+ [@id="name_time"]
+ [
+ ./select
+ [@id="name_time_hour"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_time_minute"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ ]
+ ]
+ [count(.//select)=5]
+'
+ );
+ }
+
+ public function testDateTimeWithSeconds()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', '2011-02-03 04:05:06', array(
+ 'input' => 'string',
+ 'with_seconds' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./div
+ [@id="name_date"]
+ [
+ ./select
+ [@id="name_date_month"]
+ [./option[@value="2"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_day"]
+ [./option[@value="3"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_date_year"]
+ [./option[@value="2011"][@selected="selected"]]
+ ]
+ /following-sibling::div
+ [@id="name_time"]
+ [
+ ./select
+ [@id="name_time_hour"]
+ [./option[@value="4"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_time_minute"]
+ [./option[@value="5"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_time_second"]
+ [./option[@value="6"][@selected="selected"]]
+ ]
+ ]
+ [count(.//select)=6]
+'
+ );
+ }
+
+ public function testDateTimeSingleText()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', '2011-02-03 04:05:06', array(
+ 'input' => 'string',
+ 'date_widget' => 'single_text',
+ 'time_widget' => 'single_text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input
+ [@type="date"]
+ [@id="name_date"]
+ [@name="name[date]"]
+ [@value="2011-02-03"]
+ /following-sibling::input
+ [@type="time"]
+ [@id="name_time"]
+ [@name="name[time]"]
+ [@value="04:05"]
+ ]
+'
+ );
+ }
+
+ public function testDateTimeWithWidgetSingleText()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', '2011-02-03 04:05:06', array(
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="datetime"]
+ [@name="name"]
+ [@value="2011-02-03T04:05:06Z"]
+'
+ );
+ }
+
+ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets()
+ {
+ $form = $this->factory->createNamed('name', 'datetime', '2011-02-03 04:05:06', array(
+ 'input' => 'string',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ 'widget' => 'single_text',
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="datetime"]
+ [@name="name"]
+ [@value="2011-02-03T04:05:06Z"]
+'
+ );
+ }
+
+ public function testDateChoice()
+ {
+ $form = $this->factory->createNamed('name', 'date', '2011-02-03', array(
+ 'input' => 'string',
+ 'widget' => 'choice',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_month"]
+ [./option[@value="2"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_day"]
+ [./option[@value="3"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_year"]
+ [./option[@value="2011"][@selected="selected"]]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testDateChoiceWithEmptyValueGlobal()
+ {
+ $form = $this->factory->createNamed('name', 'date', null, array(
+ 'input' => 'string',
+ 'widget' => 'choice',
+ 'empty_value' => 'Change&Me',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_month"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_day"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ /following-sibling::select
+ [@id="name_year"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testDateChoiceWithEmptyValueOnYear()
+ {
+ $form = $this->factory->createNamed('name', 'date', null, array(
+ 'input' => 'string',
+ 'widget' => 'choice',
+ 'required' => false,
+ 'empty_value' => array('year' => 'Change&Me'),
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_month"]
+ [./option[@value="1"]]
+ /following-sibling::select
+ [@id="name_day"]
+ [./option[@value="1"]]
+ /following-sibling::select
+ [@id="name_year"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testDateText()
+ {
+ $form = $this->factory->createNamed('name', 'date', '2011-02-03', array(
+ 'input' => 'string',
+ 'widget' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input
+ [@id="name_month"]
+ [@type="text"]
+ [@value="2"]
+ /following-sibling::input
+ [@id="name_day"]
+ [@type="text"]
+ [@value="3"]
+ /following-sibling::input
+ [@id="name_year"]
+ [@type="text"]
+ [@value="2011"]
+ ]
+ [count(./input)=3]
+'
+ );
+ }
+
+ public function testDateSingleText()
+ {
+ $form = $this->factory->createNamed('name', 'date', '2011-02-03', array(
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="date"]
+ [@name="name"]
+ [@value="2011-02-03"]
+'
+ );
+ }
+
+ public function testDateErrorBubbling()
+ {
+ $form = $this->factory->createNamedBuilder('form', 'form')
+ ->add('date', 'date')
+ ->getForm();
+ $form->get('date')->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+
+ $this->assertEmpty($this->renderErrors($view));
+ $this->assertNotEmpty($this->renderErrors($view['date']));
+ }
+
+ public function testBirthDay()
+ {
+ $form = $this->factory->createNamed('name', 'birthday', '2000-02-03', array(
+ 'input' => 'string',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_month"]
+ [./option[@value="2"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_day"]
+ [./option[@value="3"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_year"]
+ [./option[@value="2000"][@selected="selected"]]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testBirthDayWithEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'birthday', '1950-01-01', array(
+ 'input' => 'string',
+ 'empty_value' => '',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_month"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans][/trans]"]]
+ [./option[@value="1"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_day"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans][/trans]"]]
+ [./option[@value="1"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_year"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans][/trans]"]]
+ [./option[@value="1950"][@selected="selected"]]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testEmail()
+ {
+ $form = $this->factory->createNamed('name', 'email', 'foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="email"]
+ [@name="name"]
+ [@value="foo&bar"]
+ [not(@maxlength)]
+'
+ );
+ }
+
+ public function testEmailWithMaxLength()
+ {
+ $form = $this->factory->createNamed('name', 'email', 'foo&bar', array(
+ 'max_length' => 123,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="email"]
+ [@name="name"]
+ [@value="foo&bar"]
+ [@maxlength="123"]
+'
+ );
+ }
+
+ public function testFile()
+ {
+ $form = $this->factory->createNamed('name', 'file');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="file"]
+'
+ );
+ }
+
+ public function testHidden()
+ {
+ $form = $this->factory->createNamed('name', 'hidden', 'foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="hidden"]
+ [@name="name"]
+ [@value="foo&bar"]
+'
+ );
+ }
+
+ public function testReadOnly()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'read_only' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@readonly="readonly"]
+'
+ );
+ }
+
+ public function testDisabled()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'disabled' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@disabled="disabled"]
+'
+ );
+ }
+
+ public function testInteger()
+ {
+ $form = $this->factory->createNamed('name', 'integer', 123);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="number"]
+ [@name="name"]
+ [@value="123"]
+'
+ );
+ }
+
+ public function testLanguage()
+ {
+ $form = $this->factory->createNamed('name', 'language', 'de');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [./option[@value="de"][@selected="selected"][.="[trans]German[/trans]"]]
+ [count(./option)>200]
+'
+ );
+ }
+
+ public function testLocale()
+ {
+ $form = $this->factory->createNamed('name', 'locale', 'de_AT');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [./option[@value="de_AT"][@selected="selected"][.="[trans]German (Austria)[/trans]"]]
+ [count(./option)>200]
+'
+ );
+ }
+
+ public function testMoney()
+ {
+ $form = $this->factory->createNamed('name', 'money', 1234.56, array(
+ 'currency' => 'EUR',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@value="1234.56"]
+ [contains(.., "€")]
+'
+ );
+ }
+
+ public function testNumber()
+ {
+ $form = $this->factory->createNamed('name', 'number', 1234.56);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@value="1234.56"]
+'
+ );
+ }
+
+ public function testPassword()
+ {
+ $form = $this->factory->createNamed('name', 'password', 'foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="password"]
+ [@name="name"]
+'
+ );
+ }
+
+ public function testPasswordSubmittedWithNotAlwaysEmpty()
+ {
+ $form = $this->factory->createNamed('name', 'password', null, array(
+ 'always_empty' => false,
+ ));
+ $form->submit('foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="password"]
+ [@name="name"]
+ [@value="foo&bar"]
+'
+ );
+ }
+
+ public function testPasswordWithMaxLength()
+ {
+ $form = $this->factory->createNamed('name', 'password', 'foo&bar', array(
+ 'max_length' => 123,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="password"]
+ [@name="name"]
+ [@maxlength="123"]
+'
+ );
+ }
+
+ public function testPercent()
+ {
+ $form = $this->factory->createNamed('name', 'percent', 0.1);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@value="10"]
+ [contains(.., "%")]
+'
+ );
+ }
+
+ public function testCheckedRadio()
+ {
+ $form = $this->factory->createNamed('name', 'radio', true);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="radio"]
+ [@name="name"]
+ [@checked="checked"]
+ [@value="1"]
+'
+ );
+ }
+
+ public function testUncheckedRadio()
+ {
+ $form = $this->factory->createNamed('name', 'radio', false);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="radio"]
+ [@name="name"]
+ [not(@checked)]
+'
+ );
+ }
+
+ public function testRadioWithValue()
+ {
+ $form = $this->factory->createNamed('name', 'radio', false, array(
+ 'value' => 'foo&bar',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="radio"]
+ [@name="name"]
+ [@value="foo&bar"]
+'
+ );
+ }
+
+ public function testTextarea()
+ {
+ $form = $this->factory->createNamed('name', 'textarea', 'foo&bar', array(
+ 'pattern' => 'foo',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/textarea
+ [@name="name"]
+ [not(@pattern)]
+ [.="foo&bar"]
+'
+ );
+ }
+
+ public function testText()
+ {
+ $form = $this->factory->createNamed('name', 'text', 'foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@value="foo&bar"]
+ [not(@maxlength)]
+'
+ );
+ }
+
+ public function testTextWithMaxLength()
+ {
+ $form = $this->factory->createNamed('name', 'text', 'foo&bar', array(
+ 'max_length' => 123,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="text"]
+ [@name="name"]
+ [@value="foo&bar"]
+ [@maxlength="123"]
+'
+ );
+ }
+
+ public function testSearch()
+ {
+ $form = $this->factory->createNamed('name', 'search', 'foo&bar');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="search"]
+ [@name="name"]
+ [@value="foo&bar"]
+ [not(@maxlength)]
+'
+ );
+ }
+
+ public function testTime()
+ {
+ $form = $this->factory->createNamed('name', 'time', '04:05:06', array(
+ 'input' => 'string',
+ 'with_seconds' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_hour"]
+ [not(@size)]
+ [./option[@value="4"][@selected="selected"]]
+ /following-sibling::select
+ [@id="name_minute"]
+ [not(@size)]
+ [./option[@value="5"][@selected="selected"]]
+ ]
+ [count(./select)=2]
+'
+ );
+ }
+
+ public function testTimeWithSeconds()
+ {
+ $form = $this->factory->createNamed('name', 'time', '04:05:06', array(
+ 'input' => 'string',
+ 'with_seconds' => true,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_hour"]
+ [not(@size)]
+ [./option[@value="4"][@selected="selected"]]
+ [count(./option)>23]
+ /following-sibling::select
+ [@id="name_minute"]
+ [not(@size)]
+ [./option[@value="5"][@selected="selected"]]
+ [count(./option)>59]
+ /following-sibling::select
+ [@id="name_second"]
+ [not(@size)]
+ [./option[@value="6"][@selected="selected"]]
+ [count(./option)>59]
+ ]
+ [count(./select)=3]
+'
+ );
+ }
+
+ public function testTimeText()
+ {
+ $form = $this->factory->createNamed('name', 'time', '04:05:06', array(
+ 'input' => 'string',
+ 'widget' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./input
+ [@type="text"]
+ [@id="name_hour"]
+ [@name="name[hour]"]
+ [@value="04"]
+ [@size="1"]
+ [@required="required"]
+ /following-sibling::input
+ [@type="text"]
+ [@id="name_minute"]
+ [@name="name[minute]"]
+ [@value="05"]
+ [@size="1"]
+ [@required="required"]
+ ]
+ [count(./input)=2]
+'
+ );
+ }
+
+ public function testTimeSingleText()
+ {
+ $form = $this->factory->createNamed('name', 'time', '04:05:06', array(
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="time"]
+ [@name="name"]
+ [@value="04:05"]
+ [not(@size)]
+'
+ );
+ }
+
+ public function testTimeWithEmptyValueGlobal()
+ {
+ $form = $this->factory->createNamed('name', 'time', null, array(
+ 'input' => 'string',
+ 'empty_value' => 'Change&Me',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_hour"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ [count(./option)>24]
+ /following-sibling::select
+ [@id="name_minute"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ [count(./option)>60]
+ ]
+ [count(./select)=2]
+'
+ );
+ }
+
+ public function testTimeWithEmptyValueOnYear()
+ {
+ $form = $this->factory->createNamed('name', 'time', null, array(
+ 'input' => 'string',
+ 'required' => false,
+ 'empty_value' => array('hour' => 'Change&Me'),
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/div
+ [
+ ./select
+ [@id="name_hour"]
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Change&Me[/trans]"]]
+ [count(./option)>24]
+ /following-sibling::select
+ [@id="name_minute"]
+ [./option[@value="1"]]
+ [count(./option)>59]
+ ]
+ [count(./select)=2]
+'
+ );
+ }
+
+ public function testTimeErrorBubbling()
+ {
+ $form = $this->factory->createNamedBuilder('form', 'form')
+ ->add('time', 'time')
+ ->getForm();
+ $form->get('time')->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+
+ $this->assertEmpty($this->renderErrors($view));
+ $this->assertNotEmpty($this->renderErrors($view['time']));
+ }
+
+ public function testTimezone()
+ {
+ $form = $this->factory->createNamed('name', 'timezone', 'Europe/Vienna');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [@name="name"]
+ [@required="required"]
+ [./optgroup
+ [@label="[trans]Europe[/trans]"]
+ [./option[@value="Europe/Vienna"][@selected="selected"][.="[trans]Vienna[/trans]"]]
+ ]
+ [count(./optgroup)>10]
+ [count(.//option)>200]
+'
+ );
+ }
+
+ public function testTimezoneWithEmptyValue()
+ {
+ $form = $this->factory->createNamed('name', 'timezone', null, array(
+ 'empty_value' => 'Select&Timezone',
+ 'required' => false,
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/select
+ [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Timezone[/trans]"]]
+ [count(./optgroup)>10]
+ [count(.//option)>201]
+'
+ );
+ }
+
+ public function testUrl()
+ {
+ $url = 'http://www.google.com?foo1=bar1&foo2=bar2';
+ $form = $this->factory->createNamed('name', 'url', $url);
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/input
+ [@type="url"]
+ [@name="name"]
+ [@value="http://www.google.com?foo1=bar1&foo2=bar2"]
+'
+ );
+ }
+
+ public function testCollectionPrototype()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form', array('items' => array('one', 'two', 'three')))
+ ->add('items', 'collection', array('allow_add' => true))
+ ->getForm()
+ ->createView();
+
+ $html = $this->renderWidget($form);
+
+ $this->assertMatchesXpath($html,
+ '//div[@id="name_items"][@data-prototype]
+ |
+ //table[@id="name_items"][@data-prototype]'
+ );
+ }
+
+ public function testEmptyRootFormName()
+ {
+ $form = $this->factory->createNamedBuilder('', 'form')
+ ->add('child', 'text')
+ ->getForm();
+
+ $this->assertMatchesXpath($this->renderWidget($form->createView()),
+ '//input[@type="hidden"][@id="_token"][@name="_token"]
+ |
+ //input[@type="text"][@id="child"][@name="child"]'
+ , 2);
+ }
+
+ public function testButton()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="button"][@name="name"][.="[trans]Name[/trans]"]'
+ );
+ }
+
+ public function testButtonLabelIsEmpty()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+
+ $this->assertSame('', $this->renderLabel($form->createView()));
+ }
+
+ public function testSubmit()
+ {
+ $form = $this->factory->createNamed('name', 'submit');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="submit"][@name="name"]'
+ );
+ }
+
+ public function testReset()
+ {
+ $form = $this->factory->createNamed('name', 'reset');
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+ '/button[@type="reset"][@name="name"]'
+ );
+ }
+
+ public function testStartTag()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'method' => 'get',
+ 'action' => 'http://example.com/directory'
+ ));
+
+ $html = $this->renderStart($form->createView());
+
+ $this->assertSame('<form method="get" action="http://example.com/directory">', $html);
+ }
+
+ public function testStartTagForPutRequest()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'method' => 'put',
+ 'action' => 'http://example.com/directory'
+ ));
+
+ $html = $this->renderStart($form->createView());
+
+ $this->assertMatchesXpath($html . '</form>',
+'/form
+ [./input[@type="hidden"][@name="_method"][@value="PUT"]]
+ [@method="post"]
+ [@action="http://example.com/directory"]'
+ );
+ }
+
+ public function testStartTagWithOverriddenVars()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'method' => 'put',
+ 'action' => 'http://example.com/directory',
+ ));
+
+ $html = $this->renderStart($form->createView(), array(
+ 'method' => 'post',
+ 'action' => 'http://foo.com/directory'
+ ));
+
+ $this->assertSame('<form method="post" action="http://foo.com/directory">', $html);
+ }
+
+ public function testStartTagForMultipartForm()
+ {
+ $form = $this->factory->createBuilder('form', null, array(
+ 'method' => 'get',
+ 'action' => 'http://example.com/directory'
+ ))
+ ->add('file', 'file')
+ ->getForm();
+
+ $html = $this->renderStart($form->createView());
+
+ $this->assertSame('<form method="get" action="http://example.com/directory" enctype="multipart/form-data">', $html);
+ }
+
+ public function testStartTagWithExtraAttributes()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'method' => 'get',
+ 'action' => 'http://example.com/directory'
+ ));
+
+ $html = $this->renderStart($form->createView(), array(
+ 'attr' => array('class' => 'foobar'),
+ ));
+
+ $this->assertSame('<form method="get" action="http://example.com/directory" class="foobar">', $html);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractRequestHandlerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \Symfony\Component\Form\RequestHandlerInterface
+ */
+ protected $requestHandler;
+
+ protected $request;
+
+ protected function setUp()
+ {
+ $this->requestHandler = $this->getRequestHandler();
+ $this->request = null;
+ }
+
+ public function methodExceptGetProvider()
+ {
+ return array(
+ array('POST'),
+ array('PUT'),
+ array('DELETE'),
+ array('PATCH'),
+ );
+ }
+
+ public function methodProvider()
+ {
+ return array_merge(array(
+ array('GET'),
+ ), $this->methodExceptGetProvider());
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testSubmitIfNameInRequest($method)
+ {
+ $form = $this->getMockForm('param1', $method);
+
+ $this->setRequestData($method, array(
+ 'param1' => 'DATA',
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with('DATA', 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testDoNotSubmitIfWrongRequestMethod($method)
+ {
+ $form = $this->getMockForm('param1', $method);
+
+ $otherMethod = 'POST' === $method ? 'PUT' : 'POST';
+
+ $this->setRequestData($otherMethod, array(
+ 'param1' => 'DATA',
+ ));
+
+ $form->expects($this->never())
+ ->method('submit');
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodExceptGetProvider
+ */
+ public function testSubmitSimpleFormWithNullIfNameNotInRequestAndNotGetRequest($method)
+ {
+ $form = $this->getMockForm('param1', $method, false);
+
+ $this->setRequestData($method, array(
+ 'paramx' => array(),
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with($this->identicalTo(null), 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodExceptGetProvider
+ */
+ public function testSubmitCompoundFormWithArrayIfNameNotInRequestAndNotGetRequest($method)
+ {
+ $form = $this->getMockForm('param1', $method, true);
+
+ $this->setRequestData($method, array(
+ 'paramx' => array(),
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with($this->identicalTo(array()), 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ public function testDoNotSubmitIfNameNotInRequestAndGetRequest()
+ {
+ $form = $this->getMockForm('param1', 'GET');
+
+ $this->setRequestData('GET', array(
+ 'paramx' => array(),
+ ));
+
+ $form->expects($this->never())
+ ->method('submit');
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testSubmitFormWithEmptyNameIfAtLeastOneFieldInRequest($method)
+ {
+ $form = $this->getMockForm('', $method);
+ $form->expects($this->any())
+ ->method('all')
+ ->will($this->returnValue(array(
+ 'param1' => $this->getMockForm('param1'),
+ 'param2' => $this->getMockForm('param2'),
+ )));
+
+ $this->setRequestData($method, $requestData = array(
+ 'param1' => 'submitted value',
+ 'paramx' => 'submitted value',
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with($requestData, 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ */
+ public function testDoNotSubmitFormWithEmptyNameIfNoFieldInRequest($method)
+ {
+ $form = $this->getMockForm('', $method);
+ $form->expects($this->any())
+ ->method('all')
+ ->will($this->returnValue(array(
+ 'param1' => $this->getMockForm('param1'),
+ 'param2' => $this->getMockForm('param2'),
+ )));
+
+ $this->setRequestData($method, array(
+ 'paramx' => 'submitted value',
+ ));
+
+ $form->expects($this->never())
+ ->method('submit');
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodExceptGetProvider
+ */
+ public function testMergeParamsAndFiles($method)
+ {
+ $form = $this->getMockForm('param1', $method);
+ $file = $this->getMockFile();
+
+ $this->setRequestData($method, array(
+ 'param1' => array(
+ 'field1' => 'DATA',
+ ),
+ ), array(
+ 'param1' => array(
+ 'field2' => $file,
+ ),
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with(array(
+ 'field1' => 'DATA',
+ 'field2' => $file,
+ ), 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodExceptGetProvider
+ */
+ public function testParamTakesPrecedenceOverFile($method)
+ {
+ $form = $this->getMockForm('param1', $method);
+ $file = $this->getMockFile();
+
+ $this->setRequestData($method, array(
+ 'param1' => 'DATA',
+ ), array(
+ 'param1' => $file,
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with('DATA', 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ /**
+ * @dataProvider methodExceptGetProvider
+ */
+ public function testSubmitFileIfNoParam($method)
+ {
+ $form = $this->getMockForm('param1', $method);
+ $file = $this->getMockFile();
+
+ $this->setRequestData($method, array(
+ 'param1' => null,
+ ), array(
+ 'param1' => $file,
+ ));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with($file, 'PATCH' !== $method);
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ abstract protected function setRequestData($method, $data, $files = array());
+
+ abstract protected function getRequestHandler();
+
+ abstract protected function getMockFile();
+
+ protected function getMockForm($name, $method = null, $compound = true)
+ {
+ $config = $this->getMock('Symfony\Component\Form\FormConfigInterface');
+ $config->expects($this->any())
+ ->method('getMethod')
+ ->will($this->returnValue($method));
+ $config->expects($this->any())
+ ->method('getCompound')
+ ->will($this->returnValue($compound));
+
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $form->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue($name));
+ $form->expects($this->any())
+ ->method('getConfig')
+ ->will($this->returnValue($config));
+
+ return $form;
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormError;
+
+abstract class AbstractTableLayoutTest extends AbstractLayoutTest
+{
+ public function testRow()
+ {
+ $form = $this->factory->createNamed('name', 'text');
+ $form->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [./label[@for="name"]]
+ /following-sibling::td
+ [
+ ./ul
+ [./li[.="[trans]Error![/trans]"]]
+ [count(./li)=1]
+ /following-sibling::input[@id="name"]
+ ]
+ ]
+'
+ );
+ }
+
+ public function testLabelIsNotRenderedWhenSetToFalse()
+ {
+ $form = $this->factory->createNamed('name', 'text', null, array(
+ 'label' => false
+ ));
+ $html = $this->renderRow($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [count(//label)=0]
+ /following-sibling::td
+ [./input[@id="name"]]
+ ]
+'
+ );
+ }
+
+ public function testRepeatedRow()
+ {
+ $form = $this->factory->createNamed('name', 'repeated');
+ $html = $this->renderRow($form->createView());
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [./label[@for="name_first"]]
+ /following-sibling::td
+ [./input[@id="name_first"]]
+ ]
+/following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_second"]]
+ /following-sibling::td
+ [./input[@id="name_second"]]
+ ]
+/following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ [count(../tr)=3]
+'
+ );
+ }
+
+ public function testRepeatedRowWithErrors()
+ {
+ $form = $this->factory->createNamed('name', 'repeated');
+ $form->addError(new FormError('[trans]Error![/trans]'));
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ // The errors of the form are not rendered by intention!
+ // In practice, repeated fields cannot have errors as all errors
+ // on them are mapped to the first child.
+ // (see RepeatedTypeValidatorExtension)
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [./label[@for="name_first"]]
+ /following-sibling::td
+ [./input[@id="name_first"]]
+ ]
+/following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_second"]]
+ /following-sibling::td
+ [./input[@id="name_second"]]
+ ]
+/following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ [count(../tr)=3]
+'
+ );
+ }
+
+ public function testButtonRow()
+ {
+ $form = $this->factory->createNamed('name', 'button');
+ $view = $form->createView();
+ $html = $this->renderRow($view);
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [.=""]
+ /following-sibling::td
+ [./button[@type="button"][@name="name"]]
+ ]
+ [count(//label)=0]
+'
+ );
+ }
+
+ public function testRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'repeated')
+ ->add('field3', 'text')
+ ->add('field4', 'text')
+ ->getForm()
+ ->createView();
+
+ // Render field2 row -> does not implicitly call renderWidget because
+ // it is a repeated field!
+ $this->renderRow($view['field2']);
+
+ // Render field3 widget
+ $this->renderWidget($view['field3']);
+
+ // Rest should only contain field1 and field4
+ $html = $this->renderRest($view);
+
+ $this->assertMatchesXpath($html,
+'/tr
+ [
+ ./td
+ [./label[@for="name_field1"]]
+ /following-sibling::td
+ [./input[@id="name_field1"]]
+ ]
+/following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_field4"]]
+ /following-sibling::td
+ [./input[@id="name_field4"]]
+ ]
+ [count(../tr)=3]
+ [count(..//label)=2]
+ [count(..//input)=3]
+/following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+'
+ );
+ }
+
+ public function testCollection()
+ {
+ $form = $this->factory->createNamed('name', 'collection', array('a', 'b'), array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr[./td/input[@type="text"][@value="a"]]
+ /following-sibling::tr[./td/input[@type="text"][@value="b"]]
+ /following-sibling::tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="name__token"]]
+ ]
+ [count(./tr[./td/input])=3]
+'
+ );
+ }
+
+ public function testEmptyCollection()
+ {
+ $form = $this->factory->createNamed('name', 'collection', array(), array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [./tr[@style="display: none"][./td[@colspan="2"]/input[@type="hidden"][@id="name__token"]]]
+ [count(./tr[./td/input])=1]
+'
+ );
+ }
+
+ public function testForm()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->setMethod('PUT')
+ ->setAction('http://example.com')
+ ->add('firstName', 'text')
+ ->add('lastName', 'text')
+ ->getForm()
+ ->createView();
+
+ $html = $this->renderForm($view, array(
+ 'id' => 'my&id',
+ 'attr' => array('class' => 'my&class'),
+ ));
+
+ $this->assertMatchesXpath($html,
+'/form
+ [
+ ./input[@type="hidden"][@name="_method"][@value="PUT"]
+ /following-sibling::table
+ [
+ ./tr
+ [
+ ./td
+ [./label[@for="name_firstName"]]
+ /following-sibling::td
+ [./input[@id="name_firstName"]]
+ ]
+ /following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_lastName"]]
+ /following-sibling::td
+ [./input[@id="name_lastName"]]
+ ]
+ /following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+ [count(.//input)=3]
+ [@id="my&id"]
+ [@class="my&class"]
+ ]
+ [@method="post"]
+ [@action="http://example.com"]
+ [@class="my&class"]
+'
+ );
+ }
+
+ public function testFormWidget()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('firstName', 'text')
+ ->add('lastName', 'text')
+ ->getForm()
+ ->createView();
+
+ $this->assertWidgetMatchesXpath($view, array(),
+'/table
+ [
+ ./tr
+ [
+ ./td
+ [./label[@for="name_firstName"]]
+ /following-sibling::td
+ [./input[@id="name_firstName"]]
+ ]
+ /following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_lastName"]]
+ /following-sibling::td
+ [./input[@id="name_lastName"]]
+ ]
+ /following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ // https://github.com/symfony/symfony/issues/2308
+ public function testNestedFormError()
+ {
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add($this->factory
+ ->createNamedBuilder('child', 'form', null, array('error_bubbling' => false))
+ ->add('grandChild', 'form')
+ )
+ ->getForm();
+
+ $form->get('child')->addError(new FormError('[trans]Error![/trans]'));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr/td/ul[./li[.="[trans]Error![/trans]"]]
+ /following-sibling::table[@id="name_child"]
+ ]
+ [count(.//li[.="[trans]Error![/trans]"])=1]
+'
+ );
+ }
+
+ public function testCsrf()
+ {
+ $this->csrfProvider->expects($this->any())
+ ->method('generateCsrfToken')
+ ->will($this->returnValue('foo&bar'));
+
+ $form = $this->factory->createNamedBuilder('name', 'form')
+ ->add($this->factory
+ // No CSRF protection on nested forms
+ ->createNamedBuilder('child', 'form')
+ ->add($this->factory->createNamedBuilder('grandchild', 'text'))
+ )
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+ [count(.//input[@type="hidden"])=1]
+'
+ );
+ }
+
+ public function testRepeated()
+ {
+ $form = $this->factory->createNamed('name', 'repeated', 'foobar', array(
+ 'type' => 'text',
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr
+ [
+ ./td
+ [./label[@for="name_first"]]
+ /following-sibling::td
+ [./input[@type="text"][@id="name_first"]]
+ ]
+ /following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_second"]]
+ /following-sibling::td
+ [./input[@type="text"][@id="name_second"]]
+ ]
+ /following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ public function testRepeatedWithCustomOptions()
+ {
+ $form = $this->factory->createNamed('name', 'repeated', 'foobar', array(
+ 'type' => 'password',
+ 'first_options' => array('label' => 'Test', 'required' => false),
+ 'second_options' => array('label' => 'Test2')
+ ));
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr
+ [
+ ./td
+ [./label[@for="name_first"][.="[trans]Test[/trans]"]]
+ /following-sibling::td
+ [./input[@type="password"][@id="name_first"][@required="required"]]
+ ]
+ /following-sibling::tr
+ [
+ ./td
+ [./label[@for="name_second"][.="[trans]Test2[/trans]"]]
+ /following-sibling::td
+ [./input[@type="password"][@id="name_second"][@required="required"]]
+ ]
+ /following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+ [count(.//input)=3]
+'
+ );
+ }
+
+ /**
+ * The block "_name_child_label" should be overridden in the theme of the
+ * implemented driver.
+ */
+ public function testCollectionRowWithCustomBlock()
+ {
+ $collection = array('one', 'two', 'three');
+ $form = $this->factory->createNamedBuilder('name', 'collection', $collection)
+ ->getForm();
+
+ $this->assertWidgetMatchesXpath($form->createView(), array(),
+'/table
+ [
+ ./tr[./td/label[.="Custom label: [trans]0[/trans]"]]
+ /following-sibling::tr[./td/label[.="Custom label: [trans]1[/trans]"]]
+ /following-sibling::tr[./td/label[.="Custom label: [trans]2[/trans]"]]
+ ]
+'
+ );
+ }
+
+ public function testFormEndWithRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text')
+ ->getForm()
+ ->createView();
+
+ $this->renderWidget($view['field1']);
+
+ // Rest should only contain field2
+ $html = $this->renderEnd($view);
+
+ // Insert the start tag, the end tag should be rendered by the helper
+ // Unfortunately this is not valid HTML, because the surrounding table
+ // tag is missing. If someone renders a form with table layout
+ // manually, she should call form_rest() explicitly within the <table>
+ // tag.
+ $this->assertMatchesXpath('<form>' . $html,
+'/form
+ [
+ ./tr
+ [
+ ./td
+ [./label[@for="name_field2"]]
+ /following-sibling::td
+ [./input[@id="name_field2"]]
+ ]
+ /following-sibling::tr[@style="display: none"]
+ [./td[@colspan="2"]/input
+ [@type="hidden"]
+ [@id="name__token"]
+ ]
+ ]
+'
+ );
+ }
+
+ public function testFormEndWithoutRest()
+ {
+ $view = $this->factory->createNamedBuilder('name', 'form')
+ ->add('field1', 'text')
+ ->add('field2', 'text')
+ ->getForm()
+ ->createView();
+
+ $this->renderWidget($view['field1']);
+
+ // Rest should only contain field2, but isn't rendered
+ $html = $this->renderEnd($view, array('render_rest' => false));
+
+ $this->assertEquals('</form>', $html);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CompoundFormPerformanceTest extends \Symfony\Component\Form\Tests\FormPerformanceTestCase
+{
+ /**
+ * Create a compound form multiple times, as happens in a collection form
+ *
+ * @group benchmark
+ */
+ public function testArrayBasedForm()
+ {
+ $this->setMaxRunningTime(1);
+
+ for ($i = 0; $i < 40; ++$i) {
+ $form = $this->factory->createBuilder('form')
+ ->add('firstName', 'text')
+ ->add('lastName', 'text')
+ ->add('gender', 'choice', array(
+ 'choices' => array('male' => 'Male', 'female' => 'Female'),
+ 'required' => false,
+ ))
+ ->add('age', 'number')
+ ->add('birthDate', 'birthday')
+ ->add('city', 'choice', array(
+ // simulate 300 different cities
+ 'choices' => range(1, 300),
+ ))
+ ->getForm();
+
+ // load the form into a view
+ $form->createView();
+ }
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
+
+class CompoundFormTest extends AbstractFormTest
+{
+ public function testValidIfAllChildrenAreValid()
+ {
+ $this->form->add($this->getValidForm('firstName'));
+ $this->form->add($this->getValidForm('lastName'));
+
+ $this->form->submit(array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ ));
+
+ $this->assertTrue($this->form->isValid());
+ }
+
+ public function testInvalidIfChildIsInvalid()
+ {
+ $this->form->add($this->getValidForm('firstName'));
+ $this->form->add($this->getInvalidForm('lastName'));
+
+ $this->form->submit(array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ ));
+
+ $this->assertFalse($this->form->isValid());
+ }
+
+ public function testSubmitForwardsNullIfValueIsMissing()
+ {
+ $child = $this->getMockForm('firstName');
+
+ $this->form->add($child);
+
+ $child->expects($this->once())
+ ->method('submit')
+ ->with($this->equalTo(null));
+
+ $this->form->submit(array());
+ }
+
+ public function testSubmitDoesNotForwardNullIfNotClearMissing()
+ {
+ $child = $this->getMockForm('firstName');
+
+ $this->form->add($child);
+
+ $child->expects($this->never())
+ ->method('submit');
+
+ $this->form->submit(array(), false);
+ }
+
+ public function testClearMissingFlagIsForwarded()
+ {
+ $child = $this->getMockForm('firstName');
+
+ $this->form->add($child);
+
+ $child->expects($this->once())
+ ->method('submit')
+ ->with($this->equalTo('foo'), false);
+
+ $this->form->submit(array('firstName' => 'foo'), false);
+ }
+
+ public function testCloneChildren()
+ {
+ $child = $this->getBuilder('child')->getForm();
+ $this->form->add($child);
+
+ $clone = clone $this->form;
+
+ $this->assertNotSame($this->form, $clone);
+ $this->assertNotSame($child, $clone['child']);
+ }
+
+ public function testNotEmptyIfChildNotEmpty()
+ {
+ $child = $this->getMockForm();
+ $child->expects($this->once())
+ ->method('isEmpty')
+ ->will($this->returnValue(false));
+
+ $this->form->setData(null);
+ $this->form->add($child);
+
+ $this->assertFalse($this->form->isEmpty());
+ }
+
+ public function testValidIfSubmittedAndDisabledWithChildren()
+ {
+ $this->factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('name', 'text', null, array())
+ ->will($this->returnValue($this->getBuilder('name')));
+
+ $form = $this->getBuilder('person')
+ ->setDisabled(true)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->add('name', 'text')
+ ->getForm();
+ $form->submit(array('name' => 'Jacques Doe'));
+
+ $this->assertTrue($form->isValid());
+ }
+
+ public function testNotValidIfChildNotValid()
+ {
+ $child = $this->getMockForm();
+ $child->expects($this->once())
+ ->method('isValid')
+ ->will($this->returnValue(false));
+
+ $this->form->add($child);
+ $this->form->submit(array());
+
+ $this->assertFalse($this->form->isValid());
+ }
+
+ public function testAdd()
+ {
+ $child = $this->getBuilder('foo')->getForm();
+ $this->form->add($child);
+
+ $this->assertTrue($this->form->has('foo'));
+ $this->assertSame($this->form, $child->getParent());
+ $this->assertSame(array('foo' => $child), $this->form->all());
+ }
+
+ public function testAddUsingNameAndType()
+ {
+ $child = $this->getBuilder('foo')->getForm();
+
+ $this->factory->expects($this->once())
+ ->method('createNamed')
+ ->with('foo', 'text', null, array(
+ 'bar' => 'baz',
+ 'auto_initialize' => false,
+ ))
+ ->will($this->returnValue($child));
+
+ $this->form->add('foo', 'text', array('bar' => 'baz'));
+
+ $this->assertTrue($this->form->has('foo'));
+ $this->assertSame($this->form, $child->getParent());
+ $this->assertSame(array('foo' => $child), $this->form->all());
+ }
+
+ public function testAddUsingIntegerNameAndType()
+ {
+ $child = $this->getBuilder(0)->getForm();
+
+ $this->factory->expects($this->once())
+ ->method('createNamed')
+ ->with('0', 'text', null, array(
+ 'bar' => 'baz',
+ 'auto_initialize' => false,
+ ))
+ ->will($this->returnValue($child));
+
+ // in order to make casting unnecessary
+ $this->form->add(0, 'text', array('bar' => 'baz'));
+
+ $this->assertTrue($this->form->has(0));
+ $this->assertSame($this->form, $child->getParent());
+ $this->assertSame(array(0 => $child), $this->form->all());
+ }
+
+ public function testAddUsingNameButNoType()
+ {
+ $this->form = $this->getBuilder('name', null, '\stdClass')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+
+ $child = $this->getBuilder('foo')->getForm();
+
+ $this->factory->expects($this->once())
+ ->method('createForProperty')
+ ->with('\stdClass', 'foo')
+ ->will($this->returnValue($child));
+
+ $this->form->add('foo');
+
+ $this->assertTrue($this->form->has('foo'));
+ $this->assertSame($this->form, $child->getParent());
+ $this->assertSame(array('foo' => $child), $this->form->all());
+ }
+
+ public function testAddUsingNameButNoTypeAndOptions()
+ {
+ $this->form = $this->getBuilder('name', null, '\stdClass')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+
+ $child = $this->getBuilder('foo')->getForm();
+
+ $this->factory->expects($this->once())
+ ->method('createForProperty')
+ ->with('\stdClass', 'foo', null, array(
+ 'bar' => 'baz',
+ 'auto_initialize' => false,
+ ))
+ ->will($this->returnValue($child));
+
+ $this->form->add('foo', null, array('bar' => 'baz'));
+
+ $this->assertTrue($this->form->has('foo'));
+ $this->assertSame($this->form, $child->getParent());
+ $this->assertSame(array('foo' => $child), $this->form->all());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\AlreadySubmittedException
+ */
+ public function testAddThrowsExceptionIfAlreadySubmitted()
+ {
+ $this->form->submit(array());
+ $this->form->add($this->getBuilder('foo')->getForm());
+ }
+
+ public function testRemove()
+ {
+ $child = $this->getBuilder('foo')->getForm();
+ $this->form->add($child);
+ $this->form->remove('foo');
+
+ $this->assertNull($child->getParent());
+ $this->assertCount(0, $this->form);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\AlreadySubmittedException
+ */
+ public function testRemoveThrowsExceptionIfAlreadySubmitted()
+ {
+ $this->form->add($this->getBuilder('foo')->setCompound(false)->getForm());
+ $this->form->submit(array('foo' => 'bar'));
+ $this->form->remove('foo');
+ }
+
+ public function testRemoveIgnoresUnknownName()
+ {
+ $this->form->remove('notexisting');
+ }
+
+ public function testArrayAccess()
+ {
+ $child = $this->getBuilder('foo')->getForm();
+
+ $this->form[] = $child;
+
+ $this->assertTrue(isset($this->form['foo']));
+ $this->assertSame($child, $this->form['foo']);
+
+ unset($this->form['foo']);
+
+ $this->assertFalse(isset($this->form['foo']));
+ }
+
+ public function testCountable()
+ {
+ $this->form->add($this->getBuilder('foo')->getForm());
+ $this->form->add($this->getBuilder('bar')->getForm());
+
+ $this->assertCount(2, $this->form);
+ }
+
+ public function testIterator()
+ {
+ $this->form->add($this->getBuilder('foo')->getForm());
+ $this->form->add($this->getBuilder('bar')->getForm());
+
+ $this->assertSame($this->form->all(), iterator_to_array($this->form));
+ }
+
+ public function testAddMapsViewDataToFormIfInitialized()
+ {
+ $test = $this;
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => 'bar',
+ )))
+ ->setData('foo')
+ ->getForm();
+
+ $child = $this->getBuilder()->getForm();
+ $mapper->expects($this->once())
+ ->method('mapDataToForms')
+ ->with('bar', $this->isInstanceOf('\RecursiveIteratorIterator'))
+ ->will($this->returnCallback(function ($data, \RecursiveIteratorIterator $iterator) use ($child, $test) {
+ $test->assertInstanceOf('Symfony\Component\Form\Util\InheritDataAwareIterator', $iterator->getInnerIterator());
+ $test->assertSame(array($child), iterator_to_array($iterator));
+ }));
+
+ $form->initialize();
+ $form->add($child);
+ }
+
+ public function testAddDoesNotMapViewDataToFormIfNotInitialized()
+ {
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->getForm();
+
+ $child = $this->getBuilder()->getForm();
+ $mapper->expects($this->never())
+ ->method('mapDataToForms');
+
+ $form->add($child);
+ }
+
+ public function testAddDoesNotMapViewDataToFormIfInheritData()
+ {
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->setInheritData(true)
+ ->getForm();
+
+ $child = $this->getBuilder()->getForm();
+ $mapper->expects($this->never())
+ ->method('mapDataToForms');
+
+ $form->initialize();
+ $form->add($child);
+ }
+
+ public function testSetDataMapsViewDataToChildren()
+ {
+ $test = $this;
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => 'bar',
+ )))
+ ->getForm();
+
+ $form->add($child1 = $this->getBuilder('firstName')->getForm());
+ $form->add($child2 = $this->getBuilder('lastName')->getForm());
+
+ $mapper->expects($this->once())
+ ->method('mapDataToForms')
+ ->with('bar', $this->isInstanceOf('\RecursiveIteratorIterator'))
+ ->will($this->returnCallback(function ($data, \RecursiveIteratorIterator $iterator) use ($child1, $child2, $test) {
+ $test->assertInstanceOf('Symfony\Component\Form\Util\InheritDataAwareIterator', $iterator->getInnerIterator());
+ $test->assertSame(array('firstName' => $child1, 'lastName' => $child2), iterator_to_array($iterator));
+ }));
+
+ $form->setData('foo');
+ }
+
+ public function testSubmitMapsSubmittedChildrenOntoExistingViewData()
+ {
+ $test = $this;
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => 'bar',
+ )))
+ ->setData('foo')
+ ->getForm();
+
+ $form->add($child1 = $this->getBuilder('firstName')->setCompound(false)->getForm());
+ $form->add($child2 = $this->getBuilder('lastName')->setCompound(false)->getForm());
+
+ $mapper->expects($this->once())
+ ->method('mapFormsToData')
+ ->with($this->isInstanceOf('\RecursiveIteratorIterator'), 'bar')
+ ->will($this->returnCallback(function (\RecursiveIteratorIterator $iterator) use ($child1, $child2, $test) {
+ $test->assertInstanceOf('Symfony\Component\Form\Util\InheritDataAwareIterator', $iterator->getInnerIterator());
+ $test->assertSame(array('firstName' => $child1, 'lastName' => $child2), iterator_to_array($iterator));
+ $test->assertEquals('Bernhard', $child1->getData());
+ $test->assertEquals('Schussek', $child2->getData());
+ }));
+
+ $form->submit(array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ ));
+ }
+
+ public function testMapFormsToDataIsNotInvokedIfInheritData()
+ {
+ $mapper = $this->getDataMapper();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->setInheritData(true)
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => 'bar',
+ )))
+ ->getForm();
+
+ $form->add($child1 = $this->getBuilder('firstName')->setCompound(false)->getForm());
+ $form->add($child2 = $this->getBuilder('lastName')->setCompound(false)->getForm());
+
+ $mapper->expects($this->never())
+ ->method('mapFormsToData');
+
+ $form->submit(array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ ));
+ }
+
+ /*
+ * https://github.com/symfony/symfony/issues/4480
+ */
+ public function testSubmitRestoresViewDataIfCompoundAndEmpty()
+ {
+ $mapper = $this->getDataMapper();
+ $object = new \stdClass();
+ $form = $this->getBuilder('name', null, 'stdClass')
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->setData($object)
+ ->getForm();
+
+ $form->submit(array());
+
+ $this->assertSame($object, $form->getData());
+ }
+
+ public function testSubmitMapsSubmittedChildrenOntoEmptyData()
+ {
+ $test = $this;
+ $mapper = $this->getDataMapper();
+ $object = new \stdClass();
+ $form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($mapper)
+ ->setEmptyData($object)
+ ->setData(null)
+ ->getForm();
+
+ $form->add($child = $this->getBuilder('name')->setCompound(false)->getForm());
+
+ $mapper->expects($this->once())
+ ->method('mapFormsToData')
+ ->with($this->isInstanceOf('\RecursiveIteratorIterator'), $object)
+ ->will($this->returnCallback(function (\RecursiveIteratorIterator $iterator) use ($child, $test) {
+ $test->assertInstanceOf('Symfony\Component\Form\Util\InheritDataAwareIterator', $iterator->getInnerIterator());
+ $test->assertSame(array('name' => $child), iterator_to_array($iterator));
+ }));
+
+ $form->submit(array(
+ 'name' => 'Bernhard',
+ ));
+ }
+
+ public function requestMethodProvider()
+ {
+ return array(
+ array('POST'),
+ array('PUT'),
+ array('DELETE'),
+ array('PATCH'),
+ );
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitPostOrPutRequest($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $path = tempnam(sys_get_temp_dir(), 'sf2');
+ touch($path);
+
+ $values = array(
+ 'author' => array(
+ 'name' => 'Bernhard',
+ 'image' => array('filename' => 'foobar.png'),
+ ),
+ );
+
+ $files = array(
+ 'author' => array(
+ 'error' => array('image' => UPLOAD_ERR_OK),
+ 'name' => array('image' => 'upload.png'),
+ 'size' => array('image' => 123),
+ 'tmp_name' => array('image' => $path),
+ 'type' => array('image' => 'image/png'),
+ ),
+ );
+
+ $request = new Request(array(), $values, array(), array(), $files, array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $form = $this->getBuilder('author')
+ ->setMethod($method)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+ $form->add($this->getBuilder('name')->getForm());
+ $form->add($this->getBuilder('image')->getForm());
+
+ $form->handleRequest($request);
+
+ $file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
+
+ $this->assertEquals('Bernhard', $form['name']->getData());
+ $this->assertEquals($file, $form['image']->getData());
+
+ unlink($path);
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitPostOrPutRequestWithEmptyRootFormName($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $path = tempnam(sys_get_temp_dir(), 'sf2');
+ touch($path);
+
+ $values = array(
+ 'name' => 'Bernhard',
+ 'extra' => 'data',
+ );
+
+ $files = array(
+ 'image' => array(
+ 'error' => UPLOAD_ERR_OK,
+ 'name' => 'upload.png',
+ 'size' => 123,
+ 'tmp_name' => $path,
+ 'type' => 'image/png',
+ ),
+ );
+
+ $request = new Request(array(), $values, array(), array(), $files, array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $form = $this->getBuilder('')
+ ->setMethod($method)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+ $form->add($this->getBuilder('name')->getForm());
+ $form->add($this->getBuilder('image')->getForm());
+
+ $form->handleRequest($request);
+
+ $file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
+
+ $this->assertEquals('Bernhard', $form['name']->getData());
+ $this->assertEquals($file, $form['image']->getData());
+ $this->assertEquals(array('extra' => 'data'), $form->getExtraData());
+
+ unlink($path);
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitPostOrPutRequestWithSingleChildForm($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $path = tempnam(sys_get_temp_dir(), 'sf2');
+ touch($path);
+
+ $files = array(
+ 'image' => array(
+ 'error' => UPLOAD_ERR_OK,
+ 'name' => 'upload.png',
+ 'size' => 123,
+ 'tmp_name' => $path,
+ 'type' => 'image/png',
+ ),
+ );
+
+ $request = new Request(array(), array(), array(), array(), $files, array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $form = $this->getBuilder('image')
+ ->setMethod($method)
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+
+ $form->handleRequest($request);
+
+ $file = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
+
+ $this->assertEquals($file, $form->getData());
+
+ unlink($path);
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitPostOrPutRequestWithSingleChildFormUploadedFile($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $path = tempnam(sys_get_temp_dir(), 'sf2');
+ touch($path);
+
+ $values = array(
+ 'name' => 'Bernhard',
+ );
+
+ $request = new Request(array(), $values, array(), array(), array(), array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $form = $this->getBuilder('name')
+ ->setMethod($method)
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+
+ $form->handleRequest($request);
+
+ $this->assertEquals('Bernhard', $form->getData());
+
+ unlink($path);
+ }
+
+ public function testSubmitGetRequest()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $values = array(
+ 'author' => array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ ),
+ );
+
+ $request = new Request($values, array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $form = $this->getBuilder('author')
+ ->setMethod('GET')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+ $form->add($this->getBuilder('firstName')->getForm());
+ $form->add($this->getBuilder('lastName')->getForm());
+
+ $form->handleRequest($request);
+
+ $this->assertEquals('Bernhard', $form['firstName']->getData());
+ $this->assertEquals('Schussek', $form['lastName']->getData());
+ }
+
+ public function testSubmitGetRequestWithEmptyRootFormName()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $values = array(
+ 'firstName' => 'Bernhard',
+ 'lastName' => 'Schussek',
+ 'extra' => 'data'
+ );
+
+ $request = new Request($values, array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $form = $this->getBuilder('')
+ ->setMethod('GET')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setRequestHandler(new HttpFoundationRequestHandler())
+ ->getForm();
+ $form->add($this->getBuilder('firstName')->getForm());
+ $form->add($this->getBuilder('lastName')->getForm());
+
+ $form->handleRequest($request);
+
+ $this->assertEquals('Bernhard', $form['firstName']->getData());
+ $this->assertEquals('Schussek', $form['lastName']->getData());
+ $this->assertEquals(array('extra' => 'data'), $form->getExtraData());
+ }
+
+ public function testGetErrorsAsStringDeep()
+ {
+ $parent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+
+ $this->form->addError(new FormError('Error!'));
+
+ $parent->add($this->form);
+ $parent->add($this->getBuilder('foo')->getForm());
+
+ $this->assertEquals("name:\n ERROR: Error!\nfoo:\n No errors\n", $parent->getErrorsAsString());
+ }
+
+ protected function createForm()
+ {
+ return $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class ChoiceListTest extends \PHPUnit_Framework_TestCase
+{
+ private $obj1;
+
+ private $obj2;
+
+ private $obj3;
+
+ private $obj4;
+
+ private $list;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->obj1 = new \stdClass();
+ $this->obj2 = new \stdClass();
+ $this->obj3 = new \stdClass();
+ $this->obj4 = new \stdClass();
+
+ $this->list = new ChoiceList(
+ array(
+ 'Group 1' => array($this->obj1, $this->obj2),
+ 'Group 2' => array($this->obj3, $this->obj4),
+ ),
+ array(
+ 'Group 1' => array('A', 'B'),
+ 'Group 2' => array('C', 'D'),
+ ),
+ array($this->obj2, $this->obj3)
+ );
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->obj1 = null;
+ $this->obj2 = null;
+ $this->obj3 = null;
+ $this->obj4 = null;
+ $this->list = null;
+ }
+
+ public function testInitArray()
+ {
+ $this->list = new ChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
+ array('A', 'B', 'C', 'D'),
+ array($this->obj2)
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
+ $this->assertEquals(array(1 => new ChoiceView($this->obj2, '1', 'B')), $this->list->getPreferredViews());
+ $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews());
+ }
+
+ /**
+ * Necessary for interoperability with MongoDB cursors or ORM relations as
+ * choices parameter. A choice itself that is an object implementing \Traversable
+ * is not treated as hierarchical structure, but as-is.
+ */
+ public function testInitNestedTraversable()
+ {
+ $traversableChoice = new \ArrayIterator(array($this->obj3, $this->obj4));
+
+ $this->list = new ChoiceList(
+ new \ArrayIterator(array(
+ 'Group' => array($this->obj1, $this->obj2),
+ 'Not a Group' => $traversableChoice
+ )),
+ array(
+ 'Group' => array('A', 'B'),
+ 'Not a Group' => 'C',
+ ),
+ array($this->obj2)
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $traversableChoice), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2'), $this->list->getValues());
+ $this->assertEquals(array(
+ 'Group' => array(1 => new ChoiceView($this->obj2, '1', 'B'))
+ ), $this->list->getPreferredViews());
+ $this->assertEquals(array(
+ 'Group' => array(0 => new ChoiceView($this->obj1, '0', 'A')),
+ 2 => new ChoiceView($traversableChoice, '2', 'C')
+ ), $this->list->getRemainingViews());
+ }
+
+ public function testInitNestedArray()
+ {
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
+ $this->assertEquals(array(
+ 'Group 1' => array(1 => new ChoiceView($this->obj2, '1', 'B')),
+ 'Group 2' => array(2 => new ChoiceView($this->obj3, '2', 'C'))
+ ), $this->list->getPreferredViews());
+ $this->assertEquals(array(
+ 'Group 1' => array(0 => new ChoiceView($this->obj1, '0', 'A')),
+ 'Group 2' => array(3 => new ChoiceView($this->obj4, '3', 'D'))
+ ), $this->list->getRemainingViews());
+ }
+
+ public function testGetIndicesForChoices()
+ {
+ $choices = array($this->obj2, $this->obj3);
+ $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForChoicesIgnoresNonExistingChoices()
+ {
+ $choices = array($this->obj2, $this->obj3, 'foobar');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForValues()
+ {
+ // values and indices are always the same
+ $values = array('1', '2');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
+ }
+
+ public function testGetIndicesForValuesIgnoresNonExistingValues()
+ {
+ $values = array('1', '2', '5');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
+ }
+
+ public function testGetChoicesForValues()
+ {
+ $values = array('1', '2');
+ $this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetChoicesForValuesCorrectOrderingOfResult()
+ {
+ $values = array('2', '1');
+ $this->assertSame(array($this->obj3, $this->obj2), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetChoicesForValuesIgnoresNonExistingValues()
+ {
+ $values = array('1', '2', '5');
+ $this->assertSame(array($this->obj2, $this->obj3), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetValuesForChoices()
+ {
+ $choices = array($this->obj2, $this->obj3);
+ $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
+ }
+
+ public function testGetValuesForChoicesIgnoresNonExistingChoices()
+ {
+ $choices = array($this->obj2, $this->obj3, 'foobar');
+ $this->assertSame(array('1', '2'), $this->list->getValuesForChoices($choices));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testNonMatchingLabels()
+ {
+ $this->list = new ChoiceList(
+ array($this->obj1, $this->obj2),
+ array('A')
+ );
+ }
+
+ public function testLabelsContainingNull()
+ {
+ $this->list = new ChoiceList(
+ array($this->obj1, $this->obj2),
+ array('A', null)
+ );
+
+ $this->assertEquals(
+ array(0 => new ChoiceView($this->obj1, '0', 'A'), 1 => new ChoiceView($this->obj2, '1', null)),
+ $this->list->getRemainingViews()
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class LazyChoiceListTest extends \PHPUnit_Framework_TestCase
+{
+ private $list;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->list = new LazyChoiceListTest_Impl(new SimpleChoiceList(array(
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ ), array('b')));
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->list = null;
+ }
+
+ public function testGetChoices()
+ {
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices());
+ }
+
+ public function testGetValues()
+ {
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues());
+ }
+
+ public function testGetPreferredViews()
+ {
+ $this->assertEquals(array(1 => new ChoiceView('b', 'b', 'B')), $this->list->getPreferredViews());
+ }
+
+ public function testGetRemainingViews()
+ {
+ $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews());
+ }
+
+ public function testGetIndicesForChoices()
+ {
+ $choices = array('b', 'c');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForValues()
+ {
+ $values = array('b', 'c');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
+ }
+
+ public function testGetChoicesForValues()
+ {
+ $values = array('b', 'c');
+ $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetValuesForChoices()
+ {
+ $choices = array('b', 'c');
+ $this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
+ */
+ public function testLoadChoiceListShouldReturnChoiceList()
+ {
+ $list = new LazyChoiceListTest_InvalidImpl();
+
+ $list->getChoices();
+ }
+}
+
+class LazyChoiceListTest_Impl extends LazyChoiceList
+{
+ private $choiceList;
+
+ public function __construct($choiceList)
+ {
+ $this->choiceList = $choiceList;
+ }
+
+ protected function loadChoiceList()
+ {
+ return $this->choiceList;
+ }
+}
+
+class LazyChoiceListTest_InvalidImpl extends LazyChoiceList
+{
+ protected function loadChoiceList()
+ {
+ return new \stdClass();
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class ObjectChoiceListTest_EntityWithToString
+{
+ private $property;
+
+ public function __construct($property)
+ {
+ $this->property = $property;
+ }
+
+ public function __toString()
+ {
+ return $this->property;
+ }
+}
+
+class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase
+{
+ private $obj1;
+
+ private $obj2;
+
+ private $obj3;
+
+ private $obj4;
+
+ /**
+ * @var ObjectChoiceList
+ */
+ private $list;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->obj1 = (object) array('name' => 'A');
+ $this->obj2 = (object) array('name' => 'B');
+ $this->obj3 = (object) array('name' => 'C');
+ $this->obj4 = (object) array('name' => 'D');
+
+ $this->list = new ObjectChoiceList(
+ array(
+ 'Group 1' => array($this->obj1, $this->obj2),
+ 'Group 2' => array($this->obj3, $this->obj4),
+ ),
+ 'name',
+ array($this->obj2, $this->obj3)
+ );
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->obj1 = null;
+ $this->obj2 = null;
+ $this->obj3 = null;
+ $this->obj4 = null;
+ $this->list = null;
+ }
+
+ public function testInitArray()
+ {
+ $this->list = new ObjectChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
+ 'name',
+ array($this->obj2)
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
+ $this->assertEquals(array(1 => new ChoiceView($this->obj2, '1', 'B')), $this->list->getPreferredViews());
+ $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews());
+ }
+
+ public function testInitNestedArray()
+ {
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
+ $this->assertEquals(array(
+ 'Group 1' => array(1 => new ChoiceView($this->obj2, '1', 'B')),
+ 'Group 2' => array(2 => new ChoiceView($this->obj3, '2', 'C'))
+ ), $this->list->getPreferredViews());
+ $this->assertEquals(array(
+ 'Group 1' => array(0 => new ChoiceView($this->obj1, '0', 'A')),
+ 'Group 2' => array(3 => new ChoiceView($this->obj4, '3', 'D'))
+ ), $this->list->getRemainingViews());
+ }
+
+ public function testInitArrayWithGroupPath()
+ {
+ $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1');
+ $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1');
+ $this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2');
+ $this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2');
+
+ // Objects with NULL groups are not grouped
+ $obj5 = (object) array('name' => 'E', 'category' => null);
+
+ // Objects without the group property are not grouped either
+ // see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf
+ $obj6 = (object) array('name' => 'F');
+
+ $this->list = new ObjectChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6),
+ 'name',
+ array($this->obj2, $this->obj3),
+ 'category'
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4, $obj5, $obj6), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3', '4', '5'), $this->list->getValues());
+ $this->assertEquals(array(
+ 'Group 1' => array(1 => new ChoiceView($this->obj2, '1', 'B')),
+ 'Group 2' => array(2 => new ChoiceView($this->obj3, '2', 'C'))
+ ), $this->list->getPreferredViews());
+ $this->assertEquals(array(
+ 'Group 1' => array(0 => new ChoiceView($this->obj1, '0', 'A')),
+ 'Group 2' => array(3 => new ChoiceView($this->obj4, '3', 'D')),
+ 4 => new ChoiceView($obj5, '4', 'E'),
+ 5 => new ChoiceView($obj6, '5', 'F'),
+ ), $this->list->getRemainingViews());
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testInitArrayWithGroupPathThrowsExceptionIfNestedArray()
+ {
+ $this->obj1 = (object) array('name' => 'A', 'category' => 'Group 1');
+ $this->obj2 = (object) array('name' => 'B', 'category' => 'Group 1');
+ $this->obj3 = (object) array('name' => 'C', 'category' => 'Group 2');
+ $this->obj4 = (object) array('name' => 'D', 'category' => 'Group 2');
+
+ new ObjectChoiceList(
+ array(
+ 'Group 1' => array($this->obj1, $this->obj2),
+ 'Group 2' => array($this->obj3, $this->obj4),
+ ),
+ 'name',
+ array($this->obj2, $this->obj3),
+ 'category'
+ );
+ }
+
+ public function testInitArrayWithValuePath()
+ {
+ $this->obj1 = (object) array('name' => 'A', 'id' => 10);
+ $this->obj2 = (object) array('name' => 'B', 'id' => 20);
+ $this->obj3 = (object) array('name' => 'C', 'id' => 30);
+ $this->obj4 = (object) array('name' => 'D', 'id' => 40);
+
+ $this->list = new ObjectChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4),
+ 'name',
+ array($this->obj2, $this->obj3),
+ null,
+ 'id'
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('10', '20', '30', '40'), $this->list->getValues());
+ $this->assertEquals(array(1 => new ChoiceView($this->obj2, '20', 'B'), 2 => new ChoiceView($this->obj3, '30', 'C')), $this->list->getPreferredViews());
+ $this->assertEquals(array(0 => new ChoiceView($this->obj1, '10', 'A'), 3 => new ChoiceView($this->obj4, '40', 'D')), $this->list->getRemainingViews());
+ }
+
+ public function testInitArrayUsesToString()
+ {
+ $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A');
+ $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B');
+ $this->obj3 = new ObjectChoiceListTest_EntityWithToString('C');
+ $this->obj4 = new ObjectChoiceListTest_EntityWithToString('D');
+
+ $this->list = new ObjectChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4)
+ );
+
+ $this->assertSame(array($this->obj1, $this->obj2, $this->obj3, $this->obj4), $this->list->getChoices());
+ $this->assertSame(array('0', '1', '2', '3'), $this->list->getValues());
+ $this->assertEquals(array(0 => new ChoiceView($this->obj1, '0', 'A'), 1 => new ChoiceView($this->obj2, '1', 'B'), 2 => new ChoiceView($this->obj3, '2', 'C'), 3 => new ChoiceView($this->obj4, '3', 'D')), $this->list->getRemainingViews());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\StringCastException
+ */
+ public function testInitArrayThrowsExceptionIfToStringNotFound()
+ {
+ $this->obj1 = new ObjectChoiceListTest_EntityWithToString('A');
+ $this->obj2 = new ObjectChoiceListTest_EntityWithToString('B');
+ $this->obj3 = (object) array('name' => 'C');
+ $this->obj4 = new ObjectChoiceListTest_EntityWithToString('D');
+
+ new ObjectChoiceList(
+ array($this->obj1, $this->obj2, $this->obj3, $this->obj4)
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\ChoiceList;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class SimpleChoiceListTest extends \PHPUnit_Framework_TestCase
+{
+ private $list;
+
+ private $numericList;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $choices = array(
+ 'Group 1' => array('a' => 'A', 'b' => 'B'),
+ 'Group 2' => array('c' => 'C', 'd' => 'D'),
+ );
+ $numericChoices = array(
+ 'Group 1' => array(0 => 'A', 1 => 'B'),
+ 'Group 2' => array(2 => 'C', 3 => 'D'),
+ );
+
+ $this->list = new SimpleChoiceList($choices, array('b', 'c'));
+
+ // Use COPY_CHOICE strategy to test for the various associated problems
+ $this->numericList = new SimpleChoiceList($numericChoices, array(1, 2));
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->list = null;
+ $this->numericList = null;
+ }
+
+ public function testInitArray()
+ {
+ $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C');
+ $this->list = new SimpleChoiceList($choices, array('b'));
+
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getChoices());
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c'), $this->list->getValues());
+ $this->assertEquals(array(1 => new ChoiceView('b', 'b', 'B')), $this->list->getPreferredViews());
+ $this->assertEquals(array(0 => new ChoiceView('a', 'a', 'A'), 2 => new ChoiceView('c', 'c', 'C')), $this->list->getRemainingViews());
+ }
+
+ public function testInitNestedArray()
+ {
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getChoices());
+ $this->assertSame(array(0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd'), $this->list->getValues());
+ $this->assertEquals(array(
+ 'Group 1' => array(1 => new ChoiceView('b', 'b', 'B')),
+ 'Group 2' => array(2 => new ChoiceView('c', 'c', 'C'))
+ ), $this->list->getPreferredViews());
+ $this->assertEquals(array(
+ 'Group 1' => array(0 => new ChoiceView('a', 'a', 'A')),
+ 'Group 2' => array(3 => new ChoiceView('d', 'd', 'D'))
+ ), $this->list->getRemainingViews());
+ }
+
+ public function testGetIndicesForChoices()
+ {
+ $choices = array('b', 'c');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForChoicesIgnoresNonExistingChoices()
+ {
+ $choices = array('b', 'c', 'foobar');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForChoicesDealsWithNumericChoices()
+ {
+ // Pass choices as strings although they are integers
+ $choices = array('0', '1');
+ $this->assertSame(array(0, 1), $this->numericList->getIndicesForChoices($choices));
+ }
+
+ public function testGetIndicesForValues()
+ {
+ $values = array('b', 'c');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
+ }
+
+ public function testGetIndicesForValuesIgnoresNonExistingValues()
+ {
+ $values = array('b', 'c', '100');
+ $this->assertSame(array(1, 2), $this->list->getIndicesForValues($values));
+ }
+
+ public function testGetIndicesForValuesDealsWithNumericValues()
+ {
+ // Pass values as strings although they are integers
+ $values = array('0', '1');
+ $this->assertSame(array(0, 1), $this->numericList->getIndicesForValues($values));
+ }
+
+ public function testGetChoicesForValues()
+ {
+ $values = array('b', 'c');
+ $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetChoicesForValuesIgnoresNonExistingValues()
+ {
+ $values = array('b', 'c', '100');
+ $this->assertSame(array('b', 'c'), $this->list->getChoicesForValues($values));
+ }
+
+ public function testGetChoicesForValuesDealsWithNumericValues()
+ {
+ // Pass values as strings although they are integers
+ $values = array('0', '1');
+ $this->assertSame(array(0, 1), $this->numericList->getChoicesForValues($values));
+ }
+
+ public function testGetValuesForChoices()
+ {
+ $choices = array('b', 'c');
+ $this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices));
+ }
+
+ public function testGetValuesForChoicesIgnoresNonExistingValues()
+ {
+ $choices = array('b', 'c', 'foobar');
+ $this->assertSame(array('b', 'c'), $this->list->getValuesForChoices($choices));
+ }
+
+ public function testGetValuesForChoicesDealsWithNumericValues()
+ {
+ // Pass values as strings although they are integers
+ $values = array('0', '1');
+
+ $this->assertSame(array('0', '1'), $this->numericList->getValuesForChoices($values));
+ }
+
+ /**
+ * @dataProvider dirtyValuesProvider
+ */
+ public function testGetValuesForChoicesDealsWithDirtyValues($choice, $value)
+ {
+ $choices = array(
+ '0' => 'Zero',
+ '1' => 'One',
+ '' => 'Empty',
+ '1.23' => 'Float',
+ 'foo' => 'Foo',
+ 'foo10' => 'Foo 10',
+ );
+
+ // use COPY_CHOICE strategy to test the problems
+ $this->list = new SimpleChoiceList($choices, array());
+
+ $this->assertSame(array($value), $this->list->getValuesForChoices(array($choice)));
+ }
+
+ public function dirtyValuesProvider()
+ {
+ return array(
+ array(0, '0'),
+ array('0', '0'),
+ array('1', '1'),
+ array(false, '0'),
+ array(true, '1'),
+ array('', ''),
+ array(null, ''),
+ array('1.23', '1.23'),
+ array('foo', 'foo'),
+ array('foo10', 'foo10'),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataMapper;
+
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\FormConfigInterface;
+use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper;
+
+class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var PropertyPathMapper
+ */
+ private $mapper;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dispatcher;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $propertyAccessor;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\Event')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) {
+ $this->markTestSkipped('The "PropertyAccess" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->propertyAccessor = $this->getMock('Symfony\Component\PropertyAccess\PropertyAccessorInterface');
+ $this->mapper = new PropertyPathMapper($this->propertyAccessor);
+ }
+
+ /**
+ * @param $path
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getPropertyPath($path)
+ {
+ return $this->getMockBuilder('Symfony\Component\PropertyAccess\PropertyPath')
+ ->setConstructorArgs(array($path))
+ ->setMethods(array('getValue', 'setValue'))
+ ->getMock();
+ }
+
+ /**
+ * @param FormConfigInterface $config
+ * @param Boolean $synchronized
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getForm(FormConfigInterface $config, $synchronized = true)
+ {
+ $form = $this->getMockBuilder('Symfony\Component\Form\Form')
+ ->setConstructorArgs(array($config))
+ ->setMethods(array('isSynchronized'))
+ ->getMock();
+
+ $form->expects($this->any())
+ ->method('isSynchronized')
+ ->will($this->returnValue($synchronized));
+
+ return $form;
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ public function testMapDataToFormsPassesObjectRefIfByReference()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->once())
+ ->method('getValue')
+ ->with($car, $propertyPath)
+ ->will($this->returnValue($engine));
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapDataToForms($car, array($form));
+
+ // Can't use isIdentical() above because mocks always clone their
+ // arguments which can't be disabled in PHPUnit 3.6
+ $this->assertSame($engine, $form->getData());
+ }
+
+ public function testMapDataToFormsPassesObjectCloneIfNotByReference()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->once())
+ ->method('getValue')
+ ->with($car, $propertyPath)
+ ->will($this->returnValue($engine));
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(false);
+ $config->setPropertyPath($propertyPath);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapDataToForms($car, array($form));
+
+ $this->assertNotSame($engine, $form->getData());
+ $this->assertEquals($engine, $form->getData());
+ }
+
+ public function testMapDataToFormsIgnoresEmptyPropertyPath()
+ {
+ $car = new \stdClass();
+
+ $config = new FormConfigBuilder(null, '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $form = $this->getForm($config);
+
+ $this->assertNull($form->getPropertyPath());
+
+ $this->mapper->mapDataToForms($car, array($form));
+
+ $this->assertNull($form->getData());
+ }
+
+ public function testMapDataToFormsIgnoresUnmapped()
+ {
+ $car = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('getValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setMapped(false);
+ $config->setPropertyPath($propertyPath);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapDataToForms($car, array($form));
+
+ $this->assertNull($form->getData());
+ }
+
+ public function testMapDataToFormsIgnoresEmptyData()
+ {
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('getValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapDataToForms(null, array($form));
+
+ $this->assertNull($form->getData());
+ }
+
+ public function testMapFormsToDataWritesBackIfNotByReference()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->once())
+ ->method('setValue')
+ ->with($car, $propertyPath, $engine);
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(false);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataWritesBackIfByReferenceButNoReference()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->once())
+ ->method('setValue')
+ ->with($car, $propertyPath, $engine);
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataWritesBackIfByReferenceAndReference()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ // $car already contains the reference of $engine
+ $this->propertyAccessor->expects($this->once())
+ ->method('getValue')
+ ->with($car, $propertyPath)
+ ->will($this->returnValue($engine));
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('setValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataIgnoresUnmapped()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('setValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $config->setMapped(false);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataIgnoresEmptyData()
+ {
+ $car = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('setValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData(null);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataIgnoresUnsynchronized()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('setValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $form = $this->getForm($config, false);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+
+ public function testMapFormsToDataIgnoresDisabled()
+ {
+ $car = new \stdClass();
+ $engine = new \stdClass();
+ $propertyPath = $this->getPropertyPath('engine');
+
+ $this->propertyAccessor->expects($this->never())
+ ->method('setValue');
+
+ $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher);
+ $config->setByReference(true);
+ $config->setPropertyPath($propertyPath);
+ $config->setData($engine);
+ $config->setDisabled(true);
+ $form = $this->getForm($config);
+
+ $this->mapper->mapFormsToData(array($form), $car);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
+
+class ArrayToPartsTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ private $transformer;
+
+ protected function setUp()
+ {
+ $this->transformer = new ArrayToPartsTransformer(array(
+ 'first' => array('a', 'b', 'c'),
+ 'second' => array('d', 'e', 'f'),
+ ));
+ }
+
+ protected function tearDown()
+ {
+ $this->transformer = null;
+ }
+
+ public function testTransform()
+ {
+ $input = array(
+ 'a' => '1',
+ 'b' => '2',
+ 'c' => '3',
+ 'd' => '4',
+ 'e' => '5',
+ 'f' => '6',
+ );
+
+ $output = array(
+ 'first' => array(
+ 'a' => '1',
+ 'b' => '2',
+ 'c' => '3',
+ ),
+ 'second' => array(
+ 'd' => '4',
+ 'e' => '5',
+ 'f' => '6',
+ ),
+ );
+
+ $this->assertSame($output, $this->transformer->transform($input));
+ }
+
+ public function testTransformEmpty()
+ {
+ $output = array(
+ 'first' => null,
+ 'second' => null,
+ );
+
+ $this->assertSame($output, $this->transformer->transform(null));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformRequiresArray()
+ {
+ $this->transformer->transform('12345');
+ }
+
+ public function testReverseTransform()
+ {
+ $input = array(
+ 'first' => array(
+ 'a' => '1',
+ 'b' => '2',
+ 'c' => '3',
+ ),
+ 'second' => array(
+ 'd' => '4',
+ 'e' => '5',
+ 'f' => '6',
+ ),
+ );
+
+ $output = array(
+ 'a' => '1',
+ 'b' => '2',
+ 'c' => '3',
+ 'd' => '4',
+ 'e' => '5',
+ 'f' => '6',
+ );
+
+ $this->assertSame($output, $this->transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyEmpty()
+ {
+ $input = array(
+ 'first' => '',
+ 'second' => '',
+ );
+
+ $this->assertNull($this->transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyNull()
+ {
+ $input = array(
+ 'first' => null,
+ 'second' => null,
+ );
+
+ $this->assertNull($this->transformer->reverseTransform($input));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyNull()
+ {
+ $input = array(
+ 'first' => array(
+ 'a' => '1',
+ 'b' => '2',
+ 'c' => '3',
+ ),
+ 'second' => null,
+ );
+
+ $this->transformer->reverseTransform($input);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformRequiresArray()
+ {
+ $this->transformer->reverseTransform('12345');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\BooleanToStringTransformer;
+
+class BooleanToStringTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ const TRUE_VALUE = '1';
+
+ protected $transformer;
+
+ protected function setUp()
+ {
+ $this->transformer = new BooleanToStringTransformer(self::TRUE_VALUE);
+ }
+
+ protected function tearDown()
+ {
+ $this->transformer = null;
+ }
+
+ public function testTransform()
+ {
+ $this->assertEquals(self::TRUE_VALUE, $this->transformer->transform(true));
+ $this->assertNull($this->transformer->transform(false));
+ $this->assertNull($this->transformer->transform(null));
+ }
+
+ public function testTransformExpectsBoolean()
+ {
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $this->transformer->transform('1');
+ }
+
+ public function testReverseTransformExpectsString()
+ {
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $this->transformer->reverseTransform(1);
+ }
+
+ public function testReverseTransform()
+ {
+ $this->assertTrue($this->transformer->reverseTransform(self::TRUE_VALUE));
+ $this->assertTrue($this->transformer->reverseTransform('foobar'));
+ $this->assertTrue($this->transformer->reverseTransform(''));
+ $this->assertFalse($this->transformer->reverseTransform(null));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer;
+
+class ChoiceToValueTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected $transformer;
+
+ protected function setUp()
+ {
+ $list = new SimpleChoiceList(array('' => 'A', 0 => 'B', 1 => 'C'));
+ $this->transformer = new ChoiceToValueTransformer($list);
+ }
+
+ protected function tearDown()
+ {
+ $this->transformer = null;
+ }
+
+ public function transformProvider()
+ {
+ return array(
+ // more extensive test set can be found in FormUtilTest
+ array(0, '0'),
+ array(false, '0'),
+ array('', ''),
+ );
+ }
+
+ /**
+ * @dataProvider transformProvider
+ */
+ public function testTransform($in, $out)
+ {
+ $this->assertSame($out, $this->transformer->transform($in));
+ }
+
+ public function reverseTransformProvider()
+ {
+ return array(
+ // values are expected to be valid choice keys already and stay
+ // the same
+ array('0', 0),
+ array('', null),
+ array(null, null),
+ );
+ }
+
+ /**
+ * @dataProvider reverseTransformProvider
+ */
+ public function testReverseTransform($in, $out)
+ {
+ $this->assertSame($out, $this->transformer->reverseTransform($in));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsScalar()
+ {
+ $this->transformer->reverseTransform(array());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer;
+
+class ChoicesToValuesTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected $transformer;
+
+ protected function setUp()
+ {
+ $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B', 2 => 'C'));
+ $this->transformer = new ChoicesToValuesTransformer($list);
+ }
+
+ protected function tearDown()
+ {
+ $this->transformer = null;
+ }
+
+ public function testTransform()
+ {
+ // Value strategy in SimpleChoiceList is to copy and convert to string
+ $in = array(0, 1, 2);
+ $out = array('0', '1', '2');
+
+ $this->assertSame($out, $this->transformer->transform($in));
+ }
+
+ public function testTransformNull()
+ {
+ $this->assertSame(array(), $this->transformer->transform(null));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformExpectsArray()
+ {
+ $this->transformer->transform('foobar');
+ }
+
+ public function testReverseTransform()
+ {
+ // values are expected to be valid choices and stay the same
+ $in = array('0', '1', '2');
+ $out = array(0, 1, 2);
+
+ $this->assertSame($out, $this->transformer->reverseTransform($in));
+ }
+
+ public function testReverseTransformNull()
+ {
+ $this->assertSame(array(), $this->transformer->reverseTransform(null));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsArray()
+ {
+ $this->transformer->reverseTransform('foobar');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
+
+class DataTransformerChainTest extends \PHPUnit_Framework_TestCase
+{
+ public function testTransform()
+ {
+ $transformer1 = $this->getMock('Symfony\Component\Form\DataTransformerInterface');
+ $transformer1->expects($this->once())
+ ->method('transform')
+ ->with($this->identicalTo('foo'))
+ ->will($this->returnValue('bar'));
+ $transformer2 = $this->getMock('Symfony\Component\Form\DataTransformerInterface');
+ $transformer2->expects($this->once())
+ ->method('transform')
+ ->with($this->identicalTo('bar'))
+ ->will($this->returnValue('baz'));
+
+ $chain = new DataTransformerChain(array($transformer1, $transformer2));
+
+ $this->assertEquals('baz', $chain->transform('foo'));
+ }
+
+ public function testReverseTransform()
+ {
+ $transformer2 = $this->getMock('Symfony\Component\Form\DataTransformerInterface');
+ $transformer2->expects($this->once())
+ ->method('reverseTransform')
+ ->with($this->identicalTo('foo'))
+ ->will($this->returnValue('bar'));
+ $transformer1 = $this->getMock('Symfony\Component\Form\DataTransformerInterface');
+ $transformer1->expects($this->once())
+ ->method('reverseTransform')
+ ->with($this->identicalTo('bar'))
+ ->will($this->returnValue('baz'));
+
+ $chain = new DataTransformerChain(array($transformer1, $transformer2));
+
+ $this->assertEquals('baz', $chain->reverseTransform('foo'));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+abstract class DateTimeTestCase extends \PHPUnit_Framework_TestCase
+{
+ public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual)
+ {
+ self::assertEquals($expected->format('c'), $actual->format('c'));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
+
+class DateTimeToArrayTransformerTest extends DateTimeTestCase
+{
+ public function testTransform()
+ {
+ $transformer = new DateTimeToArrayTransformer('UTC', 'UTC');
+
+ $input = new \DateTime('2010-02-03 04:05:06 UTC');
+
+ $output = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ );
+
+ $this->assertSame($output, $transformer->transform($input));
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+
+ $output = array(
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ 'hour' => '',
+ 'minute' => '',
+ 'second' => '',
+ );
+
+ $this->assertSame($output, $transformer->transform(null));
+ }
+
+ public function testTransformEmptyWithFields()
+ {
+ $transformer = new DateTimeToArrayTransformer(null, null, array('year', 'minute', 'second'));
+
+ $output = array(
+ 'year' => '',
+ 'minute' => '',
+ 'second' => '',
+ );
+
+ $this->assertSame($output, $transformer->transform(null));
+ }
+
+ public function testTransformWithFields()
+ {
+ $transformer = new DateTimeToArrayTransformer('UTC', 'UTC', array('year', 'month', 'minute', 'second'));
+
+ $input = new \DateTime('2010-02-03 04:05:06 UTC');
+
+ $output = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'minute' => '5',
+ 'second' => '6',
+ );
+
+ $this->assertSame($output, $transformer->transform($input));
+ }
+
+ public function testTransformWithPadding()
+ {
+ $transformer = new DateTimeToArrayTransformer('UTC', 'UTC', null, true);
+
+ $input = new \DateTime('2010-02-03 04:05:06 UTC');
+
+ $output = array(
+ 'year' => '2010',
+ 'month' => '02',
+ 'day' => '03',
+ 'hour' => '04',
+ 'minute' => '05',
+ 'second' => '06',
+ );
+
+ $this->assertSame($output, $transformer->transform($input));
+ }
+
+ public function testTransformDifferentTimezones()
+ {
+ $transformer = new DateTimeToArrayTransformer('America/New_York', 'Asia/Hong_Kong');
+
+ $input = new \DateTime('2010-02-03 04:05:06 America/New_York');
+
+ $dateTime = new \DateTime('2010-02-03 04:05:06 America/New_York');
+ $dateTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+ $output = array(
+ 'year' => (string) (int) $dateTime->format('Y'),
+ 'month' => (string) (int) $dateTime->format('m'),
+ 'day' => (string) (int) $dateTime->format('d'),
+ 'hour' => (string) (int) $dateTime->format('H'),
+ 'minute' => (string) (int) $dateTime->format('i'),
+ 'second' => (string) (int) $dateTime->format('s'),
+ );
+
+ $this->assertSame($output, $transformer->transform($input));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformRequiresDateTime()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform('12345');
+ }
+
+ public function testReverseTransform()
+ {
+ $transformer = new DateTimeToArrayTransformer('UTC', 'UTC');
+
+ $input = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ );
+
+ $output = new \DateTime('2010-02-03 04:05:06 UTC');
+
+ $this->assertDateTimeEquals($output, $transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformWithSomeZero()
+ {
+ $transformer = new DateTimeToArrayTransformer('UTC', 'UTC');
+
+ $input = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '0',
+ 'second' => '0',
+ );
+
+ $output = new \DateTime('2010-02-03 04:00:00 UTC');
+
+ $this->assertDateTimeEquals($output, $transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyEmpty()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+
+ $input = array(
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ 'hour' => '',
+ 'minute' => '',
+ 'second' => '',
+ );
+
+ $this->assertNull($transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyEmptySubsetOfFields()
+ {
+ $transformer = new DateTimeToArrayTransformer(null, null, array('year', 'month', 'day'));
+
+ $input = array(
+ 'year' => '',
+ 'month' => '',
+ 'day' => '',
+ );
+
+ $this->assertNull($transformer->reverseTransform($input));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptyYear()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptyMonth()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptyDay()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptyHour()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptyMinute()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyEmptySecond()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ ));
+ }
+
+ public function testReverseTransformNull()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+
+ $this->assertNull($transformer->reverseTransform(null));
+ }
+
+ public function testReverseTransformDifferentTimezones()
+ {
+ $transformer = new DateTimeToArrayTransformer('America/New_York', 'Asia/Hong_Kong');
+
+ $input = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ );
+
+ $output = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong');
+ $output->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $this->assertDateTimeEquals($output, $transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformToDifferentTimezone()
+ {
+ $transformer = new DateTimeToArrayTransformer('Asia/Hong_Kong', 'UTC');
+
+ $input = array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ );
+
+ $output = new \DateTime('2010-02-03 04:05:06 UTC');
+ $output->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $this->assertDateTimeEquals($output, $transformer->reverseTransform($input));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformRequiresArray()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform('12345');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeYear()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '-1',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeMonth()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '-1',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeDay()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '-1',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeHour()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '-1',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeMinute()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '-1',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNegativeSecond()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '-1',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithInvalidMonth()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '13',
+ 'day' => '3',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithInvalidDay()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => '31',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithStringDay()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => '2',
+ 'day' => 'bazinga',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithStringMonth()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => '2010',
+ 'month' => 'bazinga',
+ 'day' => '31',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithStringYear()
+ {
+ $transformer = new DateTimeToArrayTransformer();
+ $transformer->reverseTransform(array(
+ 'year' => 'bazinga',
+ 'month' => '2',
+ 'day' => '31',
+ 'hour' => '4',
+ 'minute' => '5',
+ 'second' => '6',
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class DateTimeToLocalizedStringTransformerTest extends DateTimeTestCase
+{
+ protected $dateTime;
+ protected $dateTimeWithoutSeconds;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Since we test against "de_AT", we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+
+ $this->dateTime = new \DateTime('2010-02-03 04:05:06 UTC');
+ $this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC');
+ }
+
+ protected function tearDown()
+ {
+ $this->dateTime = null;
+ $this->dateTimeWithoutSeconds = null;
+ }
+
+ public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false)
+ {
+ if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
+ $expected = $expected->format('c');
+ $actual = $actual->format('c');
+ }
+
+ parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
+ }
+
+ public function dataProvider()
+ {
+ return array(
+ array(\IntlDateFormatter::SHORT, null, null, '03.02.10 04:05', '2010-02-03 04:05:00 UTC'),
+ array(\IntlDateFormatter::MEDIUM, null, null, '03.02.2010 04:05', '2010-02-03 04:05:00 UTC'),
+ array(\IntlDateFormatter::LONG, null, null, '03. Februar 2010 04:05', '2010-02-03 04:05:00 UTC'),
+ array(\IntlDateFormatter::FULL, null, null, 'Mittwoch, 03. Februar 2010 04:05', '2010-02-03 04:05:00 UTC'),
+ array(\IntlDateFormatter::SHORT, \IntlDateFormatter::NONE, null, '03.02.10', '2010-02-03 00:00:00 UTC'),
+ array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, null, '03.02.2010', '2010-02-03 00:00:00 UTC'),
+ array(\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, null, '03. Februar 2010', '2010-02-03 00:00:00 UTC'),
+ array(\IntlDateFormatter::FULL, \IntlDateFormatter::NONE, null, 'Mittwoch, 03. Februar 2010', '2010-02-03 00:00:00 UTC'),
+ array(null, \IntlDateFormatter::SHORT, null, '03.02.2010 04:05', '2010-02-03 04:05:00 UTC'),
+ array(null, \IntlDateFormatter::MEDIUM, null, '03.02.2010 04:05:06', '2010-02-03 04:05:06 UTC'),
+ array(null, \IntlDateFormatter::LONG, null, '03.02.2010 04:05:06 GMT', '2010-02-03 04:05:06 UTC'),
+ // see below for extra test case for time format FULL
+ array(\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, null, '04:05', '1970-01-01 04:05:00 UTC'),
+ array(\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, null, '04:05:06', '1970-01-01 04:05:06 UTC'),
+ array(\IntlDateFormatter::NONE, \IntlDateFormatter::LONG, null, '04:05:06 GMT', '1970-01-01 04:05:06 UTC'),
+ array(null, null, 'yyyy-MM-dd HH:mm:00', '2010-02-03 04:05:00', '2010-02-03 04:05:00 UTC'),
+ array(null, null, 'yyyy-MM-dd HH:mm', '2010-02-03 04:05', '2010-02-03 04:05:00 UTC'),
+ array(null, null, 'yyyy-MM-dd HH', '2010-02-03 04', '2010-02-03 04:00:00 UTC'),
+ array(null, null, 'yyyy-MM-dd', '2010-02-03', '2010-02-03 00:00:00 UTC'),
+ array(null, null, 'yyyy-MM', '2010-02', '2010-02-01 00:00:00 UTC'),
+ array(null, null, 'yyyy', '2010', '2010-01-01 00:00:00 UTC'),
+ array(null, null, 'dd-MM-yyyy', '03-02-2010', '2010-02-03 00:00:00 UTC'),
+ array(null, null, 'HH:mm:ss', '04:05:06', '1970-01-01 04:05:06 UTC'),
+ array(null, null, 'HH:mm:00', '04:05:00', '1970-01-01 04:05:00 UTC'),
+ array(null, null, 'HH:mm', '04:05', '1970-01-01 04:05:00 UTC'),
+ array(null, null, 'HH', '04', '1970-01-01 04:00:00 UTC'),
+ );
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testTransform($dateFormat, $timeFormat, $pattern, $output, $input)
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer(
+ 'UTC',
+ 'UTC',
+ $dateFormat,
+ $timeFormat,
+ \IntlDateFormatter::GREGORIAN,
+ $pattern
+ );
+
+ $input = new \DateTime($input);
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformFullTime()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::FULL);
+
+ $this->assertEquals('03.02.2010 04:05:06 GMT', $transformer->transform($this->dateTime));
+ }
+
+ public function testTransformToDifferentLocale()
+ {
+ \Locale::setDefault('en_US');
+
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC');
+
+ $this->assertEquals('Feb 3, 2010, 4:05 AM', $transformer->transform($this->dateTime));
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+
+ $this->assertSame('', $transformer->transform(null));
+ }
+
+ public function testTransformWithDifferentTimezones()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('America/New_York', 'Asia/Hong_Kong');
+
+ $input = new \DateTime('2010-02-03 04:05:06 America/New_York');
+
+ $dateTime = clone $input;
+ $dateTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $this->assertEquals($dateTime->format('d.m.Y H:i'), $transformer->transform($input));
+ }
+
+ public function testTransformWithDifferentPatterns()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, \IntlDateFormatter::GREGORIAN, 'MM*yyyy*dd HH|mm|ss');
+
+ $this->assertEquals('02*2010*03 04|05|06', $transformer->transform($this->dateTime));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformRequiresValidDateTime()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+ $transformer->transform('2010-01-01');
+ }
+
+ public function testTransformWrapsIntlErrors()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+
+ // HOW TO REPRODUCE?
+
+ //$this->setExpectedException('Symfony\Component\Form\Extension\Core\DataTransformer\Transdate_formationFailedException');
+
+ //$transformer->transform(1.5);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testReverseTransform($dateFormat, $timeFormat, $pattern, $input, $output)
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer(
+ 'UTC',
+ 'UTC',
+ $dateFormat,
+ $timeFormat,
+ \IntlDateFormatter::GREGORIAN,
+ $pattern
+ );
+
+ $output = new \DateTime($output);
+
+ $this->assertEquals($output, $transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformFullTime()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, \IntlDateFormatter::FULL);
+
+ $this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06 GMT+00:00'));
+ }
+
+ public function testReverseTransformFromDifferentLocale()
+ {
+ \Locale::setDefault('en_US');
+
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC');
+
+ $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Feb 3, 2010, 04:05 AM'));
+ }
+
+ public function testReverseTransformWithDifferentTimezones()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('America/New_York', 'Asia/Hong_Kong');
+
+ $dateTime = new \DateTime('2010-02-03 04:05:00 Asia/Hong_Kong');
+ $dateTime->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $this->assertDateTimeEquals($dateTime, $transformer->reverseTransform('03.02.2010 04:05'));
+ }
+
+ public function testReverseTransformWithDifferentPatterns()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::FULL, \IntlDateFormatter::FULL, \IntlDateFormatter::GREGORIAN, 'MM*yyyy*dd HH|mm|ss');
+
+ $this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('02*2010*03 04|05|06'));
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+
+ $this->assertNull($transformer->reverseTransform(''));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformRequiresString()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+ $transformer->reverseTransform(12345);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWrapsIntlErrors()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer();
+ $transformer->reverseTransform('12345');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testValidateDateFormatOption()
+ {
+ new DateTimeToLocalizedStringTransformer(null, null, 'foobar');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testValidateTimeFormatOption()
+ {
+ new DateTimeToLocalizedStringTransformer(null, null, null, 'foobar');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNonExistingDate()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', \IntlDateFormatter::SHORT);
+
+ $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('31.04.10 04:05'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformOutOfTimestampRange()
+ {
+ $transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC');
+ $transformer->reverseTransform('1789-07-14');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToRfc3339Transformer;
+
+class DateTimeToRfc3339TransformerTest extends DateTimeTestCase
+{
+ protected $dateTime;
+ protected $dateTimeWithoutSeconds;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->dateTime = new \DateTime('2010-02-03 04:05:06 UTC');
+ $this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC');
+ }
+
+ protected function tearDown()
+ {
+ $this->dateTime = null;
+ $this->dateTimeWithoutSeconds = null;
+ }
+
+ public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE)
+ {
+ if ($expected instanceof \DateTime && $actual instanceof \DateTime) {
+ $expected = $expected->format('c');
+ $actual = $actual->format('c');
+ }
+
+ parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
+ }
+
+ public function allProvider()
+ {
+ return array(
+ array('UTC', 'UTC', '2010-02-03 04:05:06 UTC', '2010-02-03T04:05:06Z'),
+ array('UTC', 'UTC', null, ''),
+ array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:06 America/New_York', '2010-02-03T17:05:06+08:00'),
+ array('America/New_York', 'Asia/Hong_Kong', null, ''),
+ array('UTC', 'Asia/Hong_Kong', '2010-02-03 04:05:06 UTC', '2010-02-03T12:05:06+08:00'),
+ array('America/New_York', 'UTC', '2010-02-03 04:05:06 America/New_York', '2010-02-03T09:05:06Z'),
+ );
+ }
+
+ public function transformProvider()
+ {
+ return $this->allProvider();
+ }
+
+ public function reverseTransformProvider()
+ {
+ return array_merge($this->allProvider(), array(
+ // format without seconds, as appears in some browsers
+ array('UTC', 'UTC', '2010-02-03 04:05:00 UTC', '2010-02-03T04:05Z'),
+ array('America/New_York', 'Asia/Hong_Kong', '2010-02-03 04:05:00 America/New_York', '2010-02-03T17:05+08:00'),
+ ));
+ }
+
+ /**
+ * @dataProvider transformProvider
+ */
+ public function testTransform($fromTz, $toTz, $from, $to)
+ {
+ $transformer = new DateTimeToRfc3339Transformer($fromTz, $toTz);
+
+ $this->assertSame($to, $transformer->transform(null !== $from ? new \DateTime($from) : null));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformRequiresValidDateTime()
+ {
+ $transformer = new DateTimeToRfc3339Transformer();
+ $transformer->transform('2010-01-01');
+ }
+
+ /**
+ * @dataProvider reverseTransformProvider
+ */
+ public function testReverseTransform($toTz, $fromTz, $to, $from)
+ {
+ $transformer = new DateTimeToRfc3339Transformer($toTz, $fromTz);
+
+ if (null !== $to) {
+ $this->assertDateTimeEquals(new \DateTime($to), $transformer->reverseTransform($from));
+ } else {
+ $this->assertSame($to, $transformer->reverseTransform($from));
+ }
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformRequiresString()
+ {
+ $transformer = new DateTimeToRfc3339Transformer();
+ $transformer->reverseTransform(12345);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformWithNonExistingDate()
+ {
+ $transformer = new DateTimeToRfc3339Transformer('UTC', 'UTC');
+
+ $transformer->reverseTransform('2010-04-31T04:05Z');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsValidDateString()
+ {
+ $transformer = new DateTimeToRfc3339Transformer('UTC', 'UTC');
+
+ $transformer->reverseTransform('2010-2010-2010');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
+
+class DateTimeToStringTransformerTest extends DateTimeTestCase
+{
+ public function dataProvider()
+ {
+ $data = array(
+ array('Y-m-d H:i:s', '2010-02-03 16:05:06', '2010-02-03 16:05:06 UTC'),
+ array('Y-m-d H:i:00', '2010-02-03 16:05:00', '2010-02-03 16:05:00 UTC'),
+ array('Y-m-d H:i', '2010-02-03 16:05', '2010-02-03 16:05:00 UTC'),
+ array('Y-m-d H', '2010-02-03 16', '2010-02-03 16:00:00 UTC'),
+ array('Y-m-d', '2010-02-03', '2010-02-03 00:00:00 UTC'),
+ array('Y-m', '2010-12', '2010-12-01 00:00:00 UTC'),
+ array('Y', '2010', '2010-01-01 00:00:00 UTC'),
+ array('d-m-Y', '03-02-2010', '2010-02-03 00:00:00 UTC'),
+ array('H:i:s', '16:05:06', '1970-01-01 16:05:06 UTC'),
+ array('H:i:00', '16:05:00', '1970-01-01 16:05:00 UTC'),
+ array('H:i', '16:05', '1970-01-01 16:05:00 UTC'),
+ array('H', '16', '1970-01-01 16:00:00 UTC'),
+
+ // different day representations
+ array('Y-m-j', '2010-02-3', '2010-02-03 00:00:00 UTC'),
+ array('z', '33', '1970-02-03 00:00:00 UTC'),
+
+ // not bijective
+ // this will not work as php will use actual date to replace missing info
+ // and after change of date will lookup for closest Wednesday
+ // i.e. value: 2010-02, php value: 2010-02-(today i.e. 20), parsed date: 2010-02-24
+ //array('Y-m-D', '2010-02-Wed', '2010-02-03 00:00:00 UTC'),
+ //array('Y-m-l', '2010-02-Wednesday', '2010-02-03 00:00:00 UTC'),
+
+ // different month representations
+ array('Y-n-d', '2010-2-03', '2010-02-03 00:00:00 UTC'),
+ array('Y-M-d', '2010-Feb-03', '2010-02-03 00:00:00 UTC'),
+ array('Y-F-d', '2010-February-03', '2010-02-03 00:00:00 UTC'),
+
+ // different year representations
+ array('y-m-d', '10-02-03', '2010-02-03 00:00:00 UTC'),
+
+ // different time representations
+ array('G:i:s', '16:05:06', '1970-01-01 16:05:06 UTC'),
+ array('g:i:s a', '4:05:06 pm', '1970-01-01 16:05:06 UTC'),
+ array('h:i:s a', '04:05:06 pm', '1970-01-01 16:05:06 UTC'),
+
+ // seconds since unix
+ array('U', '1265213106', '2010-02-03 16:05:06 UTC'),
+ );
+
+ // This test will fail < 5.3.9 - see https://bugs.php.net/51994
+ if (version_compare(phpversion(), '5.3.9', '>=')) {
+ $data[] = array('Y-z', '2010-33', '2010-02-03 00:00:00 UTC');
+ }
+
+ return $data;
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testTransform($format, $output, $input)
+ {
+ $transformer = new DateTimeToStringTransformer('UTC', 'UTC', $format);
+
+ $input = new \DateTime($input);
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new DateTimeToStringTransformer();
+
+ $this->assertSame('', $transformer->transform(null));
+ }
+
+ public function testTransformWithDifferentTimezones()
+ {
+ $transformer = new DateTimeToStringTransformer('Asia/Hong_Kong', 'America/New_York', 'Y-m-d H:i:s');
+
+ $input = new \DateTime('2010-02-03 12:05:06 America/New_York');
+ $output = $input->format('Y-m-d H:i:s');
+ $input->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformExpectsDateTime()
+ {
+ $transformer = new DateTimeToStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->transform('1234');
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testReverseTransformUsingPipe($format, $input, $output)
+ {
+ if (version_compare(phpversion(), '5.3.7', '<')) {
+ $this->markTestSkipped('Pipe usage requires PHP 5.3.7 or newer.');
+ }
+
+ $reverseTransformer = new DateTimeToStringTransformer('UTC', 'UTC', $format, true);
+
+ $output = new \DateTime($output);
+
+ $this->assertDateTimeEquals($output, $reverseTransformer->reverseTransform($input));
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testReverseTransformWithoutUsingPipe($format, $input, $output)
+ {
+ $reverseTransformer = new DateTimeToStringTransformer('UTC', 'UTC', $format, false);
+
+ $output = new \DateTime($output);
+
+ $this->assertDateTimeEquals($output, $reverseTransformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $reverseTransformer = new DateTimeToStringTransformer();
+
+ $this->assertNull($reverseTransformer->reverseTransform(''));
+ }
+
+ public function testReverseTransformWithDifferentTimezones()
+ {
+ $reverseTransformer = new DateTimeToStringTransformer('America/New_York', 'Asia/Hong_Kong', 'Y-m-d H:i:s');
+
+ $output = new \DateTime('2010-02-03 16:05:06 Asia/Hong_Kong');
+ $input = $output->format('Y-m-d H:i:s');
+ $output->setTimeZone(new \DateTimeZone('America/New_York'));
+
+ $this->assertDateTimeEquals($output, $reverseTransformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformExpectsString()
+ {
+ $reverseTransformer = new DateTimeToStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $reverseTransformer->reverseTransform(1234);
+ }
+
+ public function testReverseTransformExpectsValidDateString()
+ {
+ $reverseTransformer = new DateTimeToStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $reverseTransformer->reverseTransform('2010-2010-2010');
+ }
+
+ public function testReverseTransformWithNonExistingDate()
+ {
+ $reverseTransformer = new DateTimeToStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $reverseTransformer->reverseTransform('2010-04-31');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
+
+class DateTimeToTimestampTransformerTest extends DateTimeTestCase
+{
+ public function testTransform()
+ {
+ $transformer = new DateTimeToTimestampTransformer('UTC', 'UTC');
+
+ $input = new \DateTime('2010-02-03 04:05:06 UTC');
+ $output = $input->format('U');
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new DateTimeToTimestampTransformer();
+
+ $this->assertNull($transformer->transform(null));
+ }
+
+ public function testTransformWithDifferentTimezones()
+ {
+ $transformer = new DateTimeToTimestampTransformer('Asia/Hong_Kong', 'America/New_York');
+
+ $input = new \DateTime('2010-02-03 04:05:06 America/New_York');
+ $output = $input->format('U');
+ $input->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformFromDifferentTimezone()
+ {
+ $transformer = new DateTimeToTimestampTransformer('Asia/Hong_Kong', 'UTC');
+
+ $input = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong');
+
+ $dateTime = clone $input;
+ $dateTime->setTimezone(new \DateTimeZone('UTC'));
+ $output = $dateTime->format('U');
+
+ $this->assertEquals($output, $transformer->transform($input));
+ }
+
+ public function testTransformExpectsDateTime()
+ {
+ $transformer = new DateTimeToTimestampTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->transform('1234');
+ }
+
+ public function testReverseTransform()
+ {
+ $reverseTransformer = new DateTimeToTimestampTransformer('UTC', 'UTC');
+
+ $output = new \DateTime('2010-02-03 04:05:06 UTC');
+ $input = $output->format('U');
+
+ $this->assertDateTimeEquals($output, $reverseTransformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $reverseTransformer = new DateTimeToTimestampTransformer();
+
+ $this->assertNull($reverseTransformer->reverseTransform(null));
+ }
+
+ public function testReverseTransformWithDifferentTimezones()
+ {
+ $reverseTransformer = new DateTimeToTimestampTransformer('Asia/Hong_Kong', 'America/New_York');
+
+ $output = new \DateTime('2010-02-03 04:05:06 America/New_York');
+ $input = $output->format('U');
+ $output->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $this->assertDateTimeEquals($output, $reverseTransformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformExpectsValidTimestamp()
+ {
+ $reverseTransformer = new DateTimeToTimestampTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $reverseTransformer->reverseTransform('2010-2010-2010');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class IntegerToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Since we test against "de_AT", we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+ }
+
+ public function testReverseTransform()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $this->assertEquals(1, $transformer->reverseTransform('1'));
+ $this->assertEquals(1, $transformer->reverseTransform('1,5'));
+ $this->assertEquals(1234, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(12345, $transformer->reverseTransform('12345,912'));
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $this->assertNull($transformer->reverseTransform(''));
+ }
+
+ public function testReverseTransformWithGrouping()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer(null, true);
+
+ $this->assertEquals(1234, $transformer->reverseTransform('1.234,5'));
+ $this->assertEquals(12345, $transformer->reverseTransform('12.345,912'));
+ $this->assertEquals(1234, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(12345, $transformer->reverseTransform('12345,912'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsString()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform(1);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsValidNumber()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsNaN()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('NaN');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsNaN2()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('nan');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsInfinity()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('∞');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsNegativeInfinity()
+ {
+ $transformer = new IntegerToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('-∞');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class MoneyToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Since we test against "de_AT", we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+ }
+
+ public function testTransform()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->assertEquals('1,23', $transformer->transform(123));
+ }
+
+ public function testTransformExpectsNumeric()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->transform('abcd');
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer();
+
+ $this->assertSame('', $transformer->transform(null));
+ }
+
+ public function testReverseTransform()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->assertEquals(123, $transformer->reverseTransform('1,23'));
+ }
+
+ public function testReverseTransformExpectsString()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer(null, null, null, 100);
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->reverseTransform(12345);
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $transformer = new MoneyToLocalizedStringTransformer();
+
+ $this->assertNull($transformer->reverseTransform(''));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class NumberToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Since we test against "de_AT", we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+ }
+
+ public function provideTransformations()
+ {
+ return array(
+ array(null, '', 'de_AT'),
+ array(1, '1', 'de_AT'),
+ array(1.5, '1,5', 'de_AT'),
+ array(1234.5, '1234,5', 'de_AT'),
+ array(12345.912, '12345,912', 'de_AT'),
+ array(1234.5, '1234,5', 'ru'),
+ array(1234.5, '1234,5', 'fi'),
+ );
+ }
+
+ /**
+ * @dataProvider provideTransformations
+ */
+ public function testTransform($from, $to, $locale)
+ {
+ \Locale::setDefault($locale);
+
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $this->assertSame($to, $transformer->transform($from));
+ }
+
+ public function provideTransformationsWithGrouping()
+ {
+ return array(
+ array(1234.5, '1.234,5', 'de_AT'),
+ array(12345.912, '12.345,912', 'de_AT'),
+ array(1234.5, '1 234,5', 'fr'),
+ array(1234.5, '1 234,5', 'ru'),
+ array(1234.5, '1 234,5', 'fi'),
+ );
+ }
+
+ /**
+ * @dataProvider provideTransformationsWithGrouping
+ */
+ public function testTransformWithGrouping($from, $to, $locale)
+ {
+ \Locale::setDefault($locale);
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $this->assertSame($to, $transformer->transform($from));
+ }
+
+ public function testTransformWithPrecision()
+ {
+ $transformer = new NumberToLocalizedStringTransformer(2);
+
+ $this->assertEquals('1234,50', $transformer->transform(1234.5));
+ $this->assertEquals('678,92', $transformer->transform(678.916));
+ }
+
+ public function testTransformWithRoundingMode()
+ {
+ $transformer = new NumberToLocalizedStringTransformer(null, null, NumberToLocalizedStringTransformer::ROUND_DOWN);
+ $this->assertEquals('1234,547', $transformer->transform(1234.547), '->transform() only applies rounding mode if precision set');
+
+ $transformer = new NumberToLocalizedStringTransformer(2, null, NumberToLocalizedStringTransformer::ROUND_DOWN);
+ $this->assertEquals('1234,54', $transformer->transform(1234.547), '->transform() rounding-mode works');
+
+ }
+
+ /**
+ * @dataProvider provideTransformations
+ */
+ public function testReverseTransform($to, $from, $locale)
+ {
+ \Locale::setDefault($locale);
+
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $this->assertEquals($to, $transformer->reverseTransform($from));
+ }
+
+ /**
+ * @dataProvider provideTransformationsWithGrouping
+ */
+ public function testReverseTransformWithGrouping($to, $from, $locale)
+ {
+ \Locale::setDefault($locale);
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $this->assertEquals($to, $transformer->reverseTransform($from));
+ }
+
+ // https://github.com/symfony/symfony/issues/7609
+ public function testReverseTransformWithGroupingAndFixedSpaces()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('The "mbstring" extension is required for this test.');
+ }
+
+ \Locale::setDefault('ru');
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $this->assertEquals(1234.5, $transformer->reverseTransform("1\xc2\xa0234,5"));
+ }
+
+ public function testReverseTransformWithGroupingButWithoutGroupSeparator()
+ {
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ // omit group separator
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(12345.912, $transformer->reverseTransform('12345,912'));
+ }
+
+ public function testDecimalSeparatorMayBeDotIfGroupingSeparatorIsNotDot()
+ {
+ \Locale::setDefault('fr');
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ // completely valid format
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1 234,5'));
+ // accept dots
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1 234.5'));
+ // omit group separator
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234.5'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testDecimalSeparatorMayNotBeDotIfGroupingSeparatorIsDot()
+ {
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform('1.234.5');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testDecimalSeparatorMayNotBeDotIfGroupingSeparatorIsDotWithNoGroupSep()
+ {
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform('1234.5');
+ }
+
+ public function testDecimalSeparatorMayBeDotIfGroupingSeparatorIsDotButNoGroupingUsed()
+ {
+ \Locale::setDefault('fr');
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234.5'));
+ }
+
+ public function testDecimalSeparatorMayBeCommaIfGroupingSeparatorIsNotComma()
+ {
+ \Locale::setDefault('bg');
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ // completely valid format
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1 234.5'));
+ // accept commas
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1 234,5'));
+ // omit group separator
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234.5'));
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testDecimalSeparatorMayNotBeCommaIfGroupingSeparatorIsComma()
+ {
+ \Locale::setDefault('en');
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform('1,234,5');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testDecimalSeparatorMayNotBeCommaIfGroupingSeparatorIsCommaWithNoGroupSep()
+ {
+ \Locale::setDefault('en');
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform('1234,5');
+ }
+
+ public function testDecimalSeparatorMayBeCommaIfGroupingSeparatorIsCommaButNoGroupingUsed()
+ {
+ \Locale::setDefault('en');
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5'));
+ $this->assertEquals(1234.5, $transformer->reverseTransform('1234.5'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testTransformExpectsNumeric()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->transform('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsString()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform(1);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformExpectsValidNumber()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @link https://github.com/symfony/symfony/issues/3161
+ */
+ public function testReverseTransformDisallowsNaN()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('NaN');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsNaN2()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('nan');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsInfinity()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('∞');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsInfinity2()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('∞,123');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsNegativeInfinity()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('-∞');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDisallowsLeadingExtraCharacters()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('foo123');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @expectedExceptionMessage The number contains unrecognized characters: "foo3"
+ */
+ public function testReverseTransformDisallowsCenteredExtraCharacters()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('12foo3');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @expectedExceptionMessage The number contains unrecognized characters: "foo8"
+ */
+ public function testReverseTransformDisallowsCenteredExtraCharactersMultibyte()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('The "mbstring" extension is required for this test.');
+ }
+
+ \Locale::setDefault('ru');
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform("12\xc2\xa0345,67foo8");
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @expectedExceptionMessage The number contains unrecognized characters: "foo8"
+ */
+ public function testReverseTransformIgnoresTrailingSpacesInExceptionMessage()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('The "mbstring" extension is required for this test.');
+ }
+
+ \Locale::setDefault('ru');
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform("12\xc2\xa0345,67foo8 \xc2\xa0\t");
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @expectedExceptionMessage The number contains unrecognized characters: "foo"
+ */
+ public function testReverseTransformDisallowsTrailingExtraCharacters()
+ {
+ $transformer = new NumberToLocalizedStringTransformer();
+
+ $transformer->reverseTransform('123foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ * @expectedExceptionMessage The number contains unrecognized characters: "foo"
+ */
+ public function testReverseTransformDisallowsTrailingExtraCharactersMultibyte()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('The "mbstring" extension is required for this test.');
+ }
+
+ \Locale::setDefault('ru');
+
+ $transformer = new NumberToLocalizedStringTransformer(null, true);
+
+ $transformer->reverseTransform("12\xc2\xa0345,678foo");
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class PercentToLocalizedStringTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // Since we test against "de_AT", we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+ }
+
+ public function testTransform()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->assertEquals('10', $transformer->transform(0.1));
+ $this->assertEquals('15', $transformer->transform(0.15));
+ $this->assertEquals('12', $transformer->transform(0.1234));
+ $this->assertEquals('200', $transformer->transform(2));
+ }
+
+ public function testTransformEmpty()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->assertEquals('', $transformer->transform(null));
+ }
+
+ public function testTransformWithInteger()
+ {
+ $transformer = new PercentToLocalizedStringTransformer(null, 'integer');
+
+ $this->assertEquals('0', $transformer->transform(0.1));
+ $this->assertEquals('1', $transformer->transform(1));
+ $this->assertEquals('15', $transformer->transform(15));
+ $this->assertEquals('16', $transformer->transform(15.9));
+ }
+
+ public function testTransformWithPrecision()
+ {
+ $transformer = new PercentToLocalizedStringTransformer(2);
+
+ $this->assertEquals('12,34', $transformer->transform(0.1234));
+ }
+
+ public function testReverseTransform()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->assertEquals(0.1, $transformer->reverseTransform('10'));
+ $this->assertEquals(0.15, $transformer->reverseTransform('15'));
+ $this->assertEquals(0.12, $transformer->reverseTransform('12'));
+ $this->assertEquals(2, $transformer->reverseTransform('200'));
+ }
+
+ public function testReverseTransformEmpty()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->assertNull($transformer->reverseTransform(''));
+ }
+
+ public function testReverseTransformWithInteger()
+ {
+ $transformer = new PercentToLocalizedStringTransformer(null, 'integer');
+
+ $this->assertEquals(10, $transformer->reverseTransform('10'));
+ $this->assertEquals(15, $transformer->reverseTransform('15'));
+ $this->assertEquals(12, $transformer->reverseTransform('12'));
+ $this->assertEquals(200, $transformer->reverseTransform('200'));
+ }
+
+ public function testReverseTransformWithPrecision()
+ {
+ $transformer = new PercentToLocalizedStringTransformer(2);
+
+ $this->assertEquals(0.1234, $transformer->reverseTransform('12,34'));
+ }
+
+ public function testTransformExpectsNumeric()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->transform('foo');
+ }
+
+ public function testReverseTransformExpectsString()
+ {
+ $transformer = new PercentToLocalizedStringTransformer();
+
+ $this->setExpectedException('Symfony\Component\Form\Exception\TransformationFailedException');
+
+ $transformer->reverseTransform(1);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\DataTransformer;
+
+use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
+
+class ValueToDuplicatesTransformerTest extends \PHPUnit_Framework_TestCase
+{
+ private $transformer;
+
+ protected function setUp()
+ {
+ $this->transformer = new ValueToDuplicatesTransformer(array('a', 'b', 'c'));
+ }
+
+ protected function tearDown()
+ {
+ $this->transformer = null;
+ }
+
+ public function testTransform()
+ {
+ $output = array(
+ 'a' => 'Foo',
+ 'b' => 'Foo',
+ 'c' => 'Foo',
+ );
+
+ $this->assertSame($output, $this->transformer->transform('Foo'));
+ }
+
+ public function testTransformEmpty()
+ {
+ $output = array(
+ 'a' => null,
+ 'b' => null,
+ 'c' => null,
+ );
+
+ $this->assertSame($output, $this->transformer->transform(null));
+ }
+
+ public function testReverseTransform()
+ {
+ $input = array(
+ 'a' => 'Foo',
+ 'b' => 'Foo',
+ 'c' => 'Foo',
+ );
+
+ $this->assertSame('Foo', $this->transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyEmpty()
+ {
+ $input = array(
+ 'a' => '',
+ 'b' => '',
+ 'c' => '',
+ );
+
+ $this->assertNull($this->transformer->reverseTransform($input));
+ }
+
+ public function testReverseTransformCompletelyNull()
+ {
+ $input = array(
+ 'a' => null,
+ 'b' => null,
+ 'c' => null,
+ );
+
+ $this->assertNull($this->transformer->reverseTransform($input));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformPartiallyNull()
+ {
+ $input = array(
+ 'a' => 'Foo',
+ 'b' => 'Foo',
+ 'c' => null,
+ );
+
+ $this->transformer->reverseTransform($input);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformDifferences()
+ {
+ $input = array(
+ 'a' => 'Foo',
+ 'b' => 'Bar',
+ 'c' => 'Foo',
+ );
+
+ $this->transformer->reverseTransform($input);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException
+ */
+ public function testReverseTransformRequiresArray()
+ {
+ $this->transformer->reverseTransform('12345');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Core\EventListener\FixRadioInputListener;
+use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
+
+class FixRadioInputListenerTest extends \PHPUnit_Framework_TestCase
+{
+ private $choiceList;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ parent::setUp();
+
+ $this->choiceList = new SimpleChoiceList(array('' => 'Empty', 0 => 'A', 1 => 'B'));
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $listener = null;
+ }
+
+ public function testFixRadio()
+ {
+ $data = '1';
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $listener = new FixRadioInputListener($this->choiceList, true);
+ $listener->preSubmit($event);
+
+ // Indices in SimpleChoiceList are zero-based generated integers
+ $this->assertEquals(array(2 => '1'), $event->getData());
+ }
+
+ public function testFixZero()
+ {
+ $data = '0';
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $listener = new FixRadioInputListener($this->choiceList, true);
+ $listener->preSubmit($event);
+
+ // Indices in SimpleChoiceList are zero-based generated integers
+ $this->assertEquals(array(1 => '0'), $event->getData());
+ }
+
+ public function testFixEmptyString()
+ {
+ $data = '';
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $listener = new FixRadioInputListener($this->choiceList, true);
+ $listener->preSubmit($event);
+
+ // Indices in SimpleChoiceList are zero-based generated integers
+ $this->assertEquals(array(0 => ''), $event->getData());
+ }
+
+ public function testConvertEmptyStringToPlaceholderIfNotFound()
+ {
+ $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B'));
+
+ $data = '';
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $listener = new FixRadioInputListener($list, true);
+ $listener->preSubmit($event);
+
+ $this->assertEquals(array('placeholder' => ''), $event->getData());
+ }
+
+ public function testDontConvertEmptyStringToPlaceholderIfNoPlaceholderUsed()
+ {
+ $list = new SimpleChoiceList(array(0 => 'A', 1 => 'B'));
+
+ $data = '';
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $listener = new FixRadioInputListener($list, false);
+ $listener->preSubmit($event);
+
+ $this->assertEquals(array(), $event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener;
+
+class FixUrlProtocolListenerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+ }
+
+ public function testFixHttpUrl()
+ {
+ $data = "www.symfony.com";
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new FixUrlProtocolListener('http');
+ $filter->onSubmit($event);
+
+ $this->assertEquals('http://www.symfony.com', $event->getData());
+ }
+
+ public function testSkipKnownUrl()
+ {
+ $data = "http://www.symfony.com";
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new FixUrlProtocolListener('http');
+ $filter->onSubmit($event);
+
+ $this->assertEquals('http://www.symfony.com', $event->getData());
+ }
+
+ public function testSkipOtherProtocol()
+ {
+ $data = "ftp://www.symfony.com";
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new FixUrlProtocolListener('http');
+ $filter->onSubmit($event);
+
+ $this->assertEquals('ftp://www.symfony.com', $event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormBuilder;
+
+class MergeCollectionListenerArrayObjectTest extends MergeCollectionListenerTest
+{
+ protected function getData(array $data)
+ {
+ return new \ArrayObject($data);
+ }
+
+ protected function getBuilder($name = 'name')
+ {
+ return new FormBuilder($name, '\ArrayObject', $this->dispatcher, $this->factory);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormBuilder;
+
+class MergeCollectionListenerArrayTest extends MergeCollectionListenerTest
+{
+ protected function getData(array $data)
+ {
+ return $data;
+ }
+
+ protected function getBuilder($name = 'name')
+ {
+ return new FormBuilder($name, null, $this->dispatcher, $this->factory);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\Tests\Fixtures\CustomArrayObject;
+use Symfony\Component\Form\FormBuilder;
+
+class MergeCollectionListenerCustomArrayObjectTest extends MergeCollectionListenerTest
+{
+ protected function getData(array $data)
+ {
+ return new CustomArrayObject($data);
+ }
+
+ protected function getBuilder($name = 'name')
+ {
+ return new FormBuilder($name, 'Symfony\Component\Form\Tests\Fixtures\CustomArrayObject', $this->dispatcher, $this->factory);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Core\EventListener\MergeCollectionListener;
+
+abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase
+{
+ protected $dispatcher;
+ protected $factory;
+ protected $form;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->form = $this->getForm('axes');
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->factory = null;
+ $this->form = null;
+ }
+
+ abstract protected function getBuilder($name = 'name');
+
+ protected function getForm($name = 'name', $propertyPath = null)
+ {
+ $propertyPath = $propertyPath ?: $name;
+
+ return $this->getBuilder($name)->setAttribute('property_path', $propertyPath)->getForm();
+ }
+
+ protected function getMockForm()
+ {
+ return $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ }
+
+ public function getBooleanMatrix1()
+ {
+ return array(
+ array(true),
+ array(false),
+ );
+ }
+
+ public function getBooleanMatrix2()
+ {
+ return array(
+ array(true, true),
+ array(true, false),
+ array(false, true),
+ array(false, false),
+ );
+ }
+
+ abstract protected function getData(array $data);
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testAddExtraEntriesIfAllowAdd($allowDelete)
+ {
+ $originalData = $this->getData(array(1 => 'second'));
+ $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+
+ $listener = new MergeCollectionListener(true, $allowDelete);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ // The original object was modified
+ if (is_object($originalData)) {
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ // The original object matches the new object
+ $this->assertEquals($newData, $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testAddExtraEntriesIfAllowAddDontOverwriteExistingIndices($allowDelete)
+ {
+ $originalData = $this->getData(array(1 => 'first'));
+ $newData = $this->getData(array(0 => 'first', 1 => 'second'));
+
+ $listener = new MergeCollectionListener(true, $allowDelete);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ // The original object was modified
+ if (is_object($originalData)) {
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ // The original object matches the new object
+ $this->assertEquals($this->getData(array(1 => 'first', 2 => 'second')), $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testDoNothingIfNotAllowAdd($allowDelete)
+ {
+ $originalDataArray = array(1 => 'second');
+ $originalData = $this->getData($originalDataArray);
+ $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+
+ $listener = new MergeCollectionListener(false, $allowDelete);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ // We still have the original object
+ if (is_object($originalData)) {
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ // Nothing was removed
+ $this->assertEquals($this->getData($originalDataArray), $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testRemoveMissingEntriesIfAllowDelete($allowAdd)
+ {
+ $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+ $newData = $this->getData(array(1 => 'second'));
+
+ $listener = new MergeCollectionListener($allowAdd, true);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ // The original object was modified
+ if (is_object($originalData)) {
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ // The original object matches the new object
+ $this->assertEquals($newData, $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testDoNothingIfNotAllowDelete($allowAdd)
+ {
+ $originalDataArray = array(0 => 'first', 1 => 'second', 2 => 'third');
+ $originalData = $this->getData($originalDataArray);
+ $newData = $this->getData(array(1 => 'second'));
+
+ $listener = new MergeCollectionListener($allowAdd, false);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ // We still have the original object
+ if (is_object($originalData)) {
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ // Nothing was removed
+ $this->assertEquals($this->getData($originalDataArray), $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix2
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testRequireArrayOrTraversable($allowAdd, $allowDelete)
+ {
+ $newData = 'no array or traversable';
+ $event = new FormEvent($this->form, $newData);
+ $listener = new MergeCollectionListener($allowAdd, $allowDelete);
+ $listener->onSubmit($event);
+ }
+
+ public function testDealWithNullData()
+ {
+ $originalData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+ $newData = null;
+
+ $listener = new MergeCollectionListener(false, false);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ $this->assertSame($originalData, $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testDealWithNullOriginalDataIfAllowAdd($allowDelete)
+ {
+ $originalData = null;
+ $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+
+ $listener = new MergeCollectionListener(true, $allowDelete);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ $this->assertSame($newData, $event->getData());
+ }
+
+ /**
+ * @dataProvider getBooleanMatrix1
+ */
+ public function testDontDealWithNullOriginalDataIfNotAllowAdd($allowDelete)
+ {
+ $originalData = null;
+ $newData = $this->getData(array(0 => 'first', 1 => 'second', 2 => 'third'));
+
+ $listener = new MergeCollectionListener(false, $allowDelete);
+
+ $this->form->setData($originalData);
+
+ $event = new FormEvent($this->form, $newData);
+ $listener->onSubmit($event);
+
+ $this->assertNull($event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormEvent;
+
+class ResizeFormListenerTest extends \PHPUnit_Framework_TestCase
+{
+ private $dispatcher;
+ private $factory;
+ private $form;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->form = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->factory = null;
+ $this->form = null;
+ }
+
+ protected function getBuilder($name = 'name')
+ {
+ return new FormBuilder($name, null, $this->dispatcher, $this->factory);
+ }
+
+ protected function getForm($name = 'name')
+ {
+ return $this->getBuilder($name)->getForm();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ protected function getMockForm()
+ {
+ return $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ }
+
+ public function testPreSetDataResizesForm()
+ {
+ $this->form->add($this->getForm('0'));
+ $this->form->add($this->getForm('1'));
+
+ $this->factory->expects($this->at(0))
+ ->method('createNamed')
+ ->with(1, 'text', null, array('property_path' => '[1]', 'max_length' => 10, 'auto_initialize' => false))
+ ->will($this->returnValue($this->getForm('1')));
+ $this->factory->expects($this->at(1))
+ ->method('createNamed')
+ ->with(2, 'text', null, array('property_path' => '[2]', 'max_length' => 10, 'auto_initialize' => false))
+ ->will($this->returnValue($this->getForm('2')));
+
+ $data = array(1 => 'string', 2 => 'string');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array('max_length' => '10'), false, false);
+ $listener->preSetData($event);
+
+ $this->assertFalse($this->form->has('0'));
+ $this->assertTrue($this->form->has('1'));
+ $this->assertTrue($this->form->has('2'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testPreSetDataRequiresArrayOrTraversable()
+ {
+ $data = 'no array or traversable';
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->preSetData($event);
+ }
+
+ public function testPreSetDataDealsWithNullData()
+ {
+ $this->factory->expects($this->never())->method('createNamed');
+
+ $data = null;
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->preSetData($event);
+ }
+
+ public function testPreSubmitResizesUpIfAllowAdd()
+ {
+ $this->form->add($this->getForm('0'));
+
+ $this->factory->expects($this->once())
+ ->method('createNamed')
+ ->with(1, 'text', null, array('property_path' => '[1]', 'max_length' => 10, 'auto_initialize' => false))
+ ->will($this->returnValue($this->getForm('1')));
+
+ $data = array(0 => 'string', 1 => 'string');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array('max_length' => 10), true, false);
+ $listener->preSubmit($event);
+
+ $this->assertTrue($this->form->has('0'));
+ $this->assertTrue($this->form->has('1'));
+ }
+
+ public function testPreSubmitResizesDownIfAllowDelete()
+ {
+ $this->form->add($this->getForm('0'));
+ $this->form->add($this->getForm('1'));
+
+ $data = array(0 => 'string');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->preSubmit($event);
+
+ $this->assertTrue($this->form->has('0'));
+ $this->assertFalse($this->form->has('1'));
+ }
+
+ // fix for https://github.com/symfony/symfony/pull/493
+ public function testPreSubmitRemovesZeroKeys()
+ {
+ $this->form->add($this->getForm('0'));
+
+ $data = array();
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->preSubmit($event);
+
+ $this->assertFalse($this->form->has('0'));
+ }
+
+ public function testPreSubmitDoesNothingIfNotAllowAddNorAllowDelete()
+ {
+ $this->form->add($this->getForm('0'));
+ $this->form->add($this->getForm('1'));
+
+ $data = array(0 => 'string', 2 => 'string');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->preSubmit($event);
+
+ $this->assertTrue($this->form->has('0'));
+ $this->assertTrue($this->form->has('1'));
+ $this->assertFalse($this->form->has('2'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testPreSubmitRequiresArrayOrTraversable()
+ {
+ $data = 'no array or traversable';
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->preSubmit($event);
+ }
+
+ public function testPreSubmitDealsWithNullData()
+ {
+ $this->form->add($this->getForm('1'));
+
+ $data = null;
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->preSubmit($event);
+
+ $this->assertFalse($this->form->has('1'));
+ }
+
+ // fixes https://github.com/symfony/symfony/pull/40
+ public function testPreSubmitDealsWithEmptyData()
+ {
+ $this->form->add($this->getForm('1'));
+
+ $data = '';
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->preSubmit($event);
+
+ $this->assertFalse($this->form->has('1'));
+ }
+
+ public function testOnSubmitNormDataRemovesEntriesMissingInTheFormIfAllowDelete()
+ {
+ $this->form->add($this->getForm('1'));
+
+ $data = array(0 => 'first', 1 => 'second', 2 => 'third');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->onSubmit($event);
+
+ $this->assertEquals(array(1 => 'second'), $event->getData());
+ }
+
+ public function testOnSubmitNormDataDoesNothingIfNotAllowDelete()
+ {
+ $this->form->add($this->getForm('1'));
+
+ $data = array(0 => 'first', 1 => 'second', 2 => 'third');
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->onSubmit($event);
+
+ $this->assertEquals($data, $event->getData());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testOnSubmitNormDataRequiresArrayOrTraversable()
+ {
+ $data = 'no array or traversable';
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, false);
+ $listener->onSubmit($event);
+ }
+
+ public function testOnSubmitNormDataDealsWithNullData()
+ {
+ $this->form->add($this->getForm('1'));
+
+ $data = null;
+ $event = new FormEvent($this->form, $data);
+ $listener = new ResizeFormListener('text', array(), false, true);
+ $listener->onSubmit($event);
+
+ $this->assertEquals(array(), $event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\EventListener;
+
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Core\EventListener\TrimListener;
+
+class TrimListenerTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+ }
+
+ public function testTrim()
+ {
+ $data = " Foo! ";
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new TrimListener();
+ $filter->preSubmit($event);
+
+ $this->assertEquals('Foo!', $event->getData());
+ }
+
+ public function testTrimSkipNonStrings()
+ {
+ $data = 1234;
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new TrimListener();
+ $filter->preSubmit($event);
+
+ $this->assertSame(1234, $event->getData());
+ }
+
+ /**
+ * @dataProvider codePointProvider
+ */
+ public function testTrimUtf8($chars)
+ {
+ if (!function_exists('mb_check_encoding')) {
+ $this->markTestSkipped('The "mb_check_encoding" function is not available');
+ }
+
+ $data = mb_convert_encoding(pack('H*', implode('', $chars)), 'UTF-8', 'UCS-2BE');
+ $data = $data."ab\ncd".$data;
+
+ $form = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $event = new FormEvent($form, $data);
+
+ $filter = new TrimListener();
+ $filter->preSubmit($event);
+
+ $this->assertSame("ab\ncd", $event->getData(), 'TrimListener should trim character(s): '.implode(', ', $chars));
+ }
+
+ public function codePointProvider()
+ {
+ return array(
+ 'General category: Separator' => array(array('0020', '00A0', '1680', '180E', '2000', '2001', '2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '200A', '2028', '2029', '202F', '205F', '3000')),
+ 'General category: Other, control' => array(array('0009', '000A', '000B', '000C', '000D', '0085')),
+ //'General category: Other, format. ZERO WIDTH SPACE' => array(array('200B')),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class BaseTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ public function testPassDisabledAsOption()
+ {
+ $form = $this->factory->create($this->getTestedType(), null, array('disabled' => true));
+
+ $this->assertTrue($form->isDisabled());
+ }
+
+ public function testPassIdAndNameToView()
+ {
+ $view = $this->factory->createNamed('name', $this->getTestedType())
+ ->createView();
+
+ $this->assertEquals('name', $view->vars['id']);
+ $this->assertEquals('name', $view->vars['name']);
+ $this->assertEquals('name', $view->vars['full_name']);
+ }
+
+ public function testStripLeadingUnderscoresAndDigitsFromId()
+ {
+ $view = $this->factory->createNamed('_09name', $this->getTestedType())
+ ->createView();
+
+ $this->assertEquals('name', $view->vars['id']);
+ $this->assertEquals('_09name', $view->vars['name']);
+ $this->assertEquals('_09name', $view->vars['full_name']);
+ }
+
+ public function testPassIdAndNameToViewWithParent()
+ {
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('child', $this->getTestedType())
+ ->getForm()
+ ->createView();
+
+ $this->assertEquals('parent_child', $view['child']->vars['id']);
+ $this->assertEquals('child', $view['child']->vars['name']);
+ $this->assertEquals('parent[child]', $view['child']->vars['full_name']);
+ }
+
+ public function testPassIdAndNameToViewWithGrandParent()
+ {
+ $builder = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('child', 'form');
+ $builder->get('child')->add('grand_child', $this->getTestedType());
+ $view = $builder->getForm()->createView();
+
+ $this->assertEquals('parent_child_grand_child', $view['child']['grand_child']->vars['id']);
+ $this->assertEquals('grand_child', $view['child']['grand_child']->vars['name']);
+ $this->assertEquals('parent[child][grand_child]', $view['child']['grand_child']->vars['full_name']);
+ }
+
+ public function testPassTranslationDomainToView()
+ {
+ $form = $this->factory->create($this->getTestedType(), null, array(
+ 'translation_domain' => 'domain',
+ ));
+ $view = $form->createView();
+
+ $this->assertSame('domain', $view->vars['translation_domain']);
+ }
+
+ public function testInheritTranslationDomainFromParent()
+ {
+ $view = $this->factory
+ ->createNamedBuilder('parent', 'form', null, array(
+ 'translation_domain' => 'domain',
+ ))
+ ->add('child', $this->getTestedType())
+ ->getForm()
+ ->createView();
+
+ $this->assertEquals('domain', $view['child']->vars['translation_domain']);
+ }
+
+ public function testPreferOwnTranslationDomain()
+ {
+ $view = $this->factory
+ ->createNamedBuilder('parent', 'form', null, array(
+ 'translation_domain' => 'parent_domain',
+ ))
+ ->add('child', $this->getTestedType(), array(
+ 'translation_domain' => 'domain',
+ ))
+ ->getForm()
+ ->createView();
+
+ $this->assertEquals('domain', $view['child']->vars['translation_domain']);
+ }
+
+ public function testDefaultTranslationDomain()
+ {
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('child', $this->getTestedType())
+ ->getForm()
+ ->createView();
+
+ $this->assertEquals('messages', $view['child']->vars['translation_domain']);
+ }
+
+ public function testPassLabelToView()
+ {
+ $form = $this->factory->createNamed('__test___field', $this->getTestedType(), null, array('label' => 'My label'));
+ $view = $form->createView();
+
+ $this->assertSame('My label', $view->vars['label']);
+ }
+
+ public function testPassMultipartFalseToView()
+ {
+ $form = $this->factory->create($this->getTestedType());
+ $view = $form->createView();
+
+ $this->assertFalse($view->vars['multipart']);
+ }
+
+ abstract protected function getTestedType();
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ButtonTypeTest extends BaseTypeTest
+{
+ public function testCreateButtonInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\Button', $this->factory->create('button'));
+ }
+
+ protected function getTestedType()
+ {
+ return 'button';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\CallbackTransformer;
+
+class CheckboxTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ public function testPassValueToView()
+ {
+ $form = $this->factory->create('checkbox', null, array('value' => 'foobar'));
+ $view = $form->createView();
+
+ $this->assertEquals('foobar', $view->vars['value']);
+ }
+
+ public function testCheckedIfDataTrue()
+ {
+ $form = $this->factory->create('checkbox');
+ $form->setData(true);
+ $view = $form->createView();
+
+ $this->assertTrue($view->vars['checked']);
+ }
+
+ public function testCheckedIfDataTrueWithEmptyValue()
+ {
+ $form = $this->factory->create('checkbox', null, array('value' => ''));
+ $form->setData(true);
+ $view = $form->createView();
+
+ $this->assertTrue($view->vars['checked']);
+ }
+
+ public function testNotCheckedIfDataFalse()
+ {
+ $form = $this->factory->create('checkbox');
+ $form->setData(false);
+ $view = $form->createView();
+
+ $this->assertFalse($view->vars['checked']);
+ }
+
+ public function testSubmitWithValueChecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => 'foobar',
+ ));
+ $form->submit('foobar');
+
+ $this->assertTrue($form->getData());
+ $this->assertEquals('foobar', $form->getViewData());
+ }
+
+ public function testSubmitWithRandomValueChecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => 'foobar',
+ ));
+ $form->submit('krixikraxi');
+
+ $this->assertTrue($form->getData());
+ $this->assertEquals('foobar', $form->getViewData());
+ }
+
+ public function testSubmitWithValueUnchecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => 'foobar',
+ ));
+ $form->submit(null);
+
+ $this->assertFalse($form->getData());
+ $this->assertNull($form->getViewData());
+ }
+
+ public function testSubmitWithEmptyValueChecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => '',
+ ));
+ $form->submit('');
+
+ $this->assertTrue($form->getData());
+ $this->assertSame('', $form->getViewData());
+ }
+
+ public function testSubmitWithEmptyValueUnchecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => '',
+ ));
+ $form->submit(null);
+
+ $this->assertFalse($form->getData());
+ $this->assertNull($form->getViewData());
+ }
+
+ public function testBindWithEmptyValueAndFalseUnchecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => '',
+ ));
+ $form->bind(false);
+
+ $this->assertFalse($form->getData());
+ $this->assertNull($form->getViewData());
+ }
+
+ public function testBindWithEmptyValueAndTrueChecked()
+ {
+ $form = $this->factory->create('checkbox', null, array(
+ 'value' => '',
+ ));
+ $form->bind(true);
+
+ $this->assertTrue($form->getData());
+ $this->assertSame('', $form->getViewData());
+ }
+
+ /**
+ * @dataProvider provideTransformedData
+ */
+ public function testTransformedData($data, $expected)
+ {
+ // present a binary status field as a checkbox
+ $transformer = new CallbackTransformer(
+ function ($value) {
+ return 'expedited' == $value;
+ },
+ function ($value) {
+ return $value ? 'expedited' : 'standard';
+ }
+ );
+
+ $form = $this->builder
+ ->create('expedited_shipping', 'checkbox')
+ ->addModelTransformer($transformer)
+ ->getForm();
+ $form->setData($data);
+ $view = $form->createView();
+
+ $this->assertEquals($expected, $view->vars['checked']);
+ }
+
+ public function provideTransformedData()
+ {
+ return array(
+ array('expedited', true),
+ array('standard', false),
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Test\FormPerformanceTestCase;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ChoiceTypePerformanceTest extends FormPerformanceTestCase
+{
+ /**
+ * This test case is realistic in collection forms where each
+ * row contains the same choice field.
+ *
+ * @group benchmark
+ */
+ public function testSameChoiceFieldCreatedMultipleTimes()
+ {
+ $this->setMaxRunningTime(1);
+ $choices = range(1, 300);
+
+ for ($i = 0; $i < 100; ++$i) {
+ $this->factory->create('choice', rand(1, 400), array(
+ 'choices' => $choices,
+ ));
+ }
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class ChoiceTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ private $choices = array(
+ 'a' => 'Bernhard',
+ 'b' => 'Fabien',
+ 'c' => 'Kris',
+ 'd' => 'Jon',
+ 'e' => 'Roman',
+ );
+
+ private $numericChoices = array(
+ 0 => 'Bernhard',
+ 1 => 'Fabien',
+ 2 => 'Kris',
+ 3 => 'Jon',
+ 4 => 'Roman',
+ );
+
+ private $objectChoices;
+
+ protected $groupedChoices = array(
+ 'Symfony' => array(
+ 'a' => 'Bernhard',
+ 'b' => 'Fabien',
+ 'c' => 'Kris',
+ ),
+ 'Doctrine' => array(
+ 'd' => 'Jon',
+ 'e' => 'Roman',
+ )
+ );
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->objectChoices = array(
+ (object) array('id' => 1, 'name' => 'Bernhard'),
+ (object) array('id' => 2, 'name' => 'Fabien'),
+ (object) array('id' => 3, 'name' => 'Kris'),
+ (object) array('id' => 4, 'name' => 'Jon'),
+ (object) array('id' => 5, 'name' => 'Roman'),
+ );
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->objectChoices = null;
+ }
+
+ /**
+ * @expectedException \PHPUnit_Framework_Error
+ */
+ public function testChoicesOptionExpectsArray()
+ {
+ $this->factory->create('choice', null, array(
+ 'choices' => new \ArrayObject(),
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testChoiceListOptionExpectsChoiceListInterface()
+ {
+ $this->factory->create('choice', null, array(
+ 'choice_list' => array('foo' => 'foo'),
+ ));
+ }
+
+ public function testChoiceListAndChoicesCanBeEmpty()
+ {
+ $this->factory->create('choice');
+ }
+
+ public function testExpandedChoicesOptionsTurnIntoChildren()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'expanded' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $this->assertCount(count($this->choices), $form, 'Each choice should become a new field');
+ }
+
+ public function testPlaceholderPresentOnNonRequiredExpandedSingleChoice()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $this->assertTrue(isset($form['placeholder']));
+ $this->assertCount(count($this->choices) + 1, $form, 'Each choice should become a new field');
+ }
+
+ public function testPlaceholderNotPresentIfRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $this->assertFalse(isset($form['placeholder']));
+ $this->assertCount(count($this->choices), $form, 'Each choice should become a new field');
+ }
+
+ public function testPlaceholderNotPresentIfMultiple()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $this->assertFalse(isset($form['placeholder']));
+ $this->assertCount(count($this->choices), $form, 'Each choice should become a new field');
+ }
+
+ public function testPlaceholderNotPresentIfEmptyChoice()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => array(
+ '' => 'Empty',
+ 1 => 'Not empty',
+ ),
+ ));
+
+ $this->assertFalse(isset($form['placeholder']));
+ $this->assertCount(2, $form, 'Each choice should become a new field');
+ }
+
+ public function testExpandedChoicesOptionsAreFlattened()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'expanded' => true,
+ 'choices' => $this->groupedChoices,
+ ));
+
+ $flattened = array();
+ foreach ($this->groupedChoices as $choices) {
+ $flattened = array_merge($flattened, array_keys($choices));
+ }
+
+ $this->assertCount($form->count(), $flattened, 'Each nested choice should become a new field, not the groups');
+
+ foreach ($flattened as $value => $choice) {
+ $this->assertTrue($form->has($value), 'Flattened choice is named after it\'s value');
+ }
+ }
+
+ public function testExpandedCheckboxesAreNeverRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ foreach ($form as $child) {
+ $this->assertFalse($child->isRequired());
+ }
+ }
+
+ public function testExpandedRadiosAreRequiredIfChoiceChildIsRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ foreach ($form as $child) {
+ $this->assertTrue($child->isRequired());
+ }
+ }
+
+ public function testExpandedRadiosAreNotRequiredIfChoiceChildIsNotRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ foreach ($form as $child) {
+ $this->assertFalse($child->isRequired());
+ }
+ }
+
+ public function testSubmitSingleNonExpanded()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit('b');
+
+ $this->assertEquals('b', $form->getData());
+ $this->assertEquals('b', $form->getViewData());
+ }
+
+ public function testSubmitSingleNonExpandedObjectChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => false,
+ 'choice_list' => new ObjectChoiceList(
+ $this->objectChoices,
+ // label path
+ 'name',
+ array(),
+ null,
+ // value path
+ 'id'
+ ),
+ ));
+
+ // "id" value of the second entry
+ $form->submit('2');
+
+ $this->assertEquals($this->objectChoices[1], $form->getData());
+ $this->assertEquals('2', $form->getViewData());
+ }
+
+ public function testSubmitMultipleNonExpanded()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(array('a', 'b'));
+
+ $this->assertEquals(array('a', 'b'), $form->getData());
+ $this->assertEquals(array('a', 'b'), $form->getViewData());
+ }
+
+ public function testSubmitMultipleNonExpandedObjectChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => false,
+ 'choice_list' => new ObjectChoiceList(
+ $this->objectChoices,
+ // label path
+ 'name',
+ array(),
+ null,
+ // value path
+ 'id'
+ ),
+ ));
+
+ $form->submit(array('2', '3'));
+
+ $this->assertEquals(array($this->objectChoices[1], $this->objectChoices[2]), $form->getData());
+ $this->assertEquals(array('2', '3'), $form->getViewData());
+ }
+
+ public function testSubmitSingleExpandedRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit('b');
+
+ $this->assertSame('b', $form->getData());
+ $this->assertSame(array(
+ 0 => false,
+ 1 => true,
+ 2 => false,
+ 3 => false,
+ 4 => false,
+ ), $form->getViewData());
+
+ $this->assertFalse($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertSame('b', $form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitSingleExpandedNonRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit('b');
+
+ $this->assertSame('b', $form->getData());
+ $this->assertSame(array(
+ 0 => false,
+ 1 => true,
+ 2 => false,
+ 3 => false,
+ 4 => false,
+ 'placeholder' => false,
+ ), $form->getViewData());
+
+ $this->assertFalse($form['placeholder']->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form['placeholder']->getViewData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertSame('b', $form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitSingleExpandedRequiredNothingChecked()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(null);
+
+ $this->assertNull($form->getData());
+ $this->assertSame(array(
+ 0 => false,
+ 1 => false,
+ 2 => false,
+ 3 => false,
+ 4 => false,
+ ), $form->getViewData());
+
+ $this->assertFalse($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitSingleExpandedNonRequiredNothingChecked()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(null);
+
+ $this->assertNull($form->getData());
+ $this->assertSame(array(
+ 0 => false,
+ 1 => false,
+ 2 => false,
+ 3 => false,
+ 4 => false,
+ 'placeholder' => true,
+ ), $form->getViewData());
+
+ $this->assertTrue($form['placeholder']->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertSame('', $form['placeholder']->getViewData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitFalseToSingleExpandedRequiredDoesNotProduceExtraChildrenError()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(false);
+
+ $this->assertEmpty($form->getExtraData());
+ $this->assertNull($form->getData());
+ }
+
+ public function testSubmitFalseToSingleExpandedNonRequiredDoesNotProduceExtraChildrenError()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(false);
+
+ $this->assertEmpty($form->getExtraData());
+ $this->assertNull($form->getData());
+ }
+
+ public function testSubmitSingleExpandedWithEmptyChild()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'choices' => array(
+ '' => 'Empty',
+ 1 => 'Not empty',
+ ),
+ ));
+
+ $form->submit('');
+
+ $this->assertNull($form->getData());
+ $this->assertTrue($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertSame('', $form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ }
+
+ public function testSubmitSingleExpandedObjectChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'choice_list' => new ObjectChoiceList(
+ $this->objectChoices,
+ // label path
+ 'name',
+ array(),
+ null,
+ // value path
+ 'id'
+ ),
+ ));
+
+ $form->submit('2');
+
+ $this->assertSame($this->objectChoices[1], $form->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertSame('2', $form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitSingleExpandedNumericChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => true,
+ 'choices' => $this->numericChoices,
+ ));
+
+ $form->submit('1');
+
+ $this->assertSame(1, $form->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertSame('1', $form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitMultipleExpanded()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(array('a', 'c'));
+
+ $this->assertSame(array('a', 'c'), $form->getData());
+ $this->assertTrue($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertTrue($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertSame('a', $form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ $this->assertSame('c', $form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitMultipleExpandedEmpty()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'choices' => $this->choices,
+ ));
+
+ $form->submit(array());
+
+ $this->assertSame(array(), $form->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitMultipleExpandedWithEmptyChild()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'choices' => array(
+ '' => 'Empty',
+ 1 => 'Not Empty',
+ 2 => 'Not Empty 2',
+ )
+ ));
+
+ $form->submit(array('', '2'));
+
+ $this->assertSame(array('', 2), $form->getData());
+ $this->assertTrue($form[0]->getData());
+ $this->assertFalse($form[1]->getData());
+ $this->assertTrue($form[2]->getData());
+ $this->assertSame('', $form[0]->getViewData());
+ $this->assertNull($form[1]->getViewData());
+ $this->assertSame('2', $form[2]->getViewData());
+ }
+
+ public function testSubmitMultipleExpandedObjectChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'choice_list' => new ObjectChoiceList(
+ $this->objectChoices,
+ // label path
+ 'name',
+ array(),
+ null,
+ // value path
+ 'id'
+ ),
+ ));
+
+ $form->submit(array('1', '2'));
+
+ $this->assertSame(array($this->objectChoices[0], $this->objectChoices[1]), $form->getData());
+ $this->assertTrue($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertFalse($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertSame('1', $form[0]->getViewData());
+ $this->assertSame('2', $form[1]->getViewData());
+ $this->assertNull($form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ public function testSubmitMultipleExpandedNumericChoices()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => true,
+ 'choices' => $this->numericChoices,
+ ));
+
+ $form->submit(array('1', '2'));
+
+ $this->assertSame(array(1, 2), $form->getData());
+ $this->assertFalse($form[0]->getData());
+ $this->assertTrue($form[1]->getData());
+ $this->assertTrue($form[2]->getData());
+ $this->assertFalse($form[3]->getData());
+ $this->assertFalse($form[4]->getData());
+ $this->assertNull($form[0]->getViewData());
+ $this->assertSame('1', $form[1]->getViewData());
+ $this->assertSame('2', $form[2]->getViewData());
+ $this->assertNull($form[3]->getViewData());
+ $this->assertNull($form[4]->getViewData());
+ }
+
+ /*
+ * We need this functionality to create choice fields for Boolean types,
+ * e.g. false => 'No', true => 'Yes'
+ */
+ public function testSetDataSingleNonExpandedAcceptsBoolean()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'expanded' => false,
+ 'choices' => $this->numericChoices,
+ ));
+
+ $form->setData(false);
+
+ $this->assertFalse($form->getData());
+ $this->assertEquals('0', $form->getViewData());
+ }
+
+ public function testSetDataMultipleNonExpandedAcceptsBoolean()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'expanded' => false,
+ 'choices' => $this->numericChoices,
+ ));
+
+ $form->setData(array(false, true));
+
+ $this->assertEquals(array(false, true), $form->getData());
+ $this->assertEquals(array('0', '1'), $form->getViewData());
+ }
+
+ public function testPassRequiredToView()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertTrue($view->vars['required']);
+ }
+
+ public function testPassNonRequiredToView()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertFalse($view->vars['required']);
+ }
+
+ public function testPassMultipleToView()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => true,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertTrue($view->vars['multiple']);
+ }
+
+ public function testPassExpandedToView()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'expanded' => true,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertTrue($view->vars['expanded']);
+ }
+
+ public function testEmptyValueIsNullByDefaultIfRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'required' => true,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertNull($view->vars['empty_value']);
+ }
+
+ public function testEmptyValueIsEmptyStringByDefaultIfNotRequired()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => false,
+ 'required' => false,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertSame('', $view->vars['empty_value']);
+ }
+
+ /**
+ * @dataProvider getOptionsWithEmptyValue
+ */
+ public function testPassEmptyValueToView($multiple, $expanded, $required, $emptyValue, $viewValue)
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => $multiple,
+ 'expanded' => $expanded,
+ 'required' => $required,
+ 'empty_value' => $emptyValue,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals($viewValue, $view->vars['empty_value']);
+ }
+
+ /**
+ * @dataProvider getOptionsWithEmptyValue
+ */
+ public function testDontPassEmptyValueIfContainedInChoices($multiple, $expanded, $required, $emptyValue, $viewValue)
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'multiple' => $multiple,
+ 'expanded' => $expanded,
+ 'required' => $required,
+ 'empty_value' => $emptyValue,
+ 'choices' => array('a' => 'A', '' => 'Empty'),
+ ));
+ $view = $form->createView();
+
+ $this->assertNull($view->vars['empty_value']);
+ }
+
+ public function getOptionsWithEmptyValue()
+ {
+ return array(
+ // single non-expanded
+ array(false, false, false, 'foobar', 'foobar'),
+ array(false, false, false, '', ''),
+ array(false, false, false, null, null),
+ array(false, false, false, false, null),
+ array(false, false, true, 'foobar', 'foobar'),
+ array(false, false, true, '', ''),
+ array(false, false, true, null, null),
+ array(false, false, true, false, null),
+ // single expanded
+ array(false, true, false, 'foobar', 'foobar'),
+ // radios should never have an empty label
+ array(false, true, false, '', 'None'),
+ array(false, true, false, null, null),
+ array(false, true, false, false, null),
+ array(false, true, true, 'foobar', 'foobar'),
+ // radios should never have an empty label
+ array(false, true, true, '', 'None'),
+ array(false, true, true, null, null),
+ array(false, true, true, false, null),
+ // multiple non-expanded
+ array(true, false, false, 'foobar', null),
+ array(true, false, false, '', null),
+ array(true, false, false, null, null),
+ array(true, false, false, false, null),
+ array(true, false, true, 'foobar', null),
+ array(true, false, true, '', null),
+ array(true, false, true, null, null),
+ array(true, false, true, false, null),
+ // multiple expanded
+ array(true, true, false, 'foobar', null),
+ array(true, true, false, '', null),
+ array(true, true, false, null, null),
+ array(true, true, false, false, null),
+ array(true, true, true, 'foobar', null),
+ array(true, true, true, '', null),
+ array(true, true, true, null, null),
+ array(true, true, true, false, null),
+ );
+ }
+
+ public function testPassChoicesToView()
+ {
+ $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D');
+ $form = $this->factory->create('choice', null, array(
+ 'choices' => $choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('a', 'a', 'A'),
+ new ChoiceView('b', 'b', 'B'),
+ new ChoiceView('c', 'c', 'C'),
+ new ChoiceView('d', 'd', 'D'),
+ ), $view->vars['choices']);
+ }
+
+ public function testPassPreferredChoicesToView()
+ {
+ $choices = array('a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D');
+ $form = $this->factory->create('choice', null, array(
+ 'choices' => $choices,
+ 'preferred_choices' => array('b', 'd'),
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ 0 => new ChoiceView('a', 'a', 'A'),
+ 2 => new ChoiceView('c', 'c', 'C'),
+ ), $view->vars['choices']);
+ $this->assertEquals(array(
+ 1 => new ChoiceView('b', 'b', 'B'),
+ 3 => new ChoiceView('d', 'd', 'D'),
+ ), $view->vars['preferred_choices']);
+ }
+
+ public function testPassHierarchicalChoicesToView()
+ {
+ $form = $this->factory->create('choice', null, array(
+ 'choices' => $this->groupedChoices,
+ 'preferred_choices' => array('b', 'd'),
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ 'Symfony' => array(
+ 0 => new ChoiceView('a', 'a', 'Bernhard'),
+ 2 => new ChoiceView('c', 'c', 'Kris'),
+ ),
+ 'Doctrine' => array(
+ 4 => new ChoiceView('e', 'e', 'Roman'),
+ ),
+ ), $view->vars['choices']);
+ $this->assertEquals(array(
+ 'Symfony' => array(
+ 1 => new ChoiceView('b', 'b', 'Fabien'),
+ ),
+ 'Doctrine' => array(
+ 3 => new ChoiceView('d', 'd', 'Jon'),
+ ),
+ ), $view->vars['preferred_choices']);
+ }
+
+ public function testPassChoiceDataToView()
+ {
+ $obj1 = (object) array('value' => 'a', 'label' => 'A');
+ $obj2 = (object) array('value' => 'b', 'label' => 'B');
+ $obj3 = (object) array('value' => 'c', 'label' => 'C');
+ $obj4 = (object) array('value' => 'd', 'label' => 'D');
+ $form = $this->factory->create('choice', null, array(
+ 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'),
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView($obj1, 'a', 'A'),
+ new ChoiceView($obj2, 'b', 'B'),
+ new ChoiceView($obj3, 'c', 'C'),
+ new ChoiceView($obj4, 'd', 'D'),
+ ), $view->vars['choices']);
+ }
+
+ public function testAdjustFullNameForMultipleNonExpanded()
+ {
+ $form = $this->factory->createNamed('name', 'choice', null, array(
+ 'multiple' => true,
+ 'expanded' => false,
+ 'choices' => $this->choices,
+ ));
+ $view = $form->createView();
+
+ $this->assertSame('name[]', $view->vars['full_name']);
+ }
+
+ // https://github.com/symfony/symfony/issues/3298
+ public function testInitializeWithEmptyChoices()
+ {
+ $this->factory->createNamed('name', 'choice', null, array(
+ 'choices' => array(),
+ ));
+ }
+
+ public function testInitializeWithDefaultObjectChoice()
+ {
+ $obj1 = (object) array('value' => 'a', 'label' => 'A');
+ $obj2 = (object) array('value' => 'b', 'label' => 'B');
+ $obj3 = (object) array('value' => 'c', 'label' => 'C');
+ $obj4 = (object) array('value' => 'd', 'label' => 'D');
+
+ $form = $this->factory->create('choice', null, array(
+ 'choice_list' => new ObjectChoiceList(array($obj1, $obj2, $obj3, $obj4), 'label', array(), null, 'value'),
+ // Used to break because "data_class" was inferred, which needs to
+ // remain null in every case (because it refers to the view format)
+ 'data' => $obj3,
+ ));
+
+ // Trigger data initialization
+ $form->getViewData();
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Form;
+
+class CollectionTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ public function testContainsNoChildByDefault()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ ));
+
+ $this->assertCount(0, $form);
+ }
+
+ public function testSetDataAdjustsSize()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ 'options' => array(
+ 'max_length' => 20,
+ ),
+ ));
+ $form->setData(array('foo@foo.com', 'foo@bar.com'));
+
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $form[0]);
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $form[1]);
+ $this->assertCount(2, $form);
+ $this->assertEquals('foo@foo.com', $form[0]->getData());
+ $this->assertEquals('foo@bar.com', $form[1]->getData());
+ $this->assertEquals(20, $form[0]->getConfig()->getOption('max_length'));
+ $this->assertEquals(20, $form[1]->getConfig()->getOption('max_length'));
+
+ $form->setData(array('foo@baz.com'));
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $form[0]);
+ $this->assertFalse(isset($form[1]));
+ $this->assertCount(1, $form);
+ $this->assertEquals('foo@baz.com', $form[0]->getData());
+ $this->assertEquals(20, $form[0]->getConfig()->getOption('max_length'));
+ }
+
+ public function testThrowsExceptionIfObjectIsNotTraversable()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ ));
+ $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
+ $form->setData(new \stdClass());
+ }
+
+ public function testNotResizedIfSubmittedWithMissingData()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ ));
+ $form->setData(array('foo@foo.com', 'bar@bar.com'));
+ $form->submit(array('foo@bar.com'));
+
+ $this->assertTrue($form->has('0'));
+ $this->assertTrue($form->has('1'));
+ $this->assertEquals('foo@bar.com', $form[0]->getData());
+ $this->assertEquals('', $form[1]->getData());
+ }
+
+ public function testResizedDownIfSubmittedWithMissingDataAndAllowDelete()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ 'allow_delete' => true,
+ ));
+ $form->setData(array('foo@foo.com', 'bar@bar.com'));
+ $form->submit(array('foo@foo.com'));
+
+ $this->assertTrue($form->has('0'));
+ $this->assertFalse($form->has('1'));
+ $this->assertEquals('foo@foo.com', $form[0]->getData());
+ $this->assertEquals(array('foo@foo.com'), $form->getData());
+ }
+
+ public function testNotResizedIfSubmittedWithExtraData()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ ));
+ $form->setData(array('foo@bar.com'));
+ $form->submit(array('foo@foo.com', 'bar@bar.com'));
+
+ $this->assertTrue($form->has('0'));
+ $this->assertFalse($form->has('1'));
+ $this->assertEquals('foo@foo.com', $form[0]->getData());
+ }
+
+ public function testResizedUpIfSubmittedWithExtraDataAndAllowAdd()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'text',
+ 'allow_add' => true,
+ ));
+ $form->setData(array('foo@bar.com'));
+ $form->submit(array('foo@bar.com', 'bar@bar.com'));
+
+ $this->assertTrue($form->has('0'));
+ $this->assertTrue($form->has('1'));
+ $this->assertEquals('foo@bar.com', $form[0]->getData());
+ $this->assertEquals('bar@bar.com', $form[1]->getData());
+ $this->assertEquals(array('foo@bar.com', 'bar@bar.com'), $form->getData());
+ }
+
+ public function testAllowAddButNoPrototype()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'form',
+ 'allow_add' => true,
+ 'prototype' => false,
+ ));
+
+ $this->assertFalse($form->has('__name__'));
+ }
+
+ public function testPrototypeMultipartPropagation()
+ {
+ $form = $this->factory
+ ->create('collection', null, array(
+ 'type' => 'file',
+ 'allow_add' => true,
+ 'prototype' => true,
+ ))
+ ;
+
+ $this->assertTrue($form->createView()->vars['multipart']);
+ }
+
+ public function testGetDataDoesNotContainsPrototypeNameBeforeDataAreSet()
+ {
+ $form = $this->factory->create('collection', array(), array(
+ 'type' => 'file',
+ 'prototype' => true,
+ 'allow_add' => true,
+ ));
+
+ $data = $form->getData();
+ $this->assertFalse(isset($data['__name__']));
+ }
+
+ public function testGetDataDoesNotContainsPrototypeNameAfterDataAreSet()
+ {
+ $form = $this->factory->create('collection', array(), array(
+ 'type' => 'file',
+ 'allow_add' => true,
+ 'prototype' => true,
+ ));
+
+ $form->setData(array('foobar.png'));
+ $data = $form->getData();
+ $this->assertFalse(isset($data['__name__']));
+ }
+
+ public function testPrototypeNameOption()
+ {
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'form',
+ 'prototype' => true,
+ 'allow_add' => true,
+ ));
+
+ $this->assertSame('__name__', $form->getConfig()->getAttribute('prototype')->getName(), '__name__ is the default');
+
+ $form = $this->factory->create('collection', null, array(
+ 'type' => 'form',
+ 'prototype' => true,
+ 'allow_add' => true,
+ 'prototype_name' => '__test__',
+ ));
+
+ $this->assertSame('__test__', $form->getConfig()->getAttribute('prototype')->getName());
+ }
+
+ public function testPrototypeDefaultLabel()
+ {
+ $form = $this->factory->create('collection', array(), array(
+ 'type' => 'file',
+ 'allow_add' => true,
+ 'prototype' => true,
+ 'prototype_name' => '__test__',
+ ));
+
+ $this->assertSame('__test__label__', $form->createView()->vars['prototype']->vars['label']);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class CountryTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testCountriesAreSelectable()
+ {
+ $form = $this->factory->create('country');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ // Don't check objects for identity
+ $this->assertContains(new ChoiceView('DE', 'DE', 'Germany'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('GB', 'GB', 'United Kingdom'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('US', 'US', 'United States'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('FR', 'FR', 'France'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('MY', 'MY', 'Malaysia'), $choices, '', false, false);
+ }
+
+ public function testUnknownCountryIsNotIncluded()
+ {
+ $form = $this->factory->create('country', 'country');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ foreach ($choices as $choice) {
+ if ('ZZ' === $choice->value) {
+ $this->fail('Should not contain choice "ZZ"');
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class CurrencyTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testCurrenciesAreSelectable()
+ {
+ $form = $this->factory->create('currency');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ $this->assertContains(new ChoiceView('EUR', 'EUR', 'Euro'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('USD', 'USD', 'US Dollar'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('SIT', 'SIT', 'Slovenian Tolar'), $choices, '', false, false);
+ }
+
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class DateTimeTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testSubmitDateTime()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ 'input' => 'datetime',
+ ));
+
+ $form->submit(array(
+ 'date' => array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ),
+ 'time' => array(
+ 'hour' => '3',
+ 'minute' => '4',
+ ),
+ ));
+
+ $dateTime = new \DateTime('2010-06-02 03:04:00 UTC');
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ }
+
+ public function testSubmitString()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'date' => array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ),
+ 'time' => array(
+ 'hour' => '3',
+ 'minute' => '4',
+ ),
+ ));
+
+ $this->assertEquals('2010-06-02 03:04:00', $form->getData());
+ }
+
+ public function testSubmitTimestamp()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'timestamp',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'date' => array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ),
+ 'time' => array(
+ 'hour' => '3',
+ 'minute' => '4',
+ ),
+ ));
+
+ $dateTime = new \DateTime('2010-06-02 03:04:00 UTC');
+
+ $this->assertEquals($dateTime->format('U'), $form->getData());
+ }
+
+ public function testSubmitWithoutMinutes()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ 'input' => 'datetime',
+ 'with_minutes' => false,
+ ));
+
+ $form->setData(new \DateTime('2010-06-02 03:04:05 UTC'));
+
+ $input = array(
+ 'date' => array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ),
+ 'time' => array(
+ 'hour' => '3',
+ ),
+ );
+
+ $form->submit($input);
+
+ $this->assertDateTimeEquals(new \DateTime('2010-06-02 03:00:00 UTC'), $form->getData());
+ }
+
+ public function testSubmitWithSeconds()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ 'input' => 'datetime',
+ 'with_seconds' => true,
+ ));
+
+ $form->setData(new \DateTime('2010-06-02 03:04:05 UTC'));
+
+ $input = array(
+ 'date' => array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ),
+ 'time' => array(
+ 'hour' => '3',
+ 'minute' => '4',
+ 'second' => '5',
+ ),
+ );
+
+ $form->submit($input);
+
+ $this->assertDateTimeEquals(new \DateTime('2010-06-02 03:04:05 UTC'), $form->getData());
+ }
+
+ public function testSubmitDifferentTimezones()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Pacific/Tahiti',
+ 'date_widget' => 'choice',
+ 'time_widget' => 'choice',
+ 'input' => 'string',
+ 'with_seconds' => true,
+ ));
+
+ $dateTime = new \DateTime('2010-06-02 03:04:05 Pacific/Tahiti');
+
+ $form->submit(array(
+ 'date' => array(
+ 'day' => (int) $dateTime->format('d'),
+ 'month' => (int) $dateTime->format('m'),
+ 'year' => (int) $dateTime->format('Y'),
+ ),
+ 'time' => array(
+ 'hour' => (int) $dateTime->format('H'),
+ 'minute' => (int) $dateTime->format('i'),
+ 'second' => (int) $dateTime->format('s'),
+ ),
+ ));
+
+ $dateTime->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $this->assertEquals($dateTime->format('Y-m-d H:i:s'), $form->getData());
+ }
+
+ public function testSubmitDifferentTimezonesDateTime()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Pacific/Tahiti',
+ 'widget' => 'single_text',
+ 'input' => 'datetime',
+ ));
+
+ $outputTime = new \DateTime('2010-06-02 03:04:00 Pacific/Tahiti');
+
+ $form->submit('2010-06-02T03:04:00-10:00');
+
+ $outputTime->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $this->assertDateTimeEquals($outputTime, $form->getData());
+ $this->assertEquals('2010-06-02T03:04:00-10:00', $form->getViewData());
+ }
+
+ public function testSubmitStringSingleText()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ ));
+
+ $form->submit('2010-06-02T03:04:00Z');
+
+ $this->assertEquals('2010-06-02 03:04:00', $form->getData());
+ $this->assertEquals('2010-06-02T03:04:00Z', $form->getViewData());
+ }
+
+ public function testSubmitStringSingleTextWithSeconds()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit('2010-06-02T03:04:05Z');
+
+ $this->assertEquals('2010-06-02 03:04:05', $form->getData());
+ $this->assertEquals('2010-06-02T03:04:05Z', $form->getViewData());
+ }
+
+ public function testSubmitDifferentPattern()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'date_format' => 'MM*yyyy*dd',
+ 'date_widget' => 'single_text',
+ 'time_widget' => 'single_text',
+ 'input' => 'datetime',
+ ));
+
+ $dateTime = new \DateTime('2010-06-02 03:04');
+
+ $form->submit(array(
+ 'date' => '06*2010*02',
+ 'time' => '03:04',
+ ));
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ }
+
+ // Bug fix
+ public function testInitializeWithDateTime()
+ {
+ // Throws an exception if "data_class" option is not explicitly set
+ // to null in the type
+ $this->factory->create('datetime', new \DateTime());
+ }
+
+ public function testSingleTextWidgetShouldUseTheRightInputType()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'widget' => 'single_text',
+ ));
+
+ $view = $form->createView();
+ $this->assertEquals('datetime', $view->vars['type']);
+ }
+
+ public function testPassDefaultEmptyValueToViewIfNotRequired()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'required' => false,
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('', $view['date']['year']->vars['empty_value']);
+ $this->assertSame('', $view['date']['month']->vars['empty_value']);
+ $this->assertSame('', $view['date']['day']->vars['empty_value']);
+ $this->assertSame('', $view['time']['hour']->vars['empty_value']);
+ $this->assertSame('', $view['time']['minute']->vars['empty_value']);
+ $this->assertSame('', $view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassNoEmptyValueToViewIfRequired()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'required' => true,
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertNull($view['date']['year']->vars['empty_value']);
+ $this->assertNull($view['date']['month']->vars['empty_value']);
+ $this->assertNull($view['date']['day']->vars['empty_value']);
+ $this->assertNull($view['time']['hour']->vars['empty_value']);
+ $this->assertNull($view['time']['minute']->vars['empty_value']);
+ $this->assertNull($view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsString()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'empty_value' => 'Empty',
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty', $view['date']['year']->vars['empty_value']);
+ $this->assertSame('Empty', $view['date']['month']->vars['empty_value']);
+ $this->assertSame('Empty', $view['date']['day']->vars['empty_value']);
+ $this->assertSame('Empty', $view['time']['hour']->vars['empty_value']);
+ $this->assertSame('Empty', $view['time']['minute']->vars['empty_value']);
+ $this->assertSame('Empty', $view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsArray()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'month' => 'Empty month',
+ 'day' => 'Empty day',
+ 'hour' => 'Empty hour',
+ 'minute' => 'Empty minute',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['date']['year']->vars['empty_value']);
+ $this->assertSame('Empty month', $view['date']['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['date']['day']->vars['empty_value']);
+ $this->assertSame('Empty hour', $view['time']['hour']->vars['empty_value']);
+ $this->assertSame('Empty minute', $view['time']['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddEmptyIfNotRequired()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'required' => false,
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'day' => 'Empty day',
+ 'hour' => 'Empty hour',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['date']['year']->vars['empty_value']);
+ $this->assertSame('', $view['date']['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['date']['day']->vars['empty_value']);
+ $this->assertSame('Empty hour', $view['time']['hour']->vars['empty_value']);
+ $this->assertSame('', $view['time']['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddNullIfRequired()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'required' => true,
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'day' => 'Empty day',
+ 'hour' => 'Empty hour',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['date']['year']->vars['empty_value']);
+ $this->assertNull($view['date']['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['date']['day']->vars['empty_value']);
+ $this->assertSame('Empty hour', $view['time']['hour']->vars['empty_value']);
+ $this->assertNull($view['time']['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['time']['second']->vars['empty_value']);
+ }
+
+ public function testPassHtml5TypeIfSingleTextAndHtml5Format()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'widget' => 'single_text',
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('datetime', $view->vars['type']);
+ }
+
+ public function testDontPassHtml5TypeIfNotHtml5Format()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'widget' => 'single_text',
+ 'format' => 'yyyy-MM-dd HH:mm',
+ ));
+
+ $view = $form->createView();
+ $this->assertFalse(isset($view->vars['type']));
+ }
+
+ public function testDontPassHtml5TypeIfNotSingleText()
+ {
+ $form = $this->factory->create('datetime', null, array(
+ 'widget' => 'text',
+ ));
+
+ $view = $form->createView();
+ $this->assertFalse(isset($view->vars['type']));
+ }
+
+ public function testDateTypeChoiceErrorsBubbleUp()
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('datetime', null);
+
+ $form['date']->addError($error);
+
+ $this->assertSame(array(), $form['date']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ public function testDateTypeSingleTextErrorsBubbleUp()
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('datetime', null, array(
+ 'date_widget' => 'single_text'
+ ));
+
+ $form['date']->addError($error);
+
+ $this->assertSame(array(), $form['date']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ public function testTimeTypeChoiceErrorsBubbleUp()
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('datetime', null);
+
+ $form['time']->addError($error);
+
+ $this->assertSame(array(), $form['time']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ public function testTimeTypeSingleTextErrorsBubbleUp()
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('datetime', null, array(
+ 'time_widget' => 'single_text'
+ ));
+
+ $form['time']->addError($error);
+
+ $this->assertSame(array(), $form['time']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class DateTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // we test against "de_AT", so we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_AT');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testInvalidWidgetOption()
+ {
+ $this->factory->create('date', null, array(
+ 'widget' => 'fake_widget',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testInvalidInputOption()
+ {
+ $this->factory->create('date', null, array(
+ 'input' => 'fake_input',
+ ));
+ }
+
+ public function testSubmitFromSingleTextDateTimeWithDefaultFormat()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'datetime',
+ ));
+
+ $form->submit('2010-06-02');
+
+ $this->assertDateTimeEquals(new \DateTime('2010-06-02 UTC'), $form->getData());
+ $this->assertEquals('2010-06-02', $form->getViewData());
+ }
+
+ public function testSubmitFromSingleTextDateTime()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'datetime',
+ ));
+
+ $form->submit('2.6.2010');
+
+ $this->assertDateTimeEquals(new \DateTime('2010-06-02 UTC'), $form->getData());
+ $this->assertEquals('02.06.2010', $form->getViewData());
+ }
+
+ public function testSubmitFromSingleTextString()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'string',
+ ));
+
+ $form->submit('2.6.2010');
+
+ $this->assertEquals('2010-06-02', $form->getData());
+ $this->assertEquals('02.06.2010', $form->getViewData());
+ }
+
+ public function testSubmitFromSingleTextTimestamp()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'timestamp',
+ ));
+
+ $form->submit('2.6.2010');
+
+ $dateTime = new \DateTime('2010-06-02 UTC');
+
+ $this->assertEquals($dateTime->format('U'), $form->getData());
+ $this->assertEquals('02.06.2010', $form->getViewData());
+ }
+
+ public function testSubmitFromSingleTextRaw()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ 'input' => 'array',
+ ));
+
+ $form->submit('2.6.2010');
+
+ $output = array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ );
+
+ $this->assertEquals($output, $form->getData());
+ $this->assertEquals('02.06.2010', $form->getViewData());
+ }
+
+ public function testSubmitFromText()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'text',
+ ));
+
+ $text = array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ );
+
+ $form->submit($text);
+
+ $dateTime = new \DateTime('2010-06-02 UTC');
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ $this->assertEquals($text, $form->getViewData());
+ }
+
+ public function testSubmitFromChoice()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ ));
+
+ $text = array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ );
+
+ $form->submit($text);
+
+ $dateTime = new \DateTime('2010-06-02 UTC');
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ $this->assertEquals($text, $form->getViewData());
+ }
+
+ public function testSubmitFromChoiceEmpty()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ 'required' => false,
+ ));
+
+ $text = array(
+ 'day' => '',
+ 'month' => '',
+ 'year' => '',
+ );
+
+ $form->submit($text);
+
+ $this->assertNull($form->getData());
+ $this->assertEquals($text, $form->getViewData());
+ }
+
+ public function testSubmitFromInputDateTimeDifferentPattern()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'format' => 'MM*yyyy*dd',
+ 'widget' => 'single_text',
+ 'input' => 'datetime',
+ ));
+
+ $form->submit('06*2010*02');
+
+ $this->assertDateTimeEquals(new \DateTime('2010-06-02 UTC'), $form->getData());
+ $this->assertEquals('06*2010*02', $form->getViewData());
+ }
+
+ public function testSubmitFromInputStringDifferentPattern()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'format' => 'MM*yyyy*dd',
+ 'widget' => 'single_text',
+ 'input' => 'string',
+ ));
+
+ $form->submit('06*2010*02');
+
+ $this->assertEquals('2010-06-02', $form->getData());
+ $this->assertEquals('06*2010*02', $form->getViewData());
+ }
+
+ public function testSubmitFromInputTimestampDifferentPattern()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'format' => 'MM*yyyy*dd',
+ 'widget' => 'single_text',
+ 'input' => 'timestamp',
+ ));
+
+ $form->submit('06*2010*02');
+
+ $dateTime = new \DateTime('2010-06-02 UTC');
+
+ $this->assertEquals($dateTime->format('U'), $form->getData());
+ $this->assertEquals('06*2010*02', $form->getViewData());
+ }
+
+ public function testSubmitFromInputRawDifferentPattern()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'format' => 'MM*yyyy*dd',
+ 'widget' => 'single_text',
+ 'input' => 'array',
+ ));
+
+ $form->submit('06*2010*02');
+
+ $output = array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ );
+
+ $this->assertEquals($output, $form->getData());
+ $this->assertEquals('06*2010*02', $form->getViewData());
+ }
+
+ /**
+ * @dataProvider provideDateFormats
+ */
+ public function testDatePatternWithFormatOption($format, $pattern)
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => $format,
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals($pattern, $view->vars['date_pattern']);
+ }
+
+ public function provideDateFormats()
+ {
+ return array(
+ array('dMy', '{{ day }}{{ month }}{{ year }}'),
+ array('d-M-yyyy', '{{ day }}-{{ month }}-{{ year }}'),
+ array('M d y', '{{ month }} {{ day }} {{ year }}'),
+ );
+ }
+
+ /**
+ * This test is to check that the strings '0', '1', '2', '3' are no accepted
+ * as valid IntlDateFormatter constants for FULL, LONG, MEDIUM or SHORT respectively.
+ *
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testThrowExceptionIfFormatIsNoPattern()
+ {
+ $this->factory->create('date', null, array(
+ 'format' => '0',
+ 'widget' => 'single_text',
+ 'input' => 'string',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testThrowExceptionIfFormatDoesNotContainYearMonthAndDay()
+ {
+ $this->factory->create('date', null, array(
+ 'months' => array(6, 7),
+ 'format' => 'yy',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testThrowExceptionIfFormatIsNoConstant()
+ {
+ $this->factory->create('date', null, array(
+ 'format' => 105,
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testThrowExceptionIfFormatIsInvalid()
+ {
+ $this->factory->create('date', null, array(
+ 'format' => array(),
+ ));
+ }
+
+ public function testSetDataWithDifferentTimezones()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Pacific/Tahiti',
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ ));
+
+ $form->setData('2010-06-02');
+
+ $this->assertEquals('01.06.2010', $form->getViewData());
+ }
+
+ public function testSetDataWithDifferentTimezonesDateTime()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::MEDIUM,
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Pacific/Tahiti',
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ));
+
+ $dateTime = new \DateTime('2010-06-02 America/New_York');
+
+ $form->setData($dateTime);
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ $this->assertEquals('01.06.2010', $form->getViewData());
+ }
+
+ public function testYearsOption()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'years' => array(2010, 2011),
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('2010', '2010', '2010'),
+ new ChoiceView('2011', '2011', '2011'),
+ ), $view['year']->vars['choices']);
+ }
+
+ public function testMonthsOption()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'months' => array(6, 7),
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('6', '6', '06'),
+ new ChoiceView('7', '7', '07'),
+ ), $view['month']->vars['choices']);
+ }
+
+ public function testMonthsOptionShortFormat()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'months' => array(1, 4),
+ 'format' => 'dd.MMM.yy',
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('1', '1', 'Jän'),
+ new ChoiceView('4', '4', 'Apr.')
+ ), $view['month']->vars['choices']);
+ }
+
+ public function testMonthsOptionLongFormat()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'months' => array(1, 4),
+ 'format' => 'dd.MMMM.yy',
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('1', '1', 'Jänner'),
+ new ChoiceView('4', '4', 'April'),
+ ), $view['month']->vars['choices']);
+ }
+
+ public function testMonthsOptionLongFormatWithDifferentTimezone()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'months' => array(1, 4),
+ 'format' => 'dd.MMMM.yy',
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('1', '1', 'Jänner'),
+ new ChoiceView('4', '4', 'April'),
+ ), $view['month']->vars['choices']);
+ }
+
+ public function testIsDayWithinRangeReturnsTrueIfWithin()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'days' => array(6, 7),
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('6', '6', '06'),
+ new ChoiceView('7', '7', '07'),
+ ), $view['day']->vars['choices']);
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfSingleText()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'single_text',
+ ));
+
+ $form->submit('7.6.2010');
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfChoiceAndCompletelyEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'day' => '',
+ 'month' => '',
+ 'year' => '',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfChoiceAndCompletelyFilled()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'day' => '2',
+ 'month' => '6',
+ 'year' => '2010',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsTrueIfChoiceAndDayEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('date', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'day' => '',
+ 'month' => '6',
+ 'year' => '2010',
+ ));
+
+ $this->assertTrue($form->isPartiallyFilled());
+ }
+
+ public function testPassDatePatternToView()
+ {
+ $form = $this->factory->create('date');
+ $view = $form->createView();
+
+ $this->assertSame('{{ day }}{{ month }}{{ year }}', $view->vars['date_pattern']);
+ }
+
+ public function testPassDatePatternToViewDifferentFormat()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => \IntlDateFormatter::LONG,
+ ));
+
+ $view = $form->createView();
+
+ $this->assertSame('{{ day }}{{ month }}{{ year }}', $view->vars['date_pattern']);
+ }
+
+ public function testPassDatePatternToViewDifferentPattern()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => 'MMyyyydd'
+ ));
+
+ $view = $form->createView();
+
+ $this->assertSame('{{ month }}{{ year }}{{ day }}', $view->vars['date_pattern']);
+ }
+
+ public function testPassDatePatternToViewDifferentPatternWithSeparators()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'format' => 'MM*yyyy*dd'
+ ));
+
+ $view = $form->createView();
+
+ $this->assertSame('{{ month }}*{{ year }}*{{ day }}', $view->vars['date_pattern']);
+ }
+
+ public function testDontPassDatePatternIfText()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'single_text',
+ ));
+ $view = $form->createView();
+
+ $this->assertFalse(isset($view->vars['date_pattern']));
+ }
+
+ public function testPassWidgetToView()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'single_text',
+ ));
+ $view = $form->createView();
+
+ $this->assertSame('single_text', $view->vars['widget']);
+ }
+
+ // Bug fix
+ public function testInitializeWithDateTime()
+ {
+ // Throws an exception if "data_class" option is not explicitly set
+ // to null in the type
+ $this->factory->create('date', new \DateTime());
+ }
+
+ public function testSingleTextWidgetShouldUseTheRightInputType()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'single_text',
+ ));
+
+ $view = $form->createView();
+ $this->assertEquals('date', $view->vars['type']);
+ }
+
+ public function testPassDefaultEmptyValueToViewIfNotRequired()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'required' => false,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('', $view['year']->vars['empty_value']);
+ $this->assertSame('', $view['month']->vars['empty_value']);
+ $this->assertSame('', $view['day']->vars['empty_value']);
+ }
+
+ public function testPassNoEmptyValueToViewIfRequired()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'required' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertNull($view['year']->vars['empty_value']);
+ $this->assertNull($view['month']->vars['empty_value']);
+ $this->assertNull($view['day']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsString()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'empty_value' => 'Empty',
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty', $view['year']->vars['empty_value']);
+ $this->assertSame('Empty', $view['month']->vars['empty_value']);
+ $this->assertSame('Empty', $view['day']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsArray()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'month' => 'Empty month',
+ 'day' => 'Empty day',
+ ),
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['year']->vars['empty_value']);
+ $this->assertSame('Empty month', $view['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['day']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddEmptyIfNotRequired()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'required' => false,
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'day' => 'Empty day',
+ ),
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['year']->vars['empty_value']);
+ $this->assertSame('', $view['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['day']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddNullIfRequired()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'required' => true,
+ 'empty_value' => array(
+ 'year' => 'Empty year',
+ 'day' => 'Empty day',
+ ),
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty year', $view['year']->vars['empty_value']);
+ $this->assertNull($view['month']->vars['empty_value']);
+ $this->assertSame('Empty day', $view['day']->vars['empty_value']);
+ }
+
+ public function testPassHtml5TypeIfSingleTextAndHtml5Format()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'single_text',
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('date', $view->vars['type']);
+ }
+
+ public function testDontPassHtml5TypeIfNotHtml5Format()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'single_text',
+ 'format' => \IntlDateFormatter::MEDIUM,
+ ));
+
+ $view = $form->createView();
+ $this->assertFalse(isset($view->vars['type']));
+ }
+
+ public function testDontPassHtml5TypeIfNotSingleText()
+ {
+ $form = $this->factory->create('date', null, array(
+ 'widget' => 'text',
+ ));
+
+ $view = $form->createView();
+ $this->assertFalse(isset($view->vars['type']));
+ }
+
+ public function provideCompoundWidgets()
+ {
+ return array(
+ array('text'),
+ array('choice'),
+ );
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testYearErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('date', null, array(
+ 'widget' => $widget,
+ ));
+ $form['year']->addError($error);
+
+ $this->assertSame(array(), $form['year']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testMonthErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('date', null, array(
+ 'widget' => $widget,
+ ));
+ $form['month']->addError($error);
+
+ $this->assertSame(array(), $form['month']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testDayErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('date', null, array(
+ 'widget' => $widget,
+ ));
+ $form['day']->addError($error);
+
+ $this->assertSame(array(), $form['day']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+class FileTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ // https://github.com/symfony/symfony/pull/5028
+ public function testSetData()
+ {
+ $form = $this->factory->createBuilder('file')->getForm();
+ $data = $this->createUploadedFileMock('abcdef', 'original.jpg', true);
+
+ $form->setData($data);
+
+ $this->assertSame($data, $form->getData());
+ }
+
+ public function testSubmit()
+ {
+ $form = $this->factory->createBuilder('file')->getForm();
+ $data = $this->createUploadedFileMock('abcdef', 'original.jpg', true);
+
+ $form->submit($data);
+
+ $this->assertSame($data, $form->getData());
+ }
+
+ // https://github.com/symfony/symfony/issues/6134
+ public function testSubmitEmpty()
+ {
+ $form = $this->factory->createBuilder('file')->getForm();
+
+ $form->submit(null);
+
+ $this->assertNull($form->getData());
+ }
+
+ public function testDontPassValueToView()
+ {
+ $form = $this->factory->create('file');
+ $form->submit(array(
+ 'file' => $this->createUploadedFileMock('abcdef', 'original.jpg', true),
+ ));
+ $view = $form->createView();
+
+ $this->assertEquals('', $view->vars['value']);
+ }
+
+ private function createUploadedFileMock($name, $originalName, $valid)
+ {
+ $file = $this
+ ->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile')
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+ $file
+ ->expects($this->any())
+ ->method('getBasename')
+ ->will($this->returnValue($name))
+ ;
+ $file
+ ->expects($this->any())
+ ->method('getClientOriginalName')
+ ->will($this->returnValue($originalName))
+ ;
+ $file
+ ->expects($this->any())
+ ->method('isValid')
+ ->will($this->returnValue($valid))
+ ;
+
+ return $file;
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\Form\Form;
+use Symfony\Component\Form\CallbackTransformer;
+use Symfony\Component\Form\Tests\Fixtures\Author;
+use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
+use Symfony\Component\Form\FormError;
+
+class FormTest_AuthorWithoutRefSetter
+{
+ protected $reference;
+
+ protected $referenceCopy;
+
+ public function __construct($reference)
+ {
+ $this->reference = $reference;
+ $this->referenceCopy = $reference;
+ }
+
+ // The returned object should be modified by reference without having
+ // to provide a setReference() method
+ public function getReference()
+ {
+ return $this->reference;
+ }
+
+ // The returned object is a copy, so setReferenceCopy() must be used
+ // to update it
+ public function getReferenceCopy()
+ {
+ return is_object($this->referenceCopy) ? clone $this->referenceCopy : $this->referenceCopy;
+ }
+
+ public function setReferenceCopy($reference)
+ {
+ $this->referenceCopy = $reference;
+ }
+}
+
+class FormTypeTest extends BaseTypeTest
+{
+ public function testCreateFormInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $this->factory->create('form'));
+ }
+
+ public function testPassRequiredAsOption()
+ {
+ $form = $this->factory->create('form', null, array('required' => false));
+
+ $this->assertFalse($form->isRequired());
+
+ $form = $this->factory->create('form', null, array('required' => true));
+
+ $this->assertTrue($form->isRequired());
+ }
+
+ public function testSubmittedDataIsTrimmedBeforeTransforming()
+ {
+ $form = $this->factory->createBuilder('form')
+ ->addViewTransformer(new FixedDataTransformer(array(
+ null => '',
+ 'reverse[a]' => 'a',
+ )))
+ ->setCompound(false)
+ ->getForm();
+
+ $form->submit(' a ');
+
+ $this->assertEquals('a', $form->getViewData());
+ $this->assertEquals('reverse[a]', $form->getData());
+ }
+
+ public function testSubmittedDataIsNotTrimmedBeforeTransformingIfNoTrimming()
+ {
+ $form = $this->factory->createBuilder('form', null, array('trim' => false))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ null => '',
+ 'reverse[ a ]' => ' a ',
+ )))
+ ->setCompound(false)
+ ->getForm();
+
+ $form->submit(' a ');
+
+ $this->assertEquals(' a ', $form->getViewData());
+ $this->assertEquals('reverse[ a ]', $form->getData());
+ }
+
+ public function testNonReadOnlyFormWithReadOnlyParentIsReadOnly()
+ {
+ $view = $this->factory->createNamedBuilder('parent', 'form', null, array('read_only' => true))
+ ->add('child', 'form')
+ ->getForm()
+ ->createView();
+
+ $this->assertTrue($view['child']->vars['read_only']);
+ }
+
+ public function testReadOnlyFormWithNonReadOnlyParentIsReadOnly()
+ {
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('child', 'form', array('read_only' => true))
+ ->getForm()
+ ->createView();
+
+ $this->assertTrue($view['child']->vars['read_only']);
+ }
+
+ public function testNonReadOnlyFormWithNonReadOnlyParentIsNotReadOnly()
+ {
+ $view = $this->factory->createNamedBuilder('parent', 'form')
+ ->add('child', 'form')
+ ->getForm()
+ ->createView();
+
+ $this->assertFalse($view['child']->vars['read_only']);
+ }
+
+ public function testPassMaxLengthToView()
+ {
+ $form = $this->factory->create('form', null, array('max_length' => 10));
+ $view = $form->createView();
+
+ $this->assertSame(10, $view->vars['max_length']);
+ }
+
+ public function testSubmitWithEmptyDataCreatesObjectIfClassAvailable()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ 'required' => false,
+ ));
+ $builder->add('firstName', 'text');
+ $builder->add('lastName', 'text');
+ $form = $builder->getForm();
+
+ $form->setData(null);
+ // partially empty, still an object is created
+ $form->submit(array('firstName' => 'Bernhard', 'lastName' => ''));
+
+ $author = new Author();
+ $author->firstName = 'Bernhard';
+ $author->setLastName('');
+
+ $this->assertEquals($author, $form->getData());
+ }
+
+ public function testSubmitWithEmptyDataCreatesObjectIfInitiallySubmittedWithObject()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ // data class is inferred from the passed object
+ 'data' => new Author(),
+ 'required' => false,
+ ));
+ $builder->add('firstName', 'text');
+ $builder->add('lastName', 'text');
+ $form = $builder->getForm();
+
+ $form->setData(null);
+ // partially empty, still an object is created
+ $form->submit(array('firstName' => 'Bernhard', 'lastName' => ''));
+
+ $author = new Author();
+ $author->firstName = 'Bernhard';
+ $author->setLastName('');
+
+ $this->assertEquals($author, $form->getData());
+ }
+
+ public function testSubmitWithEmptyDataCreatesArrayIfDataClassIsNull()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'data_class' => null,
+ 'required' => false,
+ ));
+ $builder->add('firstName', 'text');
+ $form = $builder->getForm();
+
+ $form->setData(null);
+ $form->submit(array('firstName' => 'Bernhard'));
+
+ $this->assertSame(array('firstName' => 'Bernhard'), $form->getData());
+ }
+
+ public function testSubmitEmptyWithEmptyDataCreatesNoObjectIfNotRequired()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ 'required' => false,
+ ));
+ $builder->add('firstName', 'text');
+ $builder->add('lastName', 'text');
+ $form = $builder->getForm();
+
+ $form->setData(null);
+ $form->submit(array('firstName' => '', 'lastName' => ''));
+
+ $this->assertNull($form->getData());
+ }
+
+ public function testSubmitEmptyWithEmptyDataCreatesObjectIfRequired()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ 'required' => true,
+ ));
+ $builder->add('firstName', 'text');
+ $builder->add('lastName', 'text');
+ $form = $builder->getForm();
+
+ $form->setData(null);
+ $form->submit(array('firstName' => '', 'lastName' => ''));
+
+ $this->assertEquals(new Author(), $form->getData());
+ }
+
+ /*
+ * We need something to write the field values into
+ */
+ public function testSubmitWithEmptyDataStoresArrayIfNoClassAvailable()
+ {
+ $form = $this->factory->createBuilder('form')
+ ->add('firstName', 'text')
+ ->getForm();
+
+ $form->setData(null);
+ $form->submit(array('firstName' => 'Bernhard'));
+
+ $this->assertSame(array('firstName' => 'Bernhard'), $form->getData());
+ }
+
+ public function testSubmitWithEmptyDataPassesEmptyStringToTransformerIfNotCompound()
+ {
+ $form = $this->factory->createBuilder('form')
+ ->addViewTransformer(new FixedDataTransformer(array(
+ // required for the initial, internal setData(null)
+ null => 'null',
+ // required to test that submit(null) is converted to ''
+ 'empty' => '',
+ )))
+ ->setCompound(false)
+ ->getForm();
+
+ $form->submit(null);
+
+ $this->assertSame('empty', $form->getData());
+ }
+
+ public function testSubmitWithEmptyDataUsesEmptyDataOption()
+ {
+ $author = new Author();
+
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ 'empty_data' => $author,
+ ));
+ $builder->add('firstName', 'text');
+ $form = $builder->getForm();
+
+ $form->submit(array('firstName' => 'Bernhard'));
+
+ $this->assertSame($author, $form->getData());
+ $this->assertEquals('Bernhard', $author->firstName);
+ }
+
+ public function provideZeros()
+ {
+ return array(
+ array(0, '0'),
+ array('0', '0'),
+ array('00000', '00000'),
+ );
+ }
+
+ /**
+ * @dataProvider provideZeros
+ * @see https://github.com/symfony/symfony/issues/1986
+ */
+ public function testSetDataThroughParamsWithZero($data, $dataAsString)
+ {
+ $form = $this->factory->create('form', null, array(
+ 'data' => $data,
+ 'compound' => false,
+ ));
+ $view = $form->createView();
+
+ $this->assertFalse($form->isEmpty());
+
+ $this->assertSame($dataAsString, $view->vars['value']);
+ $this->assertSame($dataAsString, $form->getData());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testAttributesException()
+ {
+ $this->factory->create('form', null, array('attr' => ''));
+ }
+
+ public function testNameCanBeEmptyString()
+ {
+ $form = $this->factory->createNamed('', 'form');
+
+ $this->assertEquals('', $form->getName());
+ }
+
+ public function testSubformDoesntCallSetters()
+ {
+ $author = new FormTest_AuthorWithoutRefSetter(new Author());
+
+ $builder = $this->factory->createBuilder('form', $author);
+ $builder->add('reference', 'form', array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ ));
+ $builder->get('reference')->add('firstName', 'text');
+ $form = $builder->getForm();
+
+ $form->submit(array(
+ // reference has a getter, but not setter
+ 'reference' => array(
+ 'firstName' => 'Foo',
+ )
+ ));
+
+ $this->assertEquals('Foo', $author->getReference()->firstName);
+ }
+
+ public function testSubformCallsSettersIfTheObjectChanged()
+ {
+ // no reference
+ $author = new FormTest_AuthorWithoutRefSetter(null);
+ $newReference = new Author();
+
+ $builder = $this->factory->createBuilder('form', $author);
+ $builder->add('referenceCopy', 'form', array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ ));
+ $builder->get('referenceCopy')->add('firstName', 'text');
+ $form = $builder->getForm();
+
+ $form['referenceCopy']->setData($newReference); // new author object
+
+ $form->submit(array(
+ // referenceCopy has a getter that returns a copy
+ 'referenceCopy' => array(
+ 'firstName' => 'Foo',
+ )
+ ));
+
+ $this->assertEquals('Foo', $author->getReferenceCopy()->firstName);
+ }
+
+ public function testSubformCallsSettersIfByReferenceIsFalse()
+ {
+ $author = new FormTest_AuthorWithoutRefSetter(new Author());
+
+ $builder = $this->factory->createBuilder('form', $author);
+ $builder->add('referenceCopy', 'form', array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ 'by_reference' => false
+ ));
+ $builder->get('referenceCopy')->add('firstName', 'text');
+ $form = $builder->getForm();
+
+ $form->submit(array(
+ // referenceCopy has a getter that returns a copy
+ 'referenceCopy' => array(
+ 'firstName' => 'Foo',
+ )
+ ));
+
+ // firstName can only be updated if setReferenceCopy() was called
+ $this->assertEquals('Foo', $author->getReferenceCopy()->firstName);
+ }
+
+ public function testSubformCallsSettersIfReferenceIsScalar()
+ {
+ $author = new FormTest_AuthorWithoutRefSetter('scalar');
+
+ $builder = $this->factory->createBuilder('form', $author);
+ $builder->add('referenceCopy', 'form');
+ $builder->get('referenceCopy')->addViewTransformer(new CallbackTransformer(
+ function () {},
+ function ($value) { // reverseTransform
+
+ return 'foobar';
+ }
+ ));
+ $form = $builder->getForm();
+
+ $form->submit(array(
+ 'referenceCopy' => array(), // doesn't matter actually
+ ));
+
+ // firstName can only be updated if setReferenceCopy() was called
+ $this->assertEquals('foobar', $author->getReferenceCopy());
+ }
+
+ public function testSubformAlwaysInsertsIntoArrays()
+ {
+ $ref1 = new Author();
+ $ref2 = new Author();
+ $author = array('referenceCopy' => $ref1);
+
+ $builder = $this->factory->createBuilder('form');
+ $builder->setData($author);
+ $builder->add('referenceCopy', 'form');
+ $builder->get('referenceCopy')->addViewTransformer(new CallbackTransformer(
+ function () {},
+ function ($value) use ($ref2) { // reverseTransform
+
+ return $ref2;
+ }
+ ));
+ $form = $builder->getForm();
+
+ $form->submit(array(
+ 'referenceCopy' => array('a' => 'b'), // doesn't matter actually
+ ));
+
+ // the new reference was inserted into the array
+ $author = $form->getData();
+ $this->assertSame($ref2, $author['referenceCopy']);
+ }
+
+ public function testPassMultipartTrueIfAnyChildIsMultipartToView()
+ {
+ $view = $this->factory->createBuilder('form')
+ ->add('foo', 'text')
+ ->add('bar', 'file')
+ ->getForm()
+ ->createView();
+
+ $this->assertTrue($view->vars['multipart']);
+ }
+
+ public function testViewIsNotRenderedByDefault()
+ {
+ $view = $this->factory->createBuilder('form')
+ ->add('foo', 'form')
+ ->getForm()
+ ->createView();
+
+ $this->assertFalse($view->isRendered());
+ }
+
+ public function testErrorBubblingIfCompound()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'compound' => true,
+ ));
+
+ $this->assertTrue($form->getConfig()->getErrorBubbling());
+ }
+
+ public function testNoErrorBubblingIfNotCompound()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'compound' => false,
+ ));
+
+ $this->assertFalse($form->getConfig()->getErrorBubbling());
+ }
+
+ public function testOverrideErrorBubbling()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'compound' => false,
+ 'error_bubbling' => true,
+ ));
+
+ $this->assertTrue($form->getConfig()->getErrorBubbling());
+ }
+
+ public function testPropertyPath()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'property_path' => 'foo',
+ ));
+
+ $this->assertEquals(new PropertyPath('foo'), $form->getPropertyPath());
+ $this->assertTrue($form->getConfig()->getMapped());
+ }
+
+ public function testPropertyPathNullImpliesDefault()
+ {
+ $form = $this->factory->createNamed('name', 'form', null, array(
+ 'property_path' => null,
+ ));
+
+ $this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
+ $this->assertTrue($form->getConfig()->getMapped());
+ }
+
+ public function testNotMapped()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'property_path' => 'foo',
+ 'mapped' => false,
+ ));
+
+ $this->assertEquals(new PropertyPath('foo'), $form->getPropertyPath());
+ $this->assertFalse($form->getConfig()->getMapped());
+ }
+
+ public function testViewValidNotSubmitted()
+ {
+ $form = $this->factory->create('form');
+ $view = $form->createView();
+ $this->assertTrue($view->vars['valid']);
+ }
+
+ public function testViewNotValidSubmitted()
+ {
+ $form = $this->factory->create('form');
+ $form->submit(array());
+ $form->addError(new FormError('An error'));
+ $view = $form->createView();
+ $this->assertFalse($view->vars['valid']);
+ }
+
+ public function testDataOptionSupersedesSetDataCalls()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'data' => 'default',
+ 'compound' => false,
+ ));
+
+ $form->setData('foobar');
+
+ $this->assertSame('default', $form->getData());
+ }
+
+ public function testNormDataIsPassedToView()
+ {
+ $view = $this->factory->createBuilder('form')
+ ->addViewTransformer(new FixedDataTransformer(array(
+ 'foo' => 'bar',
+ )))
+ ->setData('foo')
+ ->getForm()
+ ->createView();
+
+ $this->assertSame('foo', $view->vars['data']);
+ $this->assertSame('bar', $view->vars['value']);
+ }
+
+ // https://github.com/symfony/symfony/issues/6862
+ public function testPassZeroLabelToView()
+ {
+ $view = $this->factory->create('form', null, array(
+ 'label' => '0'
+ ))
+ ->createView();
+
+ $this->assertSame('0', $view->vars['label']);
+ }
+
+ protected function getTestedType()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class IntegerTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testSubmitCastsToInteger()
+ {
+ $form = $this->factory->create('integer');
+
+ $form->submit('1.678');
+
+ $this->assertSame(1, $form->getData());
+ $this->assertSame('1', $form->getViewData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class LanguageTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testCountriesAreSelectable()
+ {
+ $form = $this->factory->create('language');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'British English'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('en_US', 'en_US', 'U.S. English'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('fr', 'fr', 'French'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('my', 'my', 'Burmese'), $choices, '', false, false);
+ }
+
+ public function testMultipleLanguagesIsNotIncluded()
+ {
+ $form = $this->factory->create('language', 'language');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ $this->assertNotContains(new ChoiceView('mul', 'mul', 'Mehrsprachig'), $choices, '', false, false);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class LocaleTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testLocalesAreSelectable()
+ {
+ $form = $this->factory->create('locale');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ $this->assertContains(new ChoiceView('en', 'en', 'English'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('en_GB', 'en_GB', 'English (United Kingdom)'), $choices, '', false, false);
+ $this->assertContains(new ChoiceView('zh_Hant_MO', 'zh_Hant_MO', 'Chinese (Traditional, Macau SAR China)'), $choices, '', false, false);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class MoneyTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ // we test against different locales, so we need the full
+ // implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testPassMoneyPatternToView()
+ {
+ \Locale::setDefault('de_DE');
+
+ $form = $this->factory->create('money');
+ $view = $form->createView();
+
+ $this->assertSame('{{ widget }} €', $view->vars['money_pattern']);
+ }
+
+ public function testMoneyPatternWorksForYen()
+ {
+ \Locale::setDefault('en_US');
+
+ $form = $this->factory->create('money', null, array('currency' => 'JPY'));
+ $view = $form->createView();
+ $this->assertTrue((Boolean) strstr($view->vars['money_pattern'], '¥'));
+ }
+
+ // https://github.com/symfony/symfony/issues/5458
+ public function testPassDifferentPatternsForDifferentCurrencies()
+ {
+ \Locale::setDefault('de_DE');
+
+ $form1 = $this->factory->create('money', null, array('currency' => 'GBP'));
+ $form2 = $this->factory->create('money', null, array('currency' => 'EUR'));
+ $view1 = $form1->createView();
+ $view2 = $form2->createView();
+
+ $this->assertSame('{{ widget }} £', $view1->vars['money_pattern']);
+ $this->assertSame('{{ widget }} €', $view2->vars['money_pattern']);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class NumberTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ // we test against "de_DE", so we need the full implementation
+ IntlTestHelper::requireFullIntl($this);
+
+ \Locale::setDefault('de_DE');
+ }
+
+ public function testDefaultFormatting()
+ {
+ $form = $this->factory->create('number');
+ $form->setData('12345.67890');
+ $view = $form->createView();
+
+ $this->assertSame('12345,679', $view->vars['value']);
+ }
+
+ public function testDefaultFormattingWithGrouping()
+ {
+ $form = $this->factory->create('number', null, array('grouping' => true));
+ $form->setData('12345.67890');
+ $view = $form->createView();
+
+ $this->assertSame('12.345,679', $view->vars['value']);
+ }
+
+ public function testDefaultFormattingWithPrecision()
+ {
+ $form = $this->factory->create('number', null, array('precision' => 2));
+ $form->setData('12345.67890');
+ $view = $form->createView();
+
+ $this->assertSame('12345,68', $view->vars['value']);
+ }
+
+ public function testDefaultFormattingWithRounding()
+ {
+ $form = $this->factory->create('number', null, array('precision' => 0, 'rounding_mode' => \NumberFormatter::ROUND_UP));
+ $form->setData('12345.54321');
+ $view = $form->createView();
+
+ $this->assertSame('12346', $view->vars['value']);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+class PasswordTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ public function testEmptyIfNotSubmitted()
+ {
+ $form = $this->factory->create('password');
+ $form->setData('pAs5w0rd');
+ $view = $form->createView();
+
+ $this->assertSame('', $view->vars['value']);
+ }
+
+ public function testEmptyIfSubmitted()
+ {
+ $form = $this->factory->create('password');
+ $form->submit('pAs5w0rd');
+ $view = $form->createView();
+
+ $this->assertSame('', $view->vars['value']);
+ }
+
+ public function testNotEmptyIfSubmittedAndNotAlwaysEmpty()
+ {
+ $form = $this->factory->create('password', null, array('always_empty' => false));
+ $form->submit('pAs5w0rd');
+ $view = $form->createView();
+
+ $this->assertSame('pAs5w0rd', $view->vars['value']);
+ }
+
+ public function testNotTrimmed()
+ {
+ $form = $this->factory->create('password', null);
+ $form->submit(' pAs5w0rd ');
+ $data = $form->getData();
+
+ $this->assertSame(' pAs5w0rd ', $data);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+class RepeatedTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ protected $form;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->form = $this->factory->create('repeated', null, array(
+ 'type' => 'text',
+ ));
+ $this->form->setData(null);
+ }
+
+ public function testSetData()
+ {
+ $this->form->setData('foobar');
+
+ $this->assertEquals('foobar', $this->form['first']->getData());
+ $this->assertEquals('foobar', $this->form['second']->getData());
+ }
+
+ public function testSetOptions()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'type' => 'text',
+ 'options' => array('label' => 'Global'),
+ ));
+
+ $this->assertEquals('Global', $form['first']->getConfig()->getOption('label'));
+ $this->assertEquals('Global', $form['second']->getConfig()->getOption('label'));
+ $this->assertTrue($form['first']->isRequired());
+ $this->assertTrue($form['second']->isRequired());
+ }
+
+ public function testSetOptionsPerChild()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ // the global required value cannot be overridden
+ 'type' => 'text',
+ 'first_options' => array('label' => 'Test', 'required' => false),
+ 'second_options' => array('label' => 'Test2')
+ ));
+
+ $this->assertEquals('Test', $form['first']->getConfig()->getOption('label'));
+ $this->assertEquals('Test2', $form['second']->getConfig()->getOption('label'));
+ $this->assertTrue($form['first']->isRequired());
+ $this->assertTrue($form['second']->isRequired());
+ }
+
+ public function testSetRequired()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'required' => false,
+ 'type' => 'text',
+ ));
+
+ $this->assertFalse($form['first']->isRequired());
+ $this->assertFalse($form['second']->isRequired());
+ }
+
+ public function testSetErrorBubblingToTrue()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'error_bubbling' => true,
+ ));
+
+ $this->assertTrue($form->getConfig()->getOption('error_bubbling'));
+ $this->assertTrue($form['first']->getConfig()->getOption('error_bubbling'));
+ $this->assertTrue($form['second']->getConfig()->getOption('error_bubbling'));
+ }
+
+ public function testSetErrorBubblingToFalse()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'error_bubbling' => false,
+ ));
+
+ $this->assertFalse($form->getConfig()->getOption('error_bubbling'));
+ $this->assertFalse($form['first']->getConfig()->getOption('error_bubbling'));
+ $this->assertFalse($form['second']->getConfig()->getOption('error_bubbling'));
+ }
+
+ public function testSetErrorBubblingIndividually()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'error_bubbling' => true,
+ 'options' => array('error_bubbling' => false),
+ 'second_options' => array('error_bubbling' => true),
+ ));
+
+ $this->assertTrue($form->getConfig()->getOption('error_bubbling'));
+ $this->assertFalse($form['first']->getConfig()->getOption('error_bubbling'));
+ $this->assertTrue($form['second']->getConfig()->getOption('error_bubbling'));
+ }
+
+ public function testSetOptionsPerChildAndOverwrite()
+ {
+ $form = $this->factory->create('repeated', null, array(
+ 'type' => 'text',
+ 'options' => array('label' => 'Label'),
+ 'second_options' => array('label' => 'Second label')
+ ));
+
+ $this->assertEquals('Label', $form['first']->getConfig()->getOption('label'));
+ $this->assertEquals('Second label', $form['second']->getConfig()->getOption('label'));
+ $this->assertTrue($form['first']->isRequired());
+ $this->assertTrue($form['second']->isRequired());
+ }
+
+ public function testSubmitUnequal()
+ {
+ $input = array('first' => 'foo', 'second' => 'bar');
+
+ $this->form->submit($input);
+
+ $this->assertEquals('foo', $this->form['first']->getViewData());
+ $this->assertEquals('bar', $this->form['second']->getViewData());
+ $this->assertFalse($this->form->isSynchronized());
+ $this->assertEquals($input, $this->form->getViewData());
+ $this->assertNull($this->form->getData());
+ }
+
+ public function testSubmitEqual()
+ {
+ $input = array('first' => 'foo', 'second' => 'foo');
+
+ $this->form->submit($input);
+
+ $this->assertEquals('foo', $this->form['first']->getViewData());
+ $this->assertEquals('foo', $this->form['second']->getViewData());
+ $this->assertTrue($this->form->isSynchronized());
+ $this->assertEquals($input, $this->form->getViewData());
+ $this->assertEquals('foo', $this->form->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SubmitTypeTest extends TypeTestCase
+{
+ public function testCreateSubmitButtonInstances()
+ {
+ $this->assertInstanceOf('Symfony\Component\Form\SubmitButton', $this->factory->create('submit'));
+ }
+
+ public function testNotClickedByDefault()
+ {
+ $button = $this->factory->create('submit');
+
+ $this->assertFalse($button->isClicked());
+ }
+
+ public function testNotClickedIfSubmittedWithNull()
+ {
+ $button = $this->factory->create('submit');
+ $button->submit(null);
+
+ $this->assertFalse($button->isClicked());
+ }
+
+ public function testClickedIfSubmittedWithEmptyString()
+ {
+ $button = $this->factory->create('submit');
+ $button->submit('');
+
+ $this->assertTrue($button->isClicked());
+ }
+
+ public function testClickedIfSubmittedWithUnemptyString()
+ {
+ $button = $this->factory->create('submit');
+ $button->submit('foo');
+
+ $this->assertTrue($button->isClicked());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+class TimeTypeTest extends TypeTestCase
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testSubmitDateTime()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'datetime',
+ ));
+
+ $input = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ );
+
+ $form->submit($input);
+
+ $dateTime = new \DateTime('1970-01-01 03:04:00 UTC');
+
+ $this->assertEquals($dateTime, $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
+ public function testSubmitString()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ ));
+
+ $input = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ );
+
+ $form->submit($input);
+
+ $this->assertEquals('03:04:00', $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
+ public function testSubmitTimestamp()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'timestamp',
+ ));
+
+ $input = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ );
+
+ $form->submit($input);
+
+ $dateTime = new \DateTime('1970-01-01 03:04:00 UTC');
+
+ $this->assertEquals($dateTime->format('U'), $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
+ public function testSubmitArray()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'array',
+ ));
+
+ $input = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ );
+
+ $form->submit($input);
+
+ $this->assertEquals($input, $form->getData());
+ $this->assertEquals($input, $form->getViewData());
+ }
+
+ public function testSubmitDatetimeSingleText()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ));
+
+ $form->submit('03:04');
+
+ $this->assertEquals(new \DateTime('1970-01-01 03:04:00 UTC'), $form->getData());
+ $this->assertEquals('03:04', $form->getViewData());
+ }
+
+ public function testSubmitDatetimeSingleTextWithoutMinutes()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ 'with_minutes' => false,
+ ));
+
+ $form->submit('03');
+
+ $this->assertEquals(new \DateTime('1970-01-01 03:00:00 UTC'), $form->getData());
+ $this->assertEquals('03', $form->getViewData());
+ }
+
+ public function testSubmitArraySingleText()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'array',
+ 'widget' => 'single_text',
+ ));
+
+ $data = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ );
+
+ $form->submit('03:04');
+
+ $this->assertEquals($data, $form->getData());
+ $this->assertEquals('03:04', $form->getViewData());
+ }
+
+ public function testSubmitArraySingleTextWithoutMinutes()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'array',
+ 'widget' => 'single_text',
+ 'with_minutes' => false,
+ ));
+
+ $data = array(
+ 'hour' => '3',
+ );
+
+ $form->submit('03');
+
+ $this->assertEquals($data, $form->getData());
+ $this->assertEquals('03', $form->getViewData());
+ }
+
+ public function testSubmitArraySingleTextWithSeconds()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'array',
+ 'widget' => 'single_text',
+ 'with_seconds' => true,
+ ));
+
+ $data = array(
+ 'hour' => '3',
+ 'minute' => '4',
+ 'second' => '5',
+ );
+
+ $form->submit('03:04:05');
+
+ $this->assertEquals($data, $form->getData());
+ $this->assertEquals('03:04:05', $form->getViewData());
+ }
+
+ public function testSubmitStringSingleText()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ ));
+
+ $form->submit('03:04');
+
+ $this->assertEquals('03:04:00', $form->getData());
+ $this->assertEquals('03:04', $form->getViewData());
+ }
+
+ public function testSubmitStringSingleTextWithoutMinutes()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'string',
+ 'widget' => 'single_text',
+ 'with_minutes' => false,
+ ));
+
+ $form->submit('03');
+
+ $this->assertEquals('03:00:00', $form->getData());
+ $this->assertEquals('03', $form->getViewData());
+ }
+
+ public function testSetDataWithoutMinutes()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'datetime',
+ 'with_minutes' => false,
+ ));
+
+ $form->setData(new \DateTime('03:04:05 UTC'));
+
+ $this->assertEquals(array('hour' => 3), $form->getViewData());
+ }
+
+ public function testSetDataWithSeconds()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'UTC',
+ 'view_timezone' => 'UTC',
+ 'input' => 'datetime',
+ 'with_seconds' => true,
+ ));
+
+ $form->setData(new \DateTime('03:04:05 UTC'));
+
+ $this->assertEquals(array('hour' => 3, 'minute' => 4, 'second' => 5), $form->getViewData());
+ }
+
+ public function testSetDataDifferentTimezones()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Asia/Hong_Kong',
+ 'input' => 'string',
+ 'with_seconds' => true,
+ ));
+
+ $dateTime = new \DateTime('2013-01-01 12:04:05');
+ $dateTime->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $form->setData($dateTime->format('H:i:s'));
+
+ $outputTime = clone $dateTime;
+ $outputTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $displayedData = array(
+ 'hour' => (int) $outputTime->format('H'),
+ 'minute' => (int) $outputTime->format('i'),
+ 'second' => (int) $outputTime->format('s')
+ );
+
+ $this->assertEquals($displayedData, $form->getViewData());
+ }
+
+ public function testSetDataDifferentTimezonesDateTime()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'model_timezone' => 'America/New_York',
+ 'view_timezone' => 'Asia/Hong_Kong',
+ 'input' => 'datetime',
+ 'with_seconds' => true,
+ ));
+
+ $dateTime = new \DateTime('12:04:05');
+ $dateTime->setTimezone(new \DateTimeZone('America/New_York'));
+
+ $form->setData($dateTime);
+
+ $outputTime = clone $dateTime;
+ $outputTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong'));
+
+ $displayedData = array(
+ 'hour' => (int) $outputTime->format('H'),
+ 'minute' => (int) $outputTime->format('i'),
+ 'second' => (int) $outputTime->format('s')
+ );
+
+ $this->assertDateTimeEquals($dateTime, $form->getData());
+ $this->assertEquals($displayedData, $form->getViewData());
+ }
+
+ public function testHoursOption()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'hours' => array(6, 7),
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('6', '6', '06'),
+ new ChoiceView('7', '7', '07'),
+ ), $view['hour']->vars['choices']);
+ }
+
+ public function testIsMinuteWithinRangeReturnsTrueIfWithin()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'minutes' => array(6, 7),
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('6', '6', '06'),
+ new ChoiceView('7', '7', '07'),
+ ), $view['minute']->vars['choices']);
+ }
+
+ public function testIsSecondWithinRangeReturnsTrueIfWithin()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'seconds' => array(6, 7),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+
+ $this->assertEquals(array(
+ new ChoiceView('6', '6', '06'),
+ new ChoiceView('7', '7', '07'),
+ ), $view['second']->vars['choices']);
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfCompletelyEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'hour' => '',
+ 'minute' => '',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfCompletelyEmptyWithSeconds()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit(array(
+ 'hour' => '',
+ 'minute' => '',
+ 'second' => '',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfCompletelyFilled()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ ));
+
+ $form->submit(array(
+ 'hour' => '0',
+ 'minute' => '0',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsFalseIfCompletelyFilledWithSeconds()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit(array(
+ 'hour' => '0',
+ 'minute' => '0',
+ 'second' => '0',
+ ));
+
+ $this->assertFalse($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsTrueIfChoiceAndHourEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit(array(
+ 'hour' => '',
+ 'minute' => '0',
+ 'second' => '0',
+ ));
+
+ $this->assertTrue($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsTrueIfChoiceAndMinuteEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit(array(
+ 'hour' => '0',
+ 'minute' => '',
+ 'second' => '0',
+ ));
+
+ $this->assertTrue($form->isPartiallyFilled());
+ }
+
+ public function testIsPartiallyFilledReturnsTrueIfChoiceAndSecondsEmpty()
+ {
+ $this->markTestIncomplete('Needs to be reimplemented using validators');
+
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'choice',
+ 'with_seconds' => true,
+ ));
+
+ $form->submit(array(
+ 'hour' => '0',
+ 'minute' => '0',
+ 'second' => '',
+ ));
+
+ $this->assertTrue($form->isPartiallyFilled());
+ }
+
+ // Bug fix
+ public function testInitializeWithDateTime()
+ {
+ // Throws an exception if "data_class" option is not explicitly set
+ // to null in the type
+ $this->factory->create('time', new \DateTime());
+ }
+
+ public function testSingleTextWidgetShouldUseTheRightInputType()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'widget' => 'single_text',
+ ));
+
+ $view = $form->createView();
+ $this->assertEquals('time', $view->vars['type']);
+ }
+
+ public function testPassDefaultEmptyValueToViewIfNotRequired()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'required' => false,
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('', $view['hour']->vars['empty_value']);
+ $this->assertSame('', $view['minute']->vars['empty_value']);
+ $this->assertSame('', $view['second']->vars['empty_value']);
+ }
+
+ public function testPassNoEmptyValueToViewIfRequired()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'required' => true,
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertNull($view['hour']->vars['empty_value']);
+ $this->assertNull($view['minute']->vars['empty_value']);
+ $this->assertNull($view['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsString()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'empty_value' => 'Empty',
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty', $view['hour']->vars['empty_value']);
+ $this->assertSame('Empty', $view['minute']->vars['empty_value']);
+ $this->assertSame('Empty', $view['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsArray()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'empty_value' => array(
+ 'hour' => 'Empty hour',
+ 'minute' => 'Empty minute',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty hour', $view['hour']->vars['empty_value']);
+ $this->assertSame('Empty minute', $view['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddEmptyIfNotRequired()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'required' => false,
+ 'empty_value' => array(
+ 'hour' => 'Empty hour',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty hour', $view['hour']->vars['empty_value']);
+ $this->assertSame('', $view['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['second']->vars['empty_value']);
+ }
+
+ public function testPassEmptyValueAsPartialArrayAddNullIfRequired()
+ {
+ $form = $this->factory->create('time', null, array(
+ 'required' => true,
+ 'empty_value' => array(
+ 'hour' => 'Empty hour',
+ 'second' => 'Empty second',
+ ),
+ 'with_seconds' => true,
+ ));
+
+ $view = $form->createView();
+ $this->assertSame('Empty hour', $view['hour']->vars['empty_value']);
+ $this->assertNull($view['minute']->vars['empty_value']);
+ $this->assertSame('Empty second', $view['second']->vars['empty_value']);
+ }
+
+ public function provideCompoundWidgets()
+ {
+ return array(
+ array('text'),
+ array('choice'),
+ );
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testHourErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('time', null, array(
+ 'widget' => $widget,
+ ));
+ $form['hour']->addError($error);
+
+ $this->assertSame(array(), $form['hour']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testMinuteErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('time', null, array(
+ 'widget' => $widget,
+ ));
+ $form['minute']->addError($error);
+
+ $this->assertSame(array(), $form['minute']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ /**
+ * @dataProvider provideCompoundWidgets
+ */
+ public function testSecondErrorsBubbleUp($widget)
+ {
+ $error = new FormError('Invalid!');
+ $form = $this->factory->create('time', null, array(
+ 'widget' => $widget,
+ 'with_seconds' => true,
+ ));
+ $form['second']->addError($error);
+
+ $this->assertSame(array(), $form['second']->getErrors());
+ $this->assertSame(array($error), $form->getErrors());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\InvalidConfigurationException
+ */
+ public function testInitializeWithSecondsAndWithoutMinutes()
+ {
+ $this->factory->create('time', null, array(
+ 'with_minutes' => false,
+ 'with_seconds' => true,
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+class TimezoneTypeTest extends \Symfony\Component\Form\Test\TypeTestCase
+{
+ public function testTimezonesAreSelectable()
+ {
+ $form = $this->factory->create('timezone');
+ $view = $form->createView();
+ $choices = $view->vars['choices'];
+
+ $this->assertArrayHasKey('Africa', $choices);
+ $this->assertContains(new ChoiceView('Africa/Kinshasa', 'Africa/Kinshasa', 'Kinshasa'), $choices['Africa'], '', false, false);
+
+ $this->assertArrayHasKey('America', $choices);
+ $this->assertContains(new ChoiceView('America/New_York', 'America/New_York', 'New York'), $choices['America'], '', false, false);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Core\Type;
+
+use Symfony\Component\Form\Test\TypeTestCase as BaseTypeTestCase;
+
+/**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use Symfony\Component\Form\Test\TypeTestCase instead.
+ */
+abstract class TypeTestCase extends BaseTypeTestCase
+{
+}
--- /dev/null
+<?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\Form\Tests\Extension\Core\Type;
+
+class UrlTypeTest extends TypeTestCase
+{
+ public function testSubmitAddsDefaultProtocolIfNoneIsIncluded()
+ {
+ $form = $this->factory->create('url', 'name');
+
+ $form->submit('www.domain.com');
+
+ $this->assertSame('http://www.domain.com', $form->getData());
+ $this->assertSame('http://www.domain.com', $form->getViewData());
+ }
+
+ public function testSubmitAddsNoDefaultProtocolIfAlreadyIncluded()
+ {
+ $form = $this->factory->create('url', null, array(
+ 'default_protocol' => 'http',
+ ));
+
+ $form->submit('ftp://www.domain.com');
+
+ $this->assertSame('ftp://www.domain.com', $form->getData());
+ $this->assertSame('ftp://www.domain.com', $form->getViewData());
+ }
+
+ public function testSubmitAddsNoDefaultProtocolIfEmpty()
+ {
+ $form = $this->factory->create('url', null, array(
+ 'default_protocol' => 'http',
+ ));
+
+ $form->submit('');
+
+ $this->assertNull($form->getData());
+ $this->assertSame('', $form->getViewData());
+ }
+
+ public function testSubmitAddsNoDefaultProtocolIfSetToNull()
+ {
+ $form = $this->factory->create('url', null, array(
+ 'default_protocol' => null,
+ ));
+
+ $form->submit('www.domain.com');
+
+ $this->assertSame('www.domain.com', $form->getData());
+ $this->assertSame('www.domain.com', $form->getViewData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Csrf\CsrfProvider;
+
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\DefaultCsrfProvider;
+
+/**
+ * @runTestsInSeparateProcesses
+ */
+class DefaultCsrfProviderTest extends \PHPUnit_Framework_TestCase
+{
+ protected $provider;
+
+ public static function setUpBeforeClass()
+ {
+ ini_set('session.save_handler', 'files');
+ ini_set('session.save_path', sys_get_temp_dir());
+ }
+
+ protected function setUp()
+ {
+ $this->provider = new DefaultCsrfProvider('SECRET');
+ }
+
+ protected function tearDown()
+ {
+ $this->provider = null;
+ }
+
+ public function testGenerateCsrfToken()
+ {
+ session_start();
+
+ $token = $this->provider->generateCsrfToken('foo');
+
+ $this->assertEquals(sha1('SECRET'.'foo'.session_id()), $token);
+ }
+
+ public function testGenerateCsrfTokenOnUnstartedSession()
+ {
+ session_id('touti');
+
+ if (!version_compare(PHP_VERSION, '5.4', '>=')) {
+ $this->markTestSkipped('This test requires PHP >= 5.4');
+ }
+
+ $this->assertSame(PHP_SESSION_NONE, session_status());
+
+ $token = $this->provider->generateCsrfToken('foo');
+
+ $this->assertEquals(sha1('SECRET'.'foo'.session_id()), $token);
+ $this->assertSame(PHP_SESSION_ACTIVE, session_status());
+ }
+
+ public function testIsCsrfTokenValidSucceeds()
+ {
+ session_start();
+
+ $token = sha1('SECRET'.'foo'.session_id());
+
+ $this->assertTrue($this->provider->isCsrfTokenValid('foo', $token));
+ }
+
+ public function testIsCsrfTokenValidFails()
+ {
+ session_start();
+
+ $token = sha1('SECRET'.'bar'.session_id());
+
+ $this->assertFalse($this->provider->isCsrfTokenValid('foo', $token));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Csrf\CsrfProvider;
+
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider;
+
+class SessionCsrfProviderTest extends \PHPUnit_Framework_TestCase
+{
+ protected $provider;
+ protected $session;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Session\Session')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $this->session = $this->getMock(
+ 'Symfony\Component\HttpFoundation\Session\Session',
+ array(),
+ array(),
+ '',
+ false // don't call constructor
+ );
+ $this->provider = new SessionCsrfProvider($this->session, 'SECRET');
+ }
+
+ protected function tearDown()
+ {
+ $this->provider = null;
+ $this->session = null;
+ }
+
+ public function testGenerateCsrfToken()
+ {
+ $this->session->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('ABCDEF'));
+
+ $token = $this->provider->generateCsrfToken('foo');
+
+ $this->assertEquals(sha1('SECRET'.'foo'.'ABCDEF'), $token);
+ }
+
+ public function testIsCsrfTokenValidSucceeds()
+ {
+ $this->session->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('ABCDEF'));
+
+ $token = sha1('SECRET'.'foo'.'ABCDEF');
+
+ $this->assertTrue($this->provider->isCsrfTokenValid('foo', $token));
+ }
+
+ public function testIsCsrfTokenValidFails()
+ {
+ $this->session->expects($this->once())
+ ->method('getId')
+ ->will($this->returnValue('ABCDEF'));
+
+ $token = sha1('SECRET'.'bar'.'ABCDEF');
+
+ $this->assertFalse($this->provider->isCsrfTokenValid('foo', $token));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Csrf\EventListener;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
+
+class CsrfValidationListenerTest extends \PHPUnit_Framework_TestCase
+{
+ protected $dispatcher;
+ protected $factory;
+ protected $csrfProvider;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
+ $this->form = $this->getBuilder('post')
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->factory = null;
+ $this->csrfProvider = null;
+ $this->form = null;
+ }
+
+ protected function getBuilder($name = 'name')
+ {
+ return new FormBuilder($name, null, $this->dispatcher, $this->factory, array('compound' => true));
+ }
+
+ protected function getForm($name = 'name')
+ {
+ return $this->getBuilder($name)->getForm();
+ }
+
+ protected function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ protected function getMockForm()
+ {
+ return $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ }
+
+ // https://github.com/symfony/symfony/pull/5838
+ public function testStringFormData()
+ {
+ $data = "XP4HUzmHPi";
+ $event = new FormEvent($this->form, $data);
+
+ $validation = new CsrfValidationListener('csrf', $this->csrfProvider, 'unknown', 'Invalid.');
+ $validation->preSubmit($event);
+
+ // Validate accordingly
+ $this->assertSame($data, $event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Csrf\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\Test\TypeTestCase;
+use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
+
+class FormTypeCsrfExtensionTest_ChildType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ // The form needs a child in order to trigger CSRF protection by
+ // default
+ $builder->add('name', 'text');
+ }
+
+ public function getName()
+ {
+ return 'csrf_collection_test';
+ }
+}
+
+class FormTypeCsrfExtensionTest extends TypeTestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $csrfProvider;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $translator;
+
+ protected function setUp()
+ {
+ $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
+ $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface');
+
+ parent::setUp();
+ }
+
+ protected function tearDown()
+ {
+ $this->csrfProvider = null;
+ $this->translator = null;
+
+ parent::tearDown();
+ }
+
+ protected function getExtensions()
+ {
+ return array_merge(parent::getExtensions(), array(
+ new CsrfExtension($this->csrfProvider, $this->translator),
+ ));
+ }
+
+ public function testCsrfProtectionByDefaultIfRootAndCompound()
+ {
+ $view = $this->factory
+ ->create('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'compound' => true,
+ ))
+ ->createView();
+
+ $this->assertTrue(isset($view['csrf']));
+ }
+
+ public function testNoCsrfProtectionByDefaultIfCompoundButNotRoot()
+ {
+ $view = $this->factory
+ ->createNamedBuilder('root', 'form')
+ ->add($this->factory
+ ->createNamedBuilder('form', 'form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'compound' => true,
+ ))
+ )
+ ->getForm()
+ ->get('form')
+ ->createView();
+
+ $this->assertFalse(isset($view['csrf']));
+ }
+
+ public function testNoCsrfProtectionByDefaultIfRootButNotCompound()
+ {
+ $view = $this->factory
+ ->create('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'compound' => false,
+ ))
+ ->createView();
+
+ $this->assertFalse(isset($view['csrf']));
+ }
+
+ public function testCsrfProtectionCanBeDisabled()
+ {
+ $view = $this->factory
+ ->create('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_protection' => false,
+ 'compound' => true,
+ ))
+ ->createView();
+
+ $this->assertFalse(isset($view['csrf']));
+ }
+
+ public function testGenerateCsrfToken()
+ {
+ $this->csrfProvider->expects($this->once())
+ ->method('generateCsrfToken')
+ ->with('%INTENTION%')
+ ->will($this->returnValue('token'));
+
+ $view = $this->factory
+ ->create('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'intention' => '%INTENTION%',
+ 'compound' => true,
+ ))
+ ->createView();
+
+ $this->assertEquals('token', $view['csrf']->vars['value']);
+ }
+
+ public function provideBoolean()
+ {
+ return array(
+ array(true),
+ array(false),
+ );
+ }
+
+ /**
+ * @dataProvider provideBoolean
+ */
+ public function testValidateTokenOnSubmitIfRootAndCompound($valid)
+ {
+ $this->csrfProvider->expects($this->once())
+ ->method('isCsrfTokenValid')
+ ->with('%INTENTION%', 'token')
+ ->will($this->returnValue($valid));
+
+ $form = $this->factory
+ ->createBuilder('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'intention' => '%INTENTION%',
+ 'compound' => true,
+ ))
+ ->add('child', 'text')
+ ->getForm();
+
+ $form->submit(array(
+ 'child' => 'foobar',
+ 'csrf' => 'token',
+ ));
+
+ // Remove token from data
+ $this->assertSame(array('child' => 'foobar'), $form->getData());
+
+ // Validate accordingly
+ $this->assertSame($valid, $form->isValid());
+ }
+
+ public function testFailIfRootAndCompoundAndTokenMissing()
+ {
+ $this->csrfProvider->expects($this->never())
+ ->method('isCsrfTokenValid');
+
+ $form = $this->factory
+ ->createBuilder('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'intention' => '%INTENTION%',
+ 'compound' => true,
+ ))
+ ->add('child', 'text')
+ ->getForm();
+
+ $form->submit(array(
+ 'child' => 'foobar',
+ // token is missing
+ ));
+
+ // Remove token from data
+ $this->assertSame(array('child' => 'foobar'), $form->getData());
+
+ // Validate accordingly
+ $this->assertFalse($form->isValid());
+ }
+
+ public function testDontValidateTokenIfCompoundButNoRoot()
+ {
+ $this->csrfProvider->expects($this->never())
+ ->method('isCsrfTokenValid');
+
+ $form = $this->factory
+ ->createNamedBuilder('root', 'form')
+ ->add($this->factory
+ ->createNamedBuilder('form', 'form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'intention' => '%INTENTION%',
+ 'compound' => true,
+ ))
+ )
+ ->getForm()
+ ->get('form');
+
+ $form->submit(array(
+ 'child' => 'foobar',
+ 'csrf' => 'token',
+ ));
+ }
+
+ public function testDontValidateTokenIfRootButNotCompound()
+ {
+ $this->csrfProvider->expects($this->never())
+ ->method('isCsrfTokenValid');
+
+ $form = $this->factory
+ ->create('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'intention' => '%INTENTION%',
+ 'compound' => false,
+ ));
+
+ $form->submit(array(
+ 'csrf' => 'token',
+ ));
+ }
+
+ public function testNoCsrfProtectionOnPrototype()
+ {
+ $prototypeView = $this->factory
+ ->create('collection', null, array(
+ 'type' => new FormTypeCsrfExtensionTest_ChildType(),
+ 'options' => array(
+ 'csrf_field_name' => 'csrf',
+ ),
+ 'prototype' => true,
+ 'allow_add' => true,
+ ))
+ ->createView()
+ ->vars['prototype'];
+
+ $this->assertFalse(isset($prototypeView['csrf']));
+ $this->assertCount(1, $prototypeView);
+ }
+
+ public function testsTranslateCustomErrorMessage()
+ {
+ $this->csrfProvider->expects($this->once())
+ ->method('isCsrfTokenValid')
+ ->with('%INTENTION%', 'token')
+ ->will($this->returnValue(false));
+
+ $this->translator->expects($this->once())
+ ->method('trans')
+ ->with('Foobar')
+ ->will($this->returnValue('[trans]Foobar[/trans]'));
+
+ $form = $this->factory
+ ->createBuilder('form', null, array(
+ 'csrf_field_name' => 'csrf',
+ 'csrf_provider' => $this->csrfProvider,
+ 'csrf_message' => 'Foobar',
+ 'intention' => '%INTENTION%',
+ 'compound' => true,
+ ))
+ ->getForm();
+
+ $form->submit(array(
+ 'csrf' => 'token',
+ ));
+
+ $errors = $form->getErrors();
+
+ $this->assertGreaterThan(0, count($errors));
+ $this->assertEquals(new FormError('[trans]Foobar[/trans]'), $errors[0]);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\HttpFoundation\EventListener;
+
+use Symfony\Component\Form\Extension\HttpFoundation\EventListener\BindRequestListener;
+use Symfony\Component\Form\Form;
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Test\DeprecationErrorHandler;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BindRequestListenerTest extends \PHPUnit_Framework_TestCase
+{
+ private $values;
+
+ private $filesPlain;
+
+ private $filesNested;
+
+ /**
+ * @var UploadedFile
+ */
+ private $uploadedFile;
+
+ protected function setUp()
+ {
+ $path = tempnam(sys_get_temp_dir(), 'sf2');
+ touch($path);
+
+ $this->values = array(
+ 'name' => 'Bernhard',
+ 'image' => array('filename' => 'foobar.png'),
+ );
+
+ $this->filesPlain = array(
+ 'image' => array(
+ 'error' => UPLOAD_ERR_OK,
+ 'name' => 'upload.png',
+ 'size' => 123,
+ 'tmp_name' => $path,
+ 'type' => 'image/png'
+ ),
+ );
+
+ $this->filesNested = array(
+ 'error' => array('image' => UPLOAD_ERR_OK),
+ 'name' => array('image' => 'upload.png'),
+ 'size' => array('image' => 123),
+ 'tmp_name' => array('image' => $path),
+ 'type' => array('image' => 'image/png'),
+ );
+
+ $this->uploadedFile = new UploadedFile($path, 'upload.png', 'image/png', 123, UPLOAD_ERR_OK);
+ }
+
+ protected function tearDown()
+ {
+ unlink($this->uploadedFile->getRealPath());
+ }
+
+ public function requestMethodProvider()
+ {
+ return array(
+ array('POST'),
+ array('PUT'),
+ array('DELETE'),
+ array('PATCH'),
+ );
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitRequest($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $values = array('author' => $this->values);
+ $files = array('author' => $this->filesNested);
+ $request = new Request(array(), $values, array(), array(), $files, array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertEquals(array(
+ 'name' => 'Bernhard',
+ 'image' => $this->uploadedFile,
+ ), $event->getData());
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitRequestWithEmptyName($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request(array(), $this->values, array(), array(), $this->filesPlain, array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('', null, $dispatcher);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertEquals(array(
+ 'name' => 'Bernhard',
+ 'image' => $this->uploadedFile,
+ ), $event->getData());
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitEmptyRequestToCompoundForm($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request(array(), array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $config->setCompound(true);
+ $config->setDataMapper($this->getMock('Symfony\Component\Form\DataMapperInterface'));
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ // Default to empty array
+ $this->assertEquals(array(), $event->getData());
+ }
+
+ /**
+ * @dataProvider requestMethodProvider
+ */
+ public function testSubmitEmptyRequestToSimpleForm($method)
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request(array(), array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => $method,
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $config->setCompound(false);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ // Default to null
+ $this->assertNull($event->getData());
+ }
+
+ public function testSubmitGetRequest()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $values = array('author' => $this->values);
+ $request = new Request($values, array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertEquals(array(
+ 'name' => 'Bernhard',
+ 'image' => array('filename' => 'foobar.png'),
+ ), $event->getData());
+ }
+
+ public function testSubmitGetRequestWithEmptyName()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request($this->values, array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('', null, $dispatcher);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertEquals(array(
+ 'name' => 'Bernhard',
+ 'image' => array('filename' => 'foobar.png'),
+ ), $event->getData());
+ }
+
+ public function testSubmitEmptyGetRequestToCompoundForm()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request(array(), array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $config->setCompound(true);
+ $config->setDataMapper($this->getMock('Symfony\Component\Form\DataMapperInterface'));
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertEquals(array(), $event->getData());
+ }
+
+ public function testSubmitEmptyGetRequestToSimpleForm()
+ {
+ if (!class_exists('Symfony\Component\HttpFoundation\Request')) {
+ $this->markTestSkipped('The "HttpFoundation" component is not available');
+ }
+
+ $request = new Request(array(), array(), array(), array(), array(), array(
+ 'REQUEST_METHOD' => 'GET',
+ ));
+
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $config = new FormConfigBuilder('author', null, $dispatcher);
+ $config->setCompound(false);
+ $form = new Form($config);
+ $event = new FormEvent($form, $request);
+
+ $listener = new BindRequestListener();
+ DeprecationErrorHandler::preBind($listener, $event);
+
+ $this->assertNull($event->getData());
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\HttpFoundation;
+
+use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
+use Symfony\Component\Form\Tests\AbstractRequestHandlerTest;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class HttpFoundationRequestHandlerTest extends AbstractRequestHandlerTest
+{
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testRequestShouldNotBeNull()
+ {
+ $this->requestHandler->handleRequest($this->getMockForm('name', 'GET'));
+ }
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testRequestShouldBeInstanceOfRequest()
+ {
+ $this->requestHandler->handleRequest($this->getMockForm('name', 'GET'), new \stdClass());
+ }
+
+ protected function setRequestData($method, $data, $files = array())
+ {
+ $this->request = Request::create('http://localhost', $method, $data, array(), $files);
+ }
+
+ protected function getRequestHandler()
+ {
+ return new HttpFoundationRequestHandler();
+ }
+
+ protected function getMockFile()
+ {
+ return $this->getMockBuilder('Symfony\Component\HttpFoundation\File\UploadedFile')
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\Constraints;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\CallbackTransformer;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\Extension\Validator\Constraints\Form;
+use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator;
+use Symfony\Component\Form\SubmitButtonBuilder;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\NotNull;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormValidatorTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dispatcher;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $factory;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $serverParams;
+
+ /**
+ * @var FormValidator
+ */
+ private $validator;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\Event')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->serverParams = $this->getMock(
+ 'Symfony\Component\Form\Extension\Validator\Util\ServerParams',
+ array('getNormalizedIniPostMaxSize', 'getContentLength')
+ );
+ $this->validator = new FormValidator($this->serverParams);
+ }
+
+ public function testValidate()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $options = array('validation_groups' => array('group1', 'group2'));
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testValidateConstraints()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $constraint1 = new NotNull(array('groups' => array('group1', 'group2')));
+ $constraint2 = new NotBlank(array('groups' => 'group2'));
+
+ $options = array(
+ 'validation_groups' => array('group1', 'group2'),
+ 'constraints' => array($constraint1, $constraint2),
+ );
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ // First default constraints
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ // Then custom constraints
+ $context->expects($this->at(2))
+ ->method('validateValue')
+ ->with($object, $constraint1, 'data', 'group1');
+ $context->expects($this->at(3))
+ ->method('validateValue')
+ ->with($object, $constraint2, 'data', 'group2');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateIfParentWithoutCascadeValidation()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => false))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $options = array('validation_groups' => array('group1', 'group2'));
+ $form = $this->getBuilder('name', '\stdClass', $options)->getForm();
+ $parent->add($form);
+
+ $form->setData($object);
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testValidateConstraintsEvenIfNoCascadeValidation()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $constraint1 = new NotNull(array('groups' => array('group1', 'group2')));
+ $constraint2 = new NotBlank(array('groups' => 'group2'));
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => false))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $options = array(
+ 'validation_groups' => array('group1', 'group2'),
+ 'constraints' => array($constraint1, $constraint2),
+ );
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+ $parent->add($form);
+
+ $context->expects($this->at(0))
+ ->method('validateValue')
+ ->with($object, $constraint1, 'data', 'group1');
+ $context->expects($this->at(1))
+ ->method('validateValue')
+ ->with($object, $constraint2, 'data', 'group2');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $form = $this->getBuilder('name', '\stdClass', array(
+ 'validation_groups' => array(),
+ ))
+ ->setData($object)
+ ->getForm();
+
+ $form->setData($object);
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateConstraintsIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint');
+ $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint');
+
+ $options = array(
+ 'validation_groups' => array(),
+ 'constraints' => array($constraint1, $constraint2),
+ );
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ // Launch transformer
+ $form->submit(array());
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateIfNotSynchronized()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $form = $this->getBuilder('name', '\stdClass', array(
+ 'invalid_message' => 'invalid_message_key',
+ // Invalid message parameters must be supported, because the
+ // invalid message can be a translation key
+ // see https://github.com/symfony/symfony/issues/5144
+ 'invalid_message_parameters' => array('{{ foo }}' => 'bar'),
+ ))
+ ->setData($object)
+ ->addViewTransformer(new CallbackTransformer(
+ function ($data) { return $data; },
+ function () { throw new TransformationFailedException(); }
+ ))
+ ->getForm();
+
+ // Launch transformer
+ $form->submit('foo');
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $context->expects($this->once())
+ ->method('addViolation')
+ ->with(
+ 'invalid_message_key',
+ array('{{ value }}' => 'foo', '{{ foo }}' => 'bar'),
+ 'foo'
+ );
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testAddInvalidErrorEvenIfNoValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $form = $this->getBuilder('name', '\stdClass', array(
+ 'invalid_message' => 'invalid_message_key',
+ // Invalid message parameters must be supported, because the
+ // invalid message can be a translation key
+ // see https://github.com/symfony/symfony/issues/5144
+ 'invalid_message_parameters' => array('{{ foo }}' => 'bar'),
+ 'validation_groups' => array(),
+ ))
+ ->setData($object)
+ ->addViewTransformer(new CallbackTransformer(
+ function ($data) { return $data; },
+ function () { throw new TransformationFailedException(); }
+ ))
+ ->getForm();
+
+ // Launch transformer
+ $form->submit('foo');
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $context->expects($this->once())
+ ->method('addViolation')
+ ->with(
+ 'invalid_message_key',
+ array('{{ value }}' => 'foo', '{{ foo }}' => 'bar'),
+ 'foo'
+ );
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontValidateConstraintsIfNotSynchronized()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint');
+ $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint');
+
+ $options = array(
+ 'validation_groups' => array('group1', 'group2'),
+ 'constraints' => array($constraint1, $constraint2),
+ );
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->addViewTransformer(new CallbackTransformer(
+ function ($data) { return $data; },
+ function () { throw new TransformationFailedException(); }
+ ))
+ ->getForm();
+
+ // Launch transformer
+ $form->submit(array());
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ // https://github.com/symfony/symfony/issues/4359
+ public function testDontMarkInvalidIfAnyChildIsNotSynchronized()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $failingTransformer = new CallbackTransformer(
+ function ($data) { return $data; },
+ function () { throw new TransformationFailedException(); }
+ );
+
+ $form = $this->getBuilder('name', '\stdClass')
+ ->setData($object)
+ ->addViewTransformer($failingTransformer)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->add(
+ $this->getBuilder('child')
+ ->addViewTransformer($failingTransformer)
+ )
+ ->getForm();
+
+ // Launch transformer
+ $form->submit(array('child' => 'foo'));
+
+ $context->expects($this->never())
+ ->method('addViolation');
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testHandleCallbackValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $options = array('validation_groups' => array($this, 'getValidationGroups'));
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontExecuteFunctionNames()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $options = array('validation_groups' => 'header');
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'header', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testHandleClosureValidationGroups()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $options = array('validation_groups' => function(FormInterface $form){
+ return array('group1', 'group2');
+ });
+ $form = $this->getBuilder('name', '\stdClass', $options)
+ ->setData($object)
+ ->getForm();
+
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testUseValidationGroupOfClickedButton()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => true))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getForm('name', '\stdClass', array(
+ 'validation_groups' => 'form_group',
+ ));
+
+ $parent->add($form);
+ $parent->add($this->getClickedSubmitButton('submit', array(
+ 'validation_groups' => 'button_group',
+ )));
+
+ $form->setData($object);
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'button_group', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontUseValidationGroupOfUnclickedButton()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parent = $this->getBuilder('parent', null, array('cascade_validation' => true))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getForm('name', '\stdClass', array(
+ 'validation_groups' => 'form_group',
+ ));
+
+ $parent->add($form);
+ $parent->add($this->getSubmitButton('submit', array(
+ 'validation_groups' => 'button_group',
+ )));
+
+ $form->setData($object);
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'form_group', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testUseInheritedValidationGroup()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parentOptions = array(
+ 'validation_groups' => 'group',
+ 'cascade_validation' => true,
+ );
+ $parent = $this->getBuilder('parent', null, $parentOptions)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getBuilder('name', '\stdClass')->getForm();
+ $parent->add($form);
+
+ $form->setData($object);
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'group', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testUseInheritedCallbackValidationGroup()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parentOptions = array(
+ 'validation_groups' => array($this, 'getValidationGroups'),
+ 'cascade_validation' => true,
+ );
+ $parent = $this->getBuilder('parent', null, $parentOptions)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getBuilder('name', '\stdClass')->getForm();
+ $parent->add($form);
+
+ $form->setData($object);
+
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testUseInheritedClosureValidationGroup()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+
+ $parentOptions = array(
+ 'validation_groups' => function(FormInterface $form){
+ return array('group1', 'group2');
+ },
+ 'cascade_validation' => true,
+ );
+ $parent = $this->getBuilder('parent', null, $parentOptions)
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getBuilder('name', '\stdClass')->getForm();
+ $parent->add($form);
+
+ $form->setData($object);
+
+ $context->expects($this->at(0))
+ ->method('validate')
+ ->with($object, 'data', 'group1', true);
+ $context->expects($this->at(1))
+ ->method('validate')
+ ->with($object, 'data', 'group2', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testAppendPropertyPath()
+ {
+ $context = $this->getMockExecutionContext();
+ $object = $this->getMock('\stdClass');
+ $form = $this->getBuilder('name', '\stdClass')
+ ->setData($object)
+ ->getForm();
+
+ $context->expects($this->once())
+ ->method('validate')
+ ->with($object, 'data', 'Default', true);
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testDontWalkScalars()
+ {
+ $context = $this->getMockExecutionContext();
+
+ $form = $this->getBuilder()
+ ->setData('scalar')
+ ->getForm();
+
+ $context->expects($this->never())
+ ->method('validate');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function testViolationIfExtraData()
+ {
+ $context = $this->getMockExecutionContext();
+
+ $form = $this->getBuilder('parent', null, array('extra_fields_message' => 'Extra!'))
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->add($this->getBuilder('child'))
+ ->getForm();
+
+ $form->submit(array('foo' => 'bar'));
+
+ $context->expects($this->once())
+ ->method('addViolation')
+ ->with(
+ 'Extra!',
+ array('{{ extra_fields }}' => 'foo'),
+ array('foo' => 'bar')
+ );
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ /**
+ * @dataProvider getPostMaxSizeFixtures
+ */
+ public function testPostMaxSizeViolation($contentLength, $iniMax, $nbViolation, array $params = array())
+ {
+ $this->serverParams->expects($this->once())
+ ->method('getContentLength')
+ ->will($this->returnValue($contentLength));
+ $this->serverParams->expects($this->any())
+ ->method('getNormalizedIniPostMaxSize')
+ ->will($this->returnValue($iniMax));
+
+ $context = $this->getMockExecutionContext();
+ $options = array('post_max_size_message' => 'Max {{ max }}!');
+ $form = $this->getBuilder('name', null, $options)->getForm();
+
+ for ($i = 0; $i < $nbViolation; ++$i) {
+ if (0 === $i && count($params) > 0) {
+ $context->expects($this->at($i))
+ ->method('addViolation')
+ ->with($options['post_max_size_message'], $params);
+ } else {
+ $context->expects($this->at($i))
+ ->method('addViolation');
+ }
+ }
+
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ public function getPostMaxSizeFixtures()
+ {
+ return array(
+ array(pow(1024, 3) + 1, '1G', 1, array('{{ max }}' => '1G')),
+ array(pow(1024, 3), '1G', 0),
+ array(pow(1024, 2) + 1, '1M', 1, array('{{ max }}' => '1M')),
+ array(pow(1024, 2), '1M', 0),
+ array(1024 + 1, '1K', 1, array('{{ max }}' => '1K')),
+ array(1024, '1K', 0),
+ array(null, '1K', 0),
+ array(1024, '', 0),
+ array(1024, 0, 0),
+ );
+ }
+
+ public function testNoViolationIfNotRoot()
+ {
+ $this->serverParams->expects($this->once())
+ ->method('getContentLength')
+ ->will($this->returnValue(1025));
+ $this->serverParams->expects($this->never())
+ ->method('getNormalizedIniPostMaxSize');
+
+ $context = $this->getMockExecutionContext();
+ $parent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getForm();
+ $parent->add($form);
+
+ $context->expects($this->never())
+ ->method('addViolation');
+ $context->expects($this->never())
+ ->method('addViolationAt');
+
+ $this->validator->initialize($context);
+ $this->validator->validate($form, new Form());
+ }
+
+ /**
+ * Access has to be public, as this method is called via callback array
+ * in {@link testValidateFormDataCanHandleCallbackValidationGroups()}
+ * and {@link testValidateFormDataUsesInheritedCallbackValidationGroup()}
+ */
+ public function getValidationGroups(FormInterface $form)
+ {
+ return array('group1', 'group2');
+ }
+
+ private function getMockExecutionContext()
+ {
+ return $this->getMock('Symfony\Component\Validator\ExecutionContextInterface');
+ }
+
+ /**
+ * @param string $name
+ * @param string $dataClass
+ * @param array $options
+ *
+ * @return FormBuilder
+ */
+ private function getBuilder($name = 'name', $dataClass = null, array $options = array())
+ {
+ $options = array_replace(array(
+ 'constraints' => array(),
+ 'invalid_message_parameters' => array(),
+ ), $options);
+
+ return new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory, $options);
+ }
+
+ private function getForm($name = 'name', $dataClass = null, array $options = array())
+ {
+ return $this->getBuilder($name, $dataClass, $options)->getForm();
+ }
+
+ private function getSubmitButton($name = 'name', array $options = array())
+ {
+ $builder = new SubmitButtonBuilder($name, $options);
+
+ return $builder->getForm();
+ }
+
+ private function getClickedSubmitButton($name = 'name', array $options = array())
+ {
+ return $this->getSubmitButton($name, $options)->submit('');
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\EventListener;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\Extension\Validator\Constraints\Form;
+use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\Validator\ConstraintViolation;
+
+class ValidationListenerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dispatcher;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $factory;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $validator;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $violationMapper;
+
+ /**
+ * @var ValidationListener
+ */
+ private $listener;
+
+ private $message;
+
+ private $messageTemplate;
+
+ private $params;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\Event')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
+ $this->violationMapper = $this->getMock('Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface');
+ $this->listener = new ValidationListener($this->validator, $this->violationMapper);
+ $this->message = 'Message';
+ $this->messageTemplate = 'Message template';
+ $this->params = array('foo' => 'bar');
+ }
+
+ private function getConstraintViolation($code = null)
+ {
+ return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, null, 'prop.path', null, null, $code);
+ }
+
+ private function getBuilder($name = 'name', $propertyPath = null, $dataClass = null)
+ {
+ $builder = new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory);
+ $builder->setPropertyPath(new PropertyPath($propertyPath ?: $name));
+ $builder->setAttribute('error_mapping', array());
+ $builder->setErrorBubbling(false);
+ $builder->setMapped(true);
+
+ return $builder;
+ }
+
+ private function getForm($name = 'name', $propertyPath = null, $dataClass = null)
+ {
+ return $this->getBuilder($name, $propertyPath, $dataClass)->getForm();
+ }
+
+ private function getMockForm()
+ {
+ return $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ }
+
+ // More specific mapping tests can be found in ViolationMapperTest
+ public function testMapViolation()
+ {
+ $violation = $this->getConstraintViolation();
+ $form = $this->getForm('street');
+
+ $this->validator->expects($this->once())
+ ->method('validate')
+ ->will($this->returnValue(array($violation)));
+
+ $this->violationMapper->expects($this->once())
+ ->method('mapViolation')
+ ->with($violation, $form, false);
+
+ $this->listener->validateForm(new FormEvent($form, null));
+ }
+
+ public function testMapViolationAllowsNonSyncIfInvalid()
+ {
+ $violation = $this->getConstraintViolation(Form::ERR_INVALID);
+ $form = $this->getForm('street');
+
+ $this->validator->expects($this->once())
+ ->method('validate')
+ ->will($this->returnValue(array($violation)));
+
+ $this->violationMapper->expects($this->once())
+ ->method('mapViolation')
+ // pass true now
+ ->with($violation, $form, true);
+
+ $this->listener->validateForm(new FormEvent($form, null));
+ }
+
+ public function testValidateIgnoresNonRoot()
+ {
+ $form = $this->getMockForm();
+ $form->expects($this->once())
+ ->method('isRoot')
+ ->will($this->returnValue(false));
+
+ $this->validator->expects($this->never())
+ ->method('validate');
+
+ $this->violationMapper->expects($this->never())
+ ->method('mapViolation');
+
+ $this->listener->validateForm(new FormEvent($form, null));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\Type;
+
+use Symfony\Component\Form\FormInterface;
+
+class FormTypeValidatorExtensionTest extends TypeTestCase
+{
+ public function testValidationGroupNullByDefault()
+ {
+ $form = $this->factory->create('form');
+
+ $this->assertNull($form->getConfig()->getOption('validation_groups'));
+ }
+
+ public function testValidationGroupsTransformedToArray()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => 'group',
+ ));
+
+ $this->assertEquals(array('group'), $form->getConfig()->getOption('validation_groups'));
+ }
+
+ public function testValidationGroupsCanBeSetToArray()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => array('group1', 'group2'),
+ ));
+
+ $this->assertEquals(array('group1', 'group2'), $form->getConfig()->getOption('validation_groups'));
+ }
+
+ public function testValidationGroupsCanBeSetToFalse()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => false,
+ ));
+
+ $this->assertEquals(array(), $form->getConfig()->getOption('validation_groups'));
+ }
+
+ public function testValidationGroupsCanBeSetToCallback()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => array($this, 'testValidationGroupsCanBeSetToCallback'),
+ ));
+
+ $this->assertTrue(is_callable($form->getConfig()->getOption('validation_groups')));
+ }
+
+ public function testValidationGroupsCanBeSetToClosure()
+ {
+ $form = $this->factory->create('form', null, array(
+ 'validation_groups' => function(FormInterface $form){ return null; },
+ ));
+
+ $this->assertTrue(is_callable($form->getConfig()->getOption('validation_groups')));
+ }
+
+ public function testSubmitValidatesData()
+ {
+ $builder = $this->factory->createBuilder('form', null, array(
+ 'validation_groups' => 'group',
+ ));
+ $builder->add('firstName', 'form');
+ $form = $builder->getForm();
+
+ $this->validator->expects($this->once())
+ ->method('validate')
+ ->with($this->equalTo($form));
+
+ // specific data is irrelevant
+ $form->submit(array());
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Form\Tests\Extension\Validator\Type;
+
+use Symfony\Component\Form\Test\TypeTestCase as BaseTypeTestCase;
+use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
+
+abstract class TypeTestCase extends BaseTypeTestCase
+{
+ protected $validator;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Validator\Constraint')) {
+ $this->markTestSkipped('The "Validator" component is not available');
+ }
+
+ $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
+ $metadataFactory = $this->getMock('Symfony\Component\Validator\MetadataFactoryInterface');
+ $this->validator->expects($this->once())->method('getMetadataFactory')->will($this->returnValue($metadataFactory));
+ $metadata = $this->getMockBuilder('Symfony\Component\Validator\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock();
+ $metadataFactory->expects($this->once())->method('getMetadataFor')->will($this->returnValue($metadata));
+
+ parent::setUp();
+ }
+
+ protected function tearDown()
+ {
+ $this->validator = null;
+
+ parent::tearDown();
+ }
+
+ protected function getExtensions()
+ {
+ return array_merge(parent::getExtensions(), array(
+ new ValidatorExtension($this->validator),
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\Util;
+
+class ServerParamsTest extends \PHPUnit_Framework_TestCase
+{
+ /** @dataProvider getGetPostMaxSizeTestData */
+ public function testGetPostMaxSize($size, $bytes)
+ {
+ $serverParams = $this->getMock('Symfony\Component\Form\Extension\Validator\Util\ServerParams', array('getNormalizedIniPostMaxSize'));
+ $serverParams
+ ->expects($this->any())
+ ->method('getNormalizedIniPostMaxSize')
+ ->will($this->returnValue(strtoupper($size)));
+
+ $this->assertEquals($bytes, $serverParams->getPostMaxSize());
+ }
+
+ public function getGetPostMaxSizeTestData()
+ {
+ return array(
+ array('2k', 2048),
+ array('2 k', 2048),
+ array('8m', 8 * 1024 * 1024),
+ array('+2 k', 2048),
+ array('+2???k', 2048),
+ array('0x10', 16),
+ array('0xf', 15),
+ array('010', 8),
+ array('+0x10 k', 16 * 1024),
+ array('1g', 1024 * 1024 * 1024),
+ array('-1', -1),
+ array('0', 0),
+ array('2mk', 2048), // the unit must be the last char, so in this case 'k', not 'm'
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\CallbackTransformer;
+use Symfony\Component\Form\Form;
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationMapperTest extends \PHPUnit_Framework_TestCase
+{
+ const LEVEL_0 = 0;
+
+ const LEVEL_1 = 1;
+
+ const LEVEL_1B = 2;
+
+ const LEVEL_2 = 3;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dispatcher;
+
+ /**
+ * @var ViolationMapper
+ */
+ private $mapper;
+
+ /**
+ * @var string
+ */
+ private $message;
+
+ /**
+ * @var string
+ */
+ private $messageTemplate;
+
+ /**
+ * @var array
+ */
+ private $params;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\Event')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->mapper = new ViolationMapper();
+ $this->message = 'Message';
+ $this->messageTemplate = 'Message template';
+ $this->params = array('foo' => 'bar');
+ }
+
+ protected function getForm($name = 'name', $propertyPath = null, $dataClass = null, $errorMapping = array(), $inheritData = false, $synchronized = true)
+ {
+ $config = new FormConfigBuilder($name, $dataClass, $this->dispatcher, array(
+ 'error_mapping' => $errorMapping,
+ ));
+ $config->setMapped(true);
+ $config->setInheritData($inheritData);
+ $config->setPropertyPath($propertyPath);
+ $config->setCompound(true);
+ $config->setDataMapper($this->getDataMapper());
+
+ if (!$synchronized) {
+ $config->addViewTransformer(new CallbackTransformer(
+ function ($normData) { return $normData; },
+ function () { throw new TransformationFailedException(); }
+ ));
+ }
+
+ return new Form($config);
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getDataMapper()
+ {
+ return $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ /**
+ * @param $propertyPath
+ *
+ * @return ConstraintViolation
+ */
+ protected function getConstraintViolation($propertyPath)
+ {
+ return new ConstraintViolation($this->message, $this->messageTemplate, $this->params, null, $propertyPath, null);
+ }
+
+ /**
+ * @return FormError
+ */
+ protected function getFormError()
+ {
+ return new FormError($this->message, $this->messageTemplate, $this->params);
+ }
+
+ public function testMapToFormInheritingParentDataIfDataDoesNotMatch()
+ {
+ $violation = $this->getConstraintViolation('children[address].data.foo');
+ $parent = $this->getForm('parent');
+ $child = $this->getForm('address', 'address', null, array(), true);
+ $grandChild = $this->getForm('street');
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $child->getErrors(), $child->getName().' should have an error, but has none');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
+ }
+
+ public function testFollowDotRules()
+ {
+ $violation = $this->getConstraintViolation('data.foo');
+ $parent = $this->getForm('parent', null, null, array(
+ 'foo' => 'address',
+ ));
+ $child = $this->getForm('address', null, null, array(
+ '.' => 'street',
+ ));
+ $grandChild = $this->getForm('street', null, null, array(
+ '.' => 'name',
+ ));
+ $grandGrandChild = $this->getForm('name');
+
+ $parent->add($child);
+ $child->add($grandChild);
+ $grandChild->add($grandGrandChild);
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $child->getName().' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $grandGrandChild->getErrors(), $grandGrandChild->getName().' should have an error, but has none');
+ }
+
+ public function testAbortMappingIfNotSynchronized()
+ {
+ $violation = $this->getConstraintViolation('children[address].data.street');
+ $parent = $this->getForm('parent');
+ $child = $this->getForm('address', 'address', null, array(), false, false);
+ // even though "street" is synchronized, it should not have any errors
+ // due to its parent not being synchronized
+ $grandChild = $this->getForm('street' , 'street');
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ // submit to invoke the transformer and mark the form unsynchronized
+ $parent->submit(array());
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $child->getName().' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
+ }
+
+ public function testAbortDotRuleMappingIfNotSynchronized()
+ {
+ $violation = $this->getConstraintViolation('data.address');
+ $parent = $this->getForm('parent');
+ $child = $this->getForm('address', 'address', null, array(
+ '.' => 'street',
+ ), false, false);
+ // even though "street" is synchronized, it should not have any errors
+ // due to its parent not being synchronized
+ $grandChild = $this->getForm('street');
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ // submit to invoke the transformer and mark the form unsynchronized
+ $parent->submit(array());
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $child->getName().' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChild->getName().' should not have an error, but has one');
+ }
+
+ public function provideDefaultTests()
+ {
+ // The mapping must be deterministic! If a child has the property path "[street]",
+ // "data[street]" should be mapped, but "data.street" should not!
+ return array(
+ // mapping target, child name, its property path, grand child name, its property path, violation path
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', ''),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'data.person.address.street'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', 'street', 'data.person.address.street.prop'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', 'street', 'data.person.address[street]'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', 'street', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', '[street]', 'data.person.address.street'),
+ array(self::LEVEL_1, 'address', 'person.address', 'street', '[street]', 'data.person.address.street.prop'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'data.person.address[street]'),
+ array(self::LEVEL_2, 'address', 'person.address', 'street', '[street]', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', '[street]', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data.person.address[street].prop'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'data.person[address].street'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', 'street', 'data.person[address].street.prop'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', 'street', 'data.person[address][street]'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', 'street', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', 'street', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data.person.address[street].prop'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', '[street]', 'data.person[address].street'),
+ array(self::LEVEL_1, 'address', 'person[address]', 'street', '[street]', 'data.person[address].street.prop'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'data.person[address][street]'),
+ array(self::LEVEL_2, 'address', 'person[address]', 'street', '[street]', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', 'person[address]', 'street', '[street]', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data.person[address][street].prop'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'data[person].address.street'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', 'street', 'data[person].address.street.prop'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', 'street', 'data[person].address[street]'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', 'street', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', 'street', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data.person[address][street].prop'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', '[street]', 'data[person].address.street'),
+ array(self::LEVEL_1, 'address', '[person].address', 'street', '[street]', 'data[person].address.street.prop'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'data[person].address[street]'),
+ array(self::LEVEL_2, 'address', '[person].address', 'street', '[street]', 'data[person].address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data[person][address].street'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data[person][address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data[person][address][street]'),
+ array(self::LEVEL_0, 'address', '[person].address', 'street', '[street]', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'children[address]'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'children[address].data'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', 'street', 'data[person].address[street].prop'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'data[person][address].street'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', 'street', 'data[person][address].street.prop'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'data[person][address][street]'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', 'street', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', '[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'children[address].data[street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person.address.street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person.address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person.address[street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person.address[street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person[address].street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person[address].street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person[address][street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data.person[address][street].prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data[person].address.street'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data[person].address.street.prop'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data[person].address[street]'),
+ array(self::LEVEL_0, 'address', '[person][address]', 'street', '[street]', 'data[person].address[street].prop'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', '[street]', 'data[person][address].street'),
+ array(self::LEVEL_1, 'address', '[person][address]', 'street', '[street]', 'data[person][address].street.prop'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'data[person][address][street]'),
+ array(self::LEVEL_2, 'address', '[person][address]', 'street', '[street]', 'data[person][address][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data.office'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'children[address].data.office.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'data.address.office.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office.street', 'data.address.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address[office][street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address].office.street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address].office.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address].office[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address].office[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address][office].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address][office].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address][office][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office.street', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data.office'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'children[address].data.office.street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address.office.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address.office.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address.office[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address.office[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address[office].street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address[office].street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address[office][street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office.street', 'data.address[office][street].prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'data[address].office.street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office.street', 'data[address].office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address].office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address].office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address][office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address][office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address][office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office.street', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data.office'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'children[address].data.office[street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address.office.street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'data.address.office[street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'office[street]', 'data.address.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office[street]', 'data.address[office][street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address].office.street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address].office.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address].office[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address].office[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address][office].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address][office].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address][office][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'office[street]', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'children[address].data.office[street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address.office.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address.office.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address.office[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address.office[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address[office].street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address[office].street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address[office][street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', 'office[street]', 'data.address[office][street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address].office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address].office.street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'data[address].office[street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', 'office[street]', 'data[address].office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address][office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address][office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address][office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'office[street]', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data.office'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data[office]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'children[address].data[office].street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address.office[street].prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'data.address[office].street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office].street', 'data.address[office].street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address[office][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office].street', 'data.address[office][street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address].office.street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address].office.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address].office[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address].office[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address][office].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address][office].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address][office][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office].street', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data.office'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data[office]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'children[address].data[office].street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data[office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address.office.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address.office.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address.office[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address.office[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address[office].street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address[office].street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address[office][street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office].street', 'data.address[office][street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address].office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address].office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address].office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address].office[street].prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'data[address][office].street'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office].street', 'data[address][office].street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address][office][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office].street', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data.office'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'children[address].data[office][street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address.office.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address.office.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address.office[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address.office[street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address[office].street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[office][street]', 'data.address[office].street.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'data.address[office][street]'),
+ array(self::LEVEL_2, 'address', 'address', 'street', '[office][street]', 'data.address[office][street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address].office.street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address].office.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address].office[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address].office[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address][office].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address][office].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address][office][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', '[office][street]', 'data[address][office][street].prop'),
+
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data.office'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data.office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data.office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data.office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data.office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data[office]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data[office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'children[address].data[office].street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'children[address].data[office][street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'children[address].data[office][street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address.office.street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address.office.street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address.office[street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address.office[street].prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address[office].street'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address[office].street.prop'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address[office][street]'),
+ array(self::LEVEL_0, 'address', '[address]', 'street', '[office][street]', 'data.address[office][street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address].office.street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address].office.street.prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address].office[street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address].office[street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address][office].street'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[office][street]', 'data[address][office].street.prop'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'data[address][office][street]'),
+ array(self::LEVEL_2, 'address', '[address]', 'street', '[office][street]', 'data[address][office][street].prop'),
+
+ // Edge cases which must not occur
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address][street].prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'children[address][street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', '[street]', 'children[address][street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'children[address][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', 'street', 'children[address][street].prop'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'children[address][street]'),
+ array(self::LEVEL_1, 'address', '[address]', 'street', '[street]', 'children[address][street].prop'),
+
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'children[person].children[address].children[street]'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'children[person].children[address].data.street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'children[person].data.address.street'),
+ array(self::LEVEL_0, 'address', 'person.address', 'street', 'street', 'data.address.street'),
+
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].children[office].children[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].children[office].data.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'office.street', 'data.address.street'),
+ );
+ }
+
+ /**
+ * @dataProvider provideDefaultTests
+ */
+ public function testDefaultErrorMapping($target, $childName, $childPath, $grandChildName, $grandChildPath, $violationPath)
+ {
+ $violation = $this->getConstraintViolation($violationPath);
+ $parent = $this->getForm('parent');
+ $child = $this->getForm($childName, $childPath);
+ $grandChild = $this->getForm($grandChildName, $grandChildPath);
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ if (self::LEVEL_0 === $target) {
+ $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } elseif (self::LEVEL_1 === $target) {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } else {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
+ }
+ }
+
+ public function provideCustomDataErrorTests()
+ {
+ return array(
+ // mapping target, error mapping, child name, its property path, grand child name, its property path, violation path
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[foo][street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.foo.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.foo.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.foo[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.foo[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[foo].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[foo].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[foo][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[foo][street].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', 'address', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street].prop'),
+
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.foo.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.foo.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.foo[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.foo[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[foo].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[foo].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[foo][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[foo][street].prop'),
+
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo][street].prop'),
+
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[street].prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].street'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].street.prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][street]'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][street].prop'),
+
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', 'street', 'data[address][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.foo.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.foo.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.foo[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.foo[street].prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[foo].street'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[foo].street.prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[foo][street]'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[foo][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.address.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.address.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.address[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data.address[street].prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[address].street'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[address].street.prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[address][street]'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', '[address]', 'street', '[street]', 'data[address][street].prop'),
+
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_2, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street'),
+ array(self::LEVEL_2, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street]'),
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street'),
+ array(self::LEVEL_1, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_2, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street]'),
+ array(self::LEVEL_2, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, 'foo.bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_2, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street'),
+ array(self::LEVEL_2, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street]'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street'),
+ array(self::LEVEL_1, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_2, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street]'),
+ array(self::LEVEL_2, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, 'foo[bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_2, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street'),
+ array(self::LEVEL_2, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street]'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street'),
+ array(self::LEVEL_1, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_2, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street]'),
+ array(self::LEVEL_2, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street]'),
+ array(self::LEVEL_0, '[foo].bar', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_2, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street'),
+ array(self::LEVEL_2, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street]'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', 'street', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar.street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo.bar[street].prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar].street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data.foo[bar][street].prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar.street.prop'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street]'),
+ array(self::LEVEL_0, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo].bar[street].prop'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street'),
+ array(self::LEVEL_1, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar].street.prop'),
+ array(self::LEVEL_2, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street]'),
+ array(self::LEVEL_2, '[foo][bar]', 'address', 'address', 'address', 'street', '[street]', 'data[foo][bar][street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', 'address', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', 'address', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', 'address', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', 'address', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo.prop'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo]'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo.prop'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo]'),
+ array(self::LEVEL_2, '[foo]', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo].prop'),
+
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', 'address', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', 'address', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', 'address', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', 'address', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', 'address', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', 'address', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', 'address', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', 'address', 'street', 'street', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo.bar'),
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo.bar.prop'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo[bar]'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', 'address', 'street', '[street]', 'data.foo[bar].prop'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo].bar'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo].bar.prop'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo][bar]'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', 'address', 'street', '[street]', 'data[foo][bar].prop'),
+
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo.bar'),
+ array(self::LEVEL_2, 'foo.bar', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo.bar.prop'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo[bar]'),
+ array(self::LEVEL_2, 'foo[bar]', 'address.street', 'address', '[address]', 'street', 'street', 'data.foo[bar].prop'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo].bar'),
+ array(self::LEVEL_2, '[foo].bar', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo].bar.prop'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo][bar]'),
+ array(self::LEVEL_2, '[foo][bar]', 'address.street', 'address', '[address]', 'street', 'street', 'data[foo][bar].prop'),
+
+ // Edge cases
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_0, 'foo', 'address', 'address', '[address]', 'street', 'street', 'data[foo][street].prop'),
+
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo.street'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo.street.prop'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo[street]'),
+ array(self::LEVEL_0, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data.foo[street].prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo].street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo].street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo][street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'address', 'address', 'street', 'street', 'data[foo][street].prop'),
+ );
+ }
+
+ /**
+ * @dataProvider provideCustomDataErrorTests
+ */
+ public function testCustomDataErrorMapping($target, $mapFrom, $mapTo, $childName, $childPath, $grandChildName, $grandChildPath, $violationPath)
+ {
+ $violation = $this->getConstraintViolation($violationPath);
+ $parent = $this->getForm('parent', null, null, array($mapFrom => $mapTo));
+ $child = $this->getForm($childName, $childPath);
+ $grandChild = $this->getForm($grandChildName, $grandChildPath);
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ // Add a field mapped to the first element of $mapFrom
+ // to try to distract the algorithm
+ // Only add it if we expect the error to come up on a different
+ // level than LEVEL_0, because in this case the error would
+ // (correctly) be mapped to the distraction field
+ if ($target !== self::LEVEL_0) {
+ $mapFromPath = new PropertyPath($mapFrom);
+ $mapFromPrefix = $mapFromPath->isIndex(0)
+ ? '['.$mapFromPath->getElement(0).']'
+ : $mapFromPath->getElement(0);
+ $distraction = $this->getForm('distraction', $mapFromPrefix);
+
+ $parent->add($distraction);
+ }
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ if ($target !== self::LEVEL_0) {
+ $this->assertCount(0, $distraction->getErrors(), 'distraction should not have an error, but has one');
+ }
+
+ if (self::LEVEL_0 === $target) {
+ $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } elseif (self::LEVEL_1 === $target) {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } else {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
+ }
+ }
+
+ public function provideCustomFormErrorTests()
+ {
+ // This case is different than the data errors, because here the
+ // left side of the mapping refers to the property path of the actual
+ // children. In other words, a child error only works if
+ // 1) the error actually maps to an existing child and
+ // 2) the property path of that child (relative to the form providing
+ // the mapping) matches the left side of the mapping
+ return array(
+ // mapping target, map from, map to, child name, its property path, grand child name, its property path, violation path
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].children[street].data'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street].prop'),
+
+ // Property path of the erroneous field and mapping must match exactly
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].children[street].data'),
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data.street'),
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data.street.prop'),
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data[street]'),
+ array(self::LEVEL_1B, 'foo', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].children[street].data'),
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data.street'),
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data.street.prop'),
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data[street]'),
+ array(self::LEVEL_1B, '[foo]', 'address', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_1, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].children[street].data'),
+ array(self::LEVEL_1, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_2, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data.street'),
+ array(self::LEVEL_2, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data.street.prop'),
+ array(self::LEVEL_1, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data[street]'),
+ array(self::LEVEL_1, '[foo]', 'address', 'foo', '[foo]', 'address', 'address', 'street', 'street', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].data.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].data.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].data[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street].prop'),
+
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].children[street].data'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].data.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].data.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].data[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].children[street].data.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].data.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].data.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].data[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo].data[street].prop'),
+
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street.prop'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'foo', 'address', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street].prop'),
+
+ // Map to a nested child
+ array(self::LEVEL_2, 'foo', 'address.street', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[foo]'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[foo]'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[foo]'),
+ array(self::LEVEL_2, 'foo', 'address.street', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[foo]'),
+
+ // Map from a nested child
+ array(self::LEVEL_1B, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_1B, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1B, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address.street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1B, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1B, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1B, 'address[street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address[street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_1B, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_1B, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1B, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, '[address].street', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', 'address', 'street', '[street]', 'children[address].data[street]'),
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].children[street]'),
+ array(self::LEVEL_2, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_1B, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1B, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].children[street]'),
+ array(self::LEVEL_1, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data.street'),
+ array(self::LEVEL_1B, '[address][street]', 'foo', 'foo', 'foo', 'address', '[address]', 'street', '[street]', 'children[address].data[street]'),
+ );
+ }
+
+ /**
+ * @dataProvider provideCustomFormErrorTests
+ */
+ public function testCustomFormErrorMapping($target, $mapFrom, $mapTo, $errorName, $errorPath, $childName, $childPath, $grandChildName, $grandChildPath, $violationPath)
+ {
+ $violation = $this->getConstraintViolation($violationPath);
+ $parent = $this->getForm('parent', null, null, array($mapFrom => $mapTo));
+ $child = $this->getForm($childName, $childPath);
+ $grandChild = $this->getForm($grandChildName, $grandChildPath);
+ $errorChild = $this->getForm($errorName, $errorPath);
+
+ $parent->add($child);
+ $parent->add($errorChild);
+ $child->add($grandChild);
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ if (self::LEVEL_0 === $target) {
+ $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } elseif (self::LEVEL_1 === $target) {
+ $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } elseif (self::LEVEL_1B === $target) {
+ $this->assertEquals(array($this->getFormError()), $errorChild->getErrors(), $errorName.' should have an error, but has none');
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } else {
+ $this->assertCount(0, $errorChild->getErrors(), $errorName.' should not have an error, but has one');
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
+ }
+ }
+
+ public function provideErrorTestsForFormInheritingParentData()
+ {
+ return array(
+ // mapping target, child name, its property path, grand child name, its property path, violation path
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].children[street].data'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].children[street].data.prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].data.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'children[address].data.street.prop'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address].data[street]'),
+ array(self::LEVEL_1, 'address', 'address', 'street', 'street', 'children[address].data[street].prop'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'data.street'),
+ array(self::LEVEL_2, 'address', 'address', 'street', 'street', 'data.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data.address.street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data.address.street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data.address[street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data.address[street].prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address].street'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address].street.prop'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address][street]'),
+ array(self::LEVEL_0, 'address', 'address', 'street', 'street', 'data[address][street].prop'),
+ );
+ }
+
+ /**
+ * @dataProvider provideErrorTestsForFormInheritingParentData
+ */
+ public function testErrorMappingForFormInheritingParentData($target, $childName, $childPath, $grandChildName, $grandChildPath, $violationPath)
+ {
+ $violation = $this->getConstraintViolation($violationPath);
+ $parent = $this->getForm('parent');
+ $child = $this->getForm($childName, $childPath, null, array(), true);
+ $grandChild = $this->getForm($grandChildName, $grandChildPath);
+
+ $parent->add($child);
+ $child->add($grandChild);
+
+ $this->mapper->mapViolation($violation, $parent);
+
+ if (self::LEVEL_0 === $target) {
+ $this->assertEquals(array($this->getFormError()), $parent->getErrors(), $parent->getName().' should have an error, but has none');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } elseif (self::LEVEL_1 === $target) {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $child->getErrors(), $childName.' should have an error, but has none');
+ $this->assertCount(0, $grandChild->getErrors(), $grandChildName.' should not have an error, but has one');
+ } else {
+ $this->assertCount(0, $parent->getErrors(), $parent->getName().' should not have an error, but has one');
+ $this->assertCount(0, $child->getErrors(), $childName.' should not have an error, but has one');
+ $this->assertEquals(array($this->getFormError()), $grandChild->getErrors(), $grandChildName.' should have an error, but has none');
+ }
+ }
+}
--- /dev/null
+<?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\Form\Tests\Extension\Validator\ViolationMapper;
+
+use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPath;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ViolationPathTest extends \PHPUnit_Framework_TestCase
+{
+ public function providePaths()
+ {
+ return array(
+ array('children[address]', array(
+ array('address', true, true),
+ )),
+ array('children[address].children[street]', array(
+ array('address', true, true),
+ array('street', true, true),
+ )),
+ array('children[address][street]', array(
+ array('address', true, true),
+ ), 'children[address]'),
+ array('children[address].data', array(
+ array('address', true, true),
+ ), 'children[address]'),
+ array('children[address].data.street', array(
+ array('address', true, true),
+ array('street', false, false),
+ )),
+ array('children[address].data[street]', array(
+ array('address', true, true),
+ array('street', false, true),
+ )),
+ array('children[address].children[street].data.name', array(
+ array('address', true, true),
+ array('street', true, true),
+ array('name', false, false),
+ )),
+ array('children[address].children[street].data[name]', array(
+ array('address', true, true),
+ array('street', true, true),
+ array('name', false, true),
+ )),
+ array('data.address', array(
+ array('address', false, false),
+ )),
+ array('data[address]', array(
+ array('address', false, true),
+ )),
+ array('data.address.street', array(
+ array('address', false, false),
+ array('street', false, false),
+ )),
+ array('data[address].street', array(
+ array('address', false, true),
+ array('street', false, false),
+ )),
+ array('data.address[street]', array(
+ array('address', false, false),
+ array('street', false, true),
+ )),
+ array('data[address][street]', array(
+ array('address', false, true),
+ array('street', false, true),
+ )),
+ // A few invalid examples
+ array('data', array(), ''),
+ array('children', array(), ''),
+ array('children.address', array(), ''),
+ array('children.address[street]', array(), ''),
+ );
+ }
+
+ /**
+ * @dataProvider providePaths
+ */
+ public function testCreatePath($string, $entries, $slicedPath = null)
+ {
+ if (null === $slicedPath) {
+ $slicedPath = $string;
+ }
+
+ $path = new ViolationPath($string);
+
+ $this->assertSame($slicedPath, $path->__toString());
+ $this->assertSame(count($entries), count($path->getElements()));
+ $this->assertSame(count($entries), $path->getLength());
+
+ foreach ($entries as $index => $entry) {
+ $this->assertEquals($entry[0], $path->getElement($index));
+ $this->assertSame($entry[1], $path->mapsForm($index));
+ $this->assertSame($entry[2], $path->isIndex($index));
+ $this->assertSame(!$entry[2], $path->isProperty($index));
+ }
+ }
+
+ public function provideParents()
+ {
+ return array(
+ array('children[address]', null),
+ array('children[address].children[street]', 'children[address]'),
+ array('children[address].data.street', 'children[address]'),
+ array('children[address].data[street]', 'children[address]'),
+ array('data.address', null),
+ array('data.address.street', 'data.address'),
+ array('data.address[street]', 'data.address'),
+ array('data[address].street', 'data[address]'),
+ array('data[address][street]', 'data[address]'),
+ );
+ }
+
+ /**
+ * @dataProvider provideParents
+ */
+ public function testGetParent($violationPath, $parentPath)
+ {
+ $path = new ViolationPath($violationPath);
+ $parent = $parentPath === null ? null : new ViolationPath($parentPath);
+
+ $this->assertEquals($parent, $path->getParent());
+ }
+
+ public function testGetElement()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $this->assertEquals('street', $path->getElement(1));
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testGetElementDoesNotAcceptInvalidIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->getElement(3);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testGetElementDoesNotAcceptNegativeIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->getElement(-1);
+ }
+
+ public function testIsProperty()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $this->assertFalse($path->isProperty(1));
+ $this->assertTrue($path->isProperty(2));
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testIsPropertyDoesNotAcceptInvalidIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->isProperty(3);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testIsPropertyDoesNotAcceptNegativeIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->isProperty(-1);
+ }
+
+ public function testIsIndex()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $this->assertTrue($path->isIndex(1));
+ $this->assertFalse($path->isIndex(2));
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testIsIndexDoesNotAcceptInvalidIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->isIndex(3);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testIsIndexDoesNotAcceptNegativeIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->isIndex(-1);
+ }
+
+ public function testMapsForm()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $this->assertTrue($path->mapsForm(0));
+ $this->assertFalse($path->mapsForm(1));
+ $this->assertFalse($path->mapsForm(2));
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testMapsFormDoesNotAcceptInvalidIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->mapsForm(3);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testMapsFormDoesNotAcceptNegativeIndices()
+ {
+ $path = new ViolationPath('children[address].data[street].name');
+
+ $path->mapsForm(-1);
+ }
+}
--- /dev/null
+<?php
+
+namespace Symfony\Component\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class AlternatingRowType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $formFactory = $builder->getFormFactory();
+
+ $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formFactory) {
+ $form = $event->getForm();
+ $type = $form->getName() % 2 === 0 ? 'text' : 'textarea';
+ $form->add('title', $type);
+ });
+ }
+
+ public function getName()
+ {
+ return 'alternating_row';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+class Author
+{
+ public $firstName;
+ private $lastName;
+ private $australian;
+ public $child;
+ private $readPermissions;
+
+ private $privateProperty;
+
+ public function setLastName($lastName)
+ {
+ $this->lastName = $lastName;
+ }
+
+ public function getLastName()
+ {
+ return $this->lastName;
+ }
+
+ private function getPrivateGetter()
+ {
+ return 'foobar';
+ }
+
+ public function setAustralian($australian)
+ {
+ $this->australian = $australian;
+ }
+
+ public function isAustralian()
+ {
+ return $this->australian;
+ }
+
+ public function setReadPermissions($bool)
+ {
+ $this->readPermissions = $bool;
+ }
+
+ public function hasReadPermissions()
+ {
+ return $this->readPermissions;
+ }
+
+ private function isPrivateIsser()
+ {
+ return true;
+ }
+
+ public function getPrivateSetter()
+ {
+ }
+
+ private function setPrivateSetter($data)
+ {
+ }
+}
--- /dev/null
+<?php
+
+namespace Symfony\Component\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class AuthorType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('firstName')
+ ->add('lastName')
+ ;
+ }
+
+ public function getName()
+ {
+ return 'author';
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
+ ));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+/**
+ * This class is a hand written simplified version of PHP native `ArrayObject`
+ * class, to show that it behaves differently than the PHP native implementation.
+ */
+class CustomArrayObject implements \ArrayAccess, \IteratorAggregate, \Countable, \Serializable
+{
+ private $array;
+
+ public function __construct(array $array = null)
+ {
+ $this->array = $array ?: array();
+ }
+
+ public function offsetExists($offset)
+ {
+ return array_key_exists($offset, $this->array);
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->array[$offset];
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ if (null === $offset) {
+ $this->array[] = $value;
+ } else {
+ $this->array[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->array[$offset]);
+ }
+
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->array);
+ }
+
+ public function count()
+ {
+ return count($this->array);
+ }
+
+ public function serialize()
+ {
+ return serialize($this->array);
+ }
+
+ public function unserialize($serialized)
+ {
+ $this->array = (array) unserialize((string) $serialized);
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\Exception\RuntimeException;
+
+class FixedDataTransformer implements DataTransformerInterface
+{
+ private $mapping;
+
+ public function __construct(array $mapping)
+ {
+ $this->mapping = $mapping;
+ }
+
+ public function transform($value)
+ {
+ if (!array_key_exists($value, $this->mapping)) {
+ throw new RuntimeException(sprintf('No mapping for value "%s"', $value));
+ }
+
+ return $this->mapping[$value];
+ }
+
+ public function reverseTransform($value)
+ {
+ $result = array_search($value, $this->mapping, true);
+
+ if ($result === false) {
+ throw new RuntimeException(sprintf('No reverse mapping for value "%s"', $value));
+ }
+
+ return $result;
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class FixedFilterListener implements EventSubscriberInterface
+{
+ private $mapping;
+
+ public function __construct(array $mapping)
+ {
+ $this->mapping = array_merge(array(
+ 'preSubmit' => array(),
+ 'onSubmit' => array(),
+ 'preSetData' => array(),
+ ), $mapping);
+ }
+
+ public function preSubmit(FormEvent $event)
+ {
+ $data = $event->getData();
+
+ if (isset($this->mapping['preSubmit'][$data])) {
+ $event->setData($this->mapping['preSubmit'][$data]);
+ }
+ }
+
+ public function onSubmit(FormEvent $event)
+ {
+ $data = $event->getData();
+
+ if (isset($this->mapping['onSubmit'][$data])) {
+ $event->setData($this->mapping['onSubmit'][$data]);
+ }
+ }
+
+ public function preSetData(FormEvent $event)
+ {
+ $data = $event->getData();
+
+ if (isset($this->mapping['preSetData'][$data])) {
+ $event->setData($this->mapping['preSetData'][$data]);
+ }
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(
+ FormEvents::PRE_SUBMIT => 'preSubmit',
+ FormEvents::SUBMIT => 'onSubmit',
+ FormEvents::PRE_SET_DATA => 'preSetData',
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class FooSubType extends AbstractType
+{
+ public function getName()
+ {
+ return 'foo_sub_type';
+ }
+
+ public function getParent()
+ {
+ return 'foo';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class FooSubTypeWithParentInstance extends AbstractType
+{
+ public function getName()
+ {
+ return 'foo_sub_type_parent_instance';
+ }
+
+ public function getParent()
+ {
+ return new FooType();
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormFactoryInterface;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+class FooType extends AbstractType
+{
+ public function getName()
+ {
+ return 'foo';
+ }
+
+ public function getParent()
+ {
+ return null;
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class FooTypeBarExtension extends AbstractTypeExtension
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->setAttribute('bar', 'x');
+ }
+
+ public function getAllowedOptionValues()
+ {
+ return array(
+ 'a_or_b' => array('c'),
+ );
+ }
+
+ public function getExtendedType()
+ {
+ return 'foo';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\FormBuilderInterface;
+
+class FooTypeBazExtension extends AbstractTypeExtension
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder->setAttribute('baz', 'x');
+ }
+
+ public function getExtendedType()
+ {
+ return 'foo';
+ }
+}
--- /dev/null
+<?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\Form\Tests\Fixtures;
+
+use Symfony\Component\Form\FormTypeInterface;
+use Symfony\Component\Form\FormTypeExtensionInterface;
+use Symfony\Component\Form\FormTypeGuesserInterface;
+use Symfony\Component\Form\FormExtensionInterface;
+
+class TestExtension implements FormExtensionInterface
+{
+ private $types = array();
+
+ private $extensions = array();
+
+ private $guesser;
+
+ public function __construct(FormTypeGuesserInterface $guesser)
+ {
+ $this->guesser = $guesser;
+ }
+
+ public function addType(FormTypeInterface $type)
+ {
+ $this->types[$type->getName()] = $type;
+ }
+
+ public function getType($name)
+ {
+ return isset($this->types[$name]) ? $this->types[$name] : null;
+ }
+
+ public function hasType($name)
+ {
+ return isset($this->types[$name]);
+ }
+
+ public function addTypeExtension(FormTypeExtensionInterface $extension)
+ {
+ $type = $extension->getExtendedType();
+
+ if (!isset($this->extensions[$type])) {
+ $this->extensions[$type] = array();
+ }
+
+ $this->extensions[$type][] = $extension;
+ }
+
+ public function getTypeExtensions($name)
+ {
+ return isset($this->extensions[$name]) ? $this->extensions[$name] : array();
+ }
+
+ public function hasTypeExtensions($name)
+ {
+ return isset($this->extensions[$name]);
+ }
+
+ public function getTypeGuesser()
+ {
+ return $this->guesser;
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormBuilder;
+
+class FormBuilderTest extends \PHPUnit_Framework_TestCase
+{
+ private $dispatcher;
+
+ private $factory;
+
+ private $builder;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->factory = null;
+ $this->builder = null;
+ }
+
+ /**
+ * Changing the name is not allowed, otherwise the name and property path
+ * are not synchronized anymore
+ *
+ * @see FormType::buildForm
+ */
+ public function testNoSetName()
+ {
+ $this->assertFalse(method_exists($this->builder, 'setName'));
+ }
+
+ public function testAddNameNoStringAndNoInteger()
+ {
+ $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
+ $this->builder->add(true);
+ }
+
+ public function testAddTypeNoString()
+ {
+ $this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
+ $this->builder->add('foo', 1234);
+ }
+
+ public function testAddWithGuessFluent()
+ {
+ $this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
+ $builder = $this->builder->add('foo');
+ $this->assertSame($builder, $this->builder);
+ }
+
+ public function testAddIsFluent()
+ {
+ $builder = $this->builder->add('foo', 'text', array('bar' => 'baz'));
+ $this->assertSame($builder, $this->builder);
+ }
+
+ public function testAdd()
+ {
+ $this->assertFalse($this->builder->has('foo'));
+ $this->builder->add('foo', 'text');
+ $this->assertTrue($this->builder->has('foo'));
+ }
+
+ public function testAddIntegerName()
+ {
+ $this->assertFalse($this->builder->has(0));
+ $this->builder->add(0, 'text');
+ $this->assertTrue($this->builder->has(0));
+ }
+
+ public function testAll()
+ {
+ $this->factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('foo', 'text')
+ ->will($this->returnValue(new FormBuilder('foo', null, $this->dispatcher, $this->factory)));
+
+ $this->assertCount(0, $this->builder->all());
+ $this->assertFalse($this->builder->has('foo'));
+
+ $this->builder->add('foo', 'text');
+ $children = $this->builder->all();
+
+ $this->assertTrue($this->builder->has('foo'));
+ $this->assertCount(1, $children);
+ $this->assertArrayHasKey('foo', $children);
+ }
+
+ /*
+ * https://github.com/symfony/symfony/issues/4693
+ */
+ public function testMaintainOrderOfLazyAndExplicitChildren()
+ {
+ $this->builder->add('foo', 'text');
+ $this->builder->add($this->getFormBuilder('bar'));
+ $this->builder->add('baz', 'text');
+
+ $children = $this->builder->all();
+
+ $this->assertSame(array('foo', 'bar', 'baz'), array_keys($children));
+ }
+
+ public function testAddFormType()
+ {
+ $this->assertFalse($this->builder->has('foo'));
+ $this->builder->add('foo', $this->getMock('Symfony\Component\Form\FormTypeInterface'));
+ $this->assertTrue($this->builder->has('foo'));
+ }
+
+ public function testRemove()
+ {
+ $this->builder->add('foo', 'text');
+ $this->builder->remove('foo');
+ $this->assertFalse($this->builder->has('foo'));
+ }
+
+ public function testRemoveUnknown()
+ {
+ $this->builder->remove('foo');
+ $this->assertFalse($this->builder->has('foo'));
+ }
+
+ // https://github.com/symfony/symfony/pull/4826
+ public function testRemoveAndGetForm()
+ {
+ $this->builder->add('foo', 'text');
+ $this->builder->remove('foo');
+ $form = $this->builder->getForm();
+ $this->assertInstanceOf('Symfony\Component\Form\Form', $form);
+ }
+
+ public function testCreateNoTypeNo()
+ {
+ $this->factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('foo', 'text', null, array())
+ ;
+
+ $this->builder->create('foo');
+ }
+
+ public function testGetUnknown()
+ {
+ $this->setExpectedException('Symfony\Component\Form\Exception\InvalidArgumentException', 'The child with the name "foo" does not exist.');
+ $this->builder->get('foo');
+ }
+
+ public function testGetExplicitType()
+ {
+ $expectedType = 'text';
+ $expectedName = 'foo';
+ $expectedOptions = array('bar' => 'baz');
+
+ $this->factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with($expectedName, $expectedType, null, $expectedOptions)
+ ->will($this->returnValue($this->getFormBuilder()));
+
+ $this->builder->add($expectedName, $expectedType, $expectedOptions);
+ $builder = $this->builder->get($expectedName);
+
+ $this->assertNotSame($builder, $this->builder);
+ }
+
+ public function testGetGuessedType()
+ {
+ $expectedName = 'foo';
+ $expectedOptions = array('bar' => 'baz');
+
+ $this->factory->expects($this->once())
+ ->method('createBuilderForProperty')
+ ->with('stdClass', $expectedName, null, $expectedOptions)
+ ->will($this->returnValue($this->getFormBuilder()));
+
+ $this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
+ $this->builder->add($expectedName, null, $expectedOptions);
+ $builder = $this->builder->get($expectedName);
+
+ $this->assertNotSame($builder, $this->builder);
+ }
+
+ public function testGetFormConfigErasesReferences()
+ {
+ $builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
+ $builder->add(new FormBuilder('child', null, $this->dispatcher, $this->factory));
+
+ $config = $builder->getFormConfig();
+ $reflClass = new \ReflectionClass($config);
+ $children = $reflClass->getProperty('children');
+ $unresolvedChildren = $reflClass->getProperty('unresolvedChildren');
+
+ $children->setAccessible(true);
+ $unresolvedChildren->setAccessible(true);
+
+ $this->assertEmpty($children->getValue($config));
+ $this->assertEmpty($unresolvedChildren->getValue($config));
+ }
+
+ private function getFormBuilder($name = 'name')
+ {
+ $mock = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue($name));
+
+ return $mock;
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\Exception\InvalidArgumentException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormConfigTest extends \PHPUnit_Framework_TestCase
+{
+ public function getHtml4Ids()
+ {
+ return array(
+ array('z0', true),
+ array('A0', true),
+ array('A9', true),
+ array('Z0', true),
+ array('#', false),
+ array('a#', false),
+ array('a$', false),
+ array('a%', false),
+ array('a ', false),
+ array("a\t", false),
+ array("a\n", false),
+ array('a-', true),
+ array('a_', true),
+ array('a:', true),
+ // Periods are allowed by the HTML4 spec, but disallowed by us
+ // because they break the generated property paths
+ array('a.', false),
+ // Contrary to the HTML4 spec, we allow names starting with a
+ // number, otherwise naming fields by collection indices is not
+ // possible.
+ // For root forms, leading digits will be stripped from the
+ // "id" attribute to produce valid HTML4.
+ array('0', true),
+ array('9', true),
+ // Contrary to the HTML4 spec, we allow names starting with an
+ // underscore, since this is already a widely used practice in
+ // Symfony2.
+ // For root forms, leading underscores will be stripped from the
+ // "id" attribute to produce valid HTML4.
+ array('_', true),
+ // Integers are allowed
+ array(0, true),
+ array(123, true),
+ // NULL is allowed
+ array(null, true),
+ // Other types are not
+ array(1.23, false),
+ array(5., false),
+ array(true, false),
+ array(new \stdClass(), false),
+ );
+ }
+
+ /**
+ * @dataProvider getHtml4Ids
+ */
+ public function testNameAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
+ {
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+
+ try {
+ new FormConfigBuilder($name, null, $dispatcher);
+ if (!$accepted) {
+ $this->fail(sprintf('The value "%s" should not be accepted', $name));
+ }
+ } catch (UnexpectedTypeException $e) {
+ // if the value was not accepted, but should be, rethrow exception
+ if ($accepted) {
+ throw $e;
+ }
+ } catch (InvalidArgumentException $e) {
+ // if the value was not accepted, but should be, rethrow exception
+ if ($accepted) {
+ throw $e;
+ }
+ }
+ }
+
+ public function testGetRequestHandlerCreatesNativeRequestHandlerIfNotSet()
+ {
+ $config = $this->getConfigBuilder()->getFormConfig();
+
+ $this->assertInstanceOf('Symfony\Component\Form\NativeRequestHandler', $config->getRequestHandler());
+ }
+
+ public function testGetRequestHandlerReusesNativeRequestHandlerInstance()
+ {
+ $config1 = $this->getConfigBuilder()->getFormConfig();
+ $config2 = $this->getConfigBuilder()->getFormConfig();
+
+ $this->assertSame($config1->getRequestHandler(), $config2->getRequestHandler());
+ }
+
+ public function testSetMethodAllowsGet()
+ {
+ $this->getConfigBuilder()->setMethod('GET');
+ }
+
+ public function testSetMethodAllowsPost()
+ {
+ $this->getConfigBuilder()->setMethod('POST');
+ }
+
+ public function testSetMethodAllowsPut()
+ {
+ $this->getConfigBuilder()->setMethod('PUT');
+ }
+
+ public function testSetMethodAllowsDelete()
+ {
+ $this->getConfigBuilder()->setMethod('DELETE');
+ }
+
+ public function testSetMethodAllowsPatch()
+ {
+ $this->getConfigBuilder()->setMethod('PATCH');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
+ */
+ public function testSetMethodDoesNotAllowOtherValues()
+ {
+ $this->getConfigBuilder()->setMethod('foo');
+ }
+
+ private function getConfigBuilder($name = 'name')
+ {
+ $dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+
+ return new FormConfigBuilder($name, null, $dispatcher);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormFactoryBuilder;
+use Symfony\Component\Form\Tests\Fixtures\FooType;
+
+class FormFactoryBuilderTest extends \PHPUnit_Framework_TestCase
+{
+ private $registry;
+ private $guesser;
+ private $type;
+
+ protected function setUp()
+ {
+ $factory = new \ReflectionClass('Symfony\Component\Form\FormFactory');
+ $this->registry = $factory->getProperty('registry');
+ $this->registry->setAccessible(true);
+
+ $this->guesser = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface');
+ $this->type = new FooType;
+ }
+
+ public function testAddType()
+ {
+ $factoryBuilder = new FormFactoryBuilder;
+ $factoryBuilder->addType($this->type);
+
+ $factory = $factoryBuilder->getFormFactory();
+ $registry = $this->registry->getValue($factory);
+ $extensions = $registry->getExtensions();
+
+ $this->assertCount(1, $extensions);
+ $this->assertTrue($extensions[0]->hasType($this->type->getName()));
+ $this->assertNull($extensions[0]->getTypeGuesser());
+ }
+
+ public function testAddTypeGuesser()
+ {
+ $factoryBuilder = new FormFactoryBuilder;
+ $factoryBuilder->addTypeGuesser($this->guesser);
+
+ $factory = $factoryBuilder->getFormFactory();
+ $registry = $this->registry->getValue($factory);
+ $extensions = $registry->getExtensions();
+
+ $this->assertCount(1, $extensions);
+ $this->assertNotNull($extensions[0]->getTypeGuesser());
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormTypeGuesserChain;
+use Symfony\Component\Form\FormFactory;
+use Symfony\Component\Form\Guess\Guess;
+use Symfony\Component\Form\Guess\ValueGuess;
+use Symfony\Component\Form\Guess\TypeGuess;
+use Symfony\Component\Form\Tests\Fixtures\Author;
+use Symfony\Component\Form\Tests\Fixtures\FooType;
+use Symfony\Component\Form\Tests\Fixtures\FooSubType;
+use Symfony\Component\Form\Tests\Fixtures\FooSubTypeWithParentInstance;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormFactoryTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $guesser1;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $guesser2;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $registry;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $resolvedTypeFactory;
+
+ /**
+ * @var FormFactory
+ */
+ private $factory;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->resolvedTypeFactory = $this->getMock('Symfony\Component\Form\ResolvedFormTypeFactoryInterface');
+ $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface');
+ $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface');
+ $this->registry = $this->getMock('Symfony\Component\Form\FormRegistryInterface');
+ $this->factory = new FormFactory($this->registry, $this->resolvedTypeFactory);
+
+ $this->registry->expects($this->any())
+ ->method('getTypeGuesser')
+ ->will($this->returnValue(new FormTypeGuesserChain(array(
+ $this->guesser1,
+ $this->guesser2,
+ ))));
+ }
+
+ public function testCreateNamedBuilderWithTypeName()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $resolvedType = $this->getMockResolvedType();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('type')
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options));
+ }
+
+ public function testCreateNamedBuilderWithTypeInstance()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $type = new FooType();
+ $resolvedType = $this->getMockResolvedType();
+
+ $this->resolvedTypeFactory->expects($this->once())
+ ->method('createResolvedType')
+ ->with($type)
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options));
+ }
+
+ public function testCreateNamedBuilderWithTypeInstanceWithParentType()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $type = new FooSubType();
+ $resolvedType = $this->getMockResolvedType();
+ $parentResolvedType = $this->getMockResolvedType();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('foo')
+ ->will($this->returnValue($parentResolvedType));
+
+ $this->resolvedTypeFactory->expects($this->once())
+ ->method('createResolvedType')
+ ->with($type, array(), $parentResolvedType)
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options));
+ }
+
+ public function testCreateNamedBuilderWithTypeInstanceWithParentTypeInstance()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $type = new FooSubTypeWithParentInstance();
+ $resolvedType = $this->getMockResolvedType();
+ $parentResolvedType = $this->getMockResolvedType();
+
+ $this->resolvedTypeFactory->expects($this->at(0))
+ ->method('createResolvedType')
+ ->with($type->getParent())
+ ->will($this->returnValue($parentResolvedType));
+
+ $this->resolvedTypeFactory->expects($this->at(1))
+ ->method('createResolvedType')
+ ->with($type, array(), $parentResolvedType)
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options));
+ }
+
+ public function testCreateNamedBuilderWithResolvedTypeInstance()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $resolvedType = $this->getMockResolvedType();
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $resolvedType, null, $options));
+ }
+
+ public function testCreateNamedBuilderFillsDataOption()
+ {
+ $givenOptions = array('a' => '1', 'b' => '2');
+ $expectedOptions = array_merge($givenOptions, array('data' => 'DATA'));
+ $resolvedType = $this->getMockResolvedType();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('type')
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $expectedOptions)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions));
+ }
+
+ public function testCreateNamedBuilderDoesNotOverrideExistingDataOption()
+ {
+ $options = array('a' => '1', 'b' => '2', 'data' => 'CUSTOM');
+ $resolvedType = $this->getMockResolvedType();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('type')
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue('BUILDER'));
+
+ $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $options));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ * @expectedExceptionMessage Expected argument of type "string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface", "stdClass" given
+ */
+ public function testCreateNamedBuilderThrowsUnderstandableException()
+ {
+ $this->factory->createNamedBuilder('name', new \stdClass());
+ }
+
+ public function testCreateUsesTypeNameIfTypeGivenAsString()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $resolvedType = $this->getMockResolvedType();
+ $builder = $this->getMockFormBuilder();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('TYPE')
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'TYPE', $options)
+ ->will($this->returnValue($builder));
+
+ $builder->expects($this->once())
+ ->method('getForm')
+ ->will($this->returnValue('FORM'));
+
+ $this->assertSame('FORM', $this->factory->create('TYPE', null, $options));
+ }
+
+ public function testCreateUsesTypeNameIfTypeGivenAsObject()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $resolvedType = $this->getMockResolvedType();
+ $builder = $this->getMockFormBuilder();
+
+ $resolvedType->expects($this->once())
+ ->method('getName')
+ ->will($this->returnValue('TYPE'));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'TYPE', $options)
+ ->will($this->returnValue($builder));
+
+ $builder->expects($this->once())
+ ->method('getForm')
+ ->will($this->returnValue('FORM'));
+
+ $this->assertSame('FORM', $this->factory->create($resolvedType, null, $options));
+ }
+
+ public function testCreateNamed()
+ {
+ $options = array('a' => '1', 'b' => '2');
+ $resolvedType = $this->getMockResolvedType();
+ $builder = $this->getMockFormBuilder();
+
+ $this->registry->expects($this->once())
+ ->method('getType')
+ ->with('type')
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->once())
+ ->method('createBuilder')
+ ->with($this->factory, 'name', $options)
+ ->will($this->returnValue($builder));
+
+ $builder->expects($this->once())
+ ->method('getForm')
+ ->will($this->returnValue('FORM'));
+
+ $this->assertSame('FORM', $this->factory->createNamed('name', 'type', null, $options));
+ }
+
+ public function testCreateBuilderForPropertyWithoutTypeGuesser()
+ {
+ $registry = $this->getMock('Symfony\Component\Form\FormRegistryInterface');
+ $factory = $this->getMockBuilder('Symfony\Component\Form\FormFactory')
+ ->setMethods(array('createNamedBuilder'))
+ ->setConstructorArgs(array($registry, $this->resolvedTypeFactory))
+ ->getMock();
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text', null, array())
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty('Application\Author', 'firstName');
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testCreateBuilderForPropertyCreatesFormWithHighestConfidence()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessType')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new TypeGuess(
+ 'text',
+ array('max_length' => 10),
+ Guess::MEDIUM_CONFIDENCE
+ )));
+
+ $this->guesser2->expects($this->once())
+ ->method('guessType')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new TypeGuess(
+ 'password',
+ array('max_length' => 7),
+ Guess::HIGH_CONFIDENCE
+ )));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'password', null, array('max_length' => 7))
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty('Application\Author', 'firstName');
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testCreateBuilderCreatesTextFormIfNoGuess()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessType')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(null));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text')
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty('Application\Author', 'firstName');
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testOptionsCanBeOverridden()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessType')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new TypeGuess(
+ 'text',
+ array('max_length' => 10),
+ Guess::MEDIUM_CONFIDENCE
+ )));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text', null, array('max_length' => 11))
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty(
+ 'Application\Author',
+ 'firstName',
+ null,
+ array('max_length' => 11)
+ );
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testCreateBuilderUsesMaxLengthIfFound()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessMaxLength')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ 15,
+ Guess::MEDIUM_CONFIDENCE
+ )));
+
+ $this->guesser2->expects($this->once())
+ ->method('guessMaxLength')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ 20,
+ Guess::HIGH_CONFIDENCE
+ )));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text', null, array('max_length' => 20))
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty(
+ 'Application\Author',
+ 'firstName'
+ );
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testCreateBuilderUsesRequiredSettingWithHighestConfidence()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessRequired')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ true,
+ Guess::MEDIUM_CONFIDENCE
+ )));
+
+ $this->guesser2->expects($this->once())
+ ->method('guessRequired')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ false,
+ Guess::HIGH_CONFIDENCE
+ )));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text', null, array('required' => false))
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty(
+ 'Application\Author',
+ 'firstName'
+ );
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ public function testCreateBuilderUsesPatternIfFound()
+ {
+ $this->guesser1->expects($this->once())
+ ->method('guessPattern')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ '[a-z]',
+ Guess::MEDIUM_CONFIDENCE
+ )));
+
+ $this->guesser2->expects($this->once())
+ ->method('guessPattern')
+ ->with('Application\Author', 'firstName')
+ ->will($this->returnValue(new ValueGuess(
+ '[a-zA-Z]',
+ Guess::HIGH_CONFIDENCE
+ )));
+
+ $factory = $this->getMockFactory(array('createNamedBuilder'));
+
+ $factory->expects($this->once())
+ ->method('createNamedBuilder')
+ ->with('firstName', 'text', null, array('pattern' => '[a-zA-Z]'))
+ ->will($this->returnValue('builderInstance'));
+
+ $builder = $factory->createBuilderForProperty(
+ 'Application\Author',
+ 'firstName'
+ );
+
+ $this->assertEquals('builderInstance', $builder);
+ }
+
+ private function getMockFactory(array $methods = array())
+ {
+ return $this->getMockBuilder('Symfony\Component\Form\FormFactory')
+ ->setMethods($methods)
+ ->setConstructorArgs(array($this->registry, $this->resolvedTypeFactory))
+ ->getMock();
+ }
+
+ private function getMockResolvedType()
+ {
+ return $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ }
+
+ private function getMockType()
+ {
+ return $this->getMock('Symfony\Component\Form\FormTypeInterface');
+ }
+
+ private function getMockFormBuilder()
+ {
+ return $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface');
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\Test\FormIntegrationTestCase as BaseFormIntegrationTestCase;
+
+/**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use Symfony\Component\Form\Test\FormIntegrationTestCase instead.
+ */
+abstract class FormIntegrationTestCase extends BaseFormIntegrationTestCase
+{
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\Test\FormPerformanceTestCase as BaseFormPerformanceTestCase;
+
+/**
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use Symfony\Component\Form\Test\FormPerformanceTestCase instead.
+ */
+abstract class FormPerformanceTestCase extends BaseFormPerformanceTestCase
+{
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\FormRegistry;
+use Symfony\Component\Form\FormTypeGuesserChain;
+use Symfony\Component\Form\Tests\Fixtures\TestExtension;
+use Symfony\Component\Form\Tests\Fixtures\FooSubTypeWithParentInstance;
+use Symfony\Component\Form\Tests\Fixtures\FooSubType;
+use Symfony\Component\Form\Tests\Fixtures\FooTypeBazExtension;
+use Symfony\Component\Form\Tests\Fixtures\FooTypeBarExtension;
+use Symfony\Component\Form\Tests\Fixtures\FooType;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormRegistryTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var FormRegistry
+ */
+ private $registry;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $resolvedTypeFactory;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $guesser1;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $guesser2;
+
+ /**
+ * @var TestExtension
+ */
+ private $extension1;
+
+ /**
+ * @var TestExtension
+ */
+ private $extension2;
+
+ protected function setUp()
+ {
+ $this->resolvedTypeFactory = $this->getMock('Symfony\Component\Form\ResolvedFormTypeFactory');
+ $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface');
+ $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface');
+ $this->extension1 = new TestExtension($this->guesser1);
+ $this->extension2 = new TestExtension($this->guesser2);
+ $this->registry = new FormRegistry(array(
+ $this->extension1,
+ $this->extension2,
+ ), $this->resolvedTypeFactory);
+ }
+
+ public function testGetTypeFromExtension()
+ {
+ $type = new FooType();
+ $resolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+
+ $this->extension2->addType($type);
+
+ $this->resolvedTypeFactory->expects($this->once())
+ ->method('createResolvedType')
+ ->with($type)
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo'));
+
+ $resolvedType = $this->registry->getType('foo');
+
+ $this->assertSame($resolvedType, $this->registry->getType('foo'));
+ }
+
+ public function testGetTypeWithTypeExtensions()
+ {
+ $type = new FooType();
+ $ext1 = new FooTypeBarExtension();
+ $ext2 = new FooTypeBazExtension();
+ $resolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+
+ $this->extension2->addType($type);
+ $this->extension1->addTypeExtension($ext1);
+ $this->extension2->addTypeExtension($ext2);
+
+ $this->resolvedTypeFactory->expects($this->once())
+ ->method('createResolvedType')
+ ->with($type, array($ext1, $ext2))
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo'));
+
+ $this->assertSame($resolvedType, $this->registry->getType('foo'));
+ }
+
+ public function testGetTypeConnectsParent()
+ {
+ $parentType = new FooType();
+ $type = new FooSubType();
+ $parentResolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ $resolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+
+ $this->extension1->addType($parentType);
+ $this->extension2->addType($type);
+
+ $this->resolvedTypeFactory->expects($this->at(0))
+ ->method('createResolvedType')
+ ->with($parentType)
+ ->will($this->returnValue($parentResolvedType));
+
+ $this->resolvedTypeFactory->expects($this->at(1))
+ ->method('createResolvedType')
+ ->with($type, array(), $parentResolvedType)
+ ->will($this->returnValue($resolvedType));
+
+ $parentResolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo'));
+
+ $resolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo_sub_type'));
+
+ $this->assertSame($resolvedType, $this->registry->getType('foo_sub_type'));
+ }
+
+ public function testGetTypeConnectsParentIfGetParentReturnsInstance()
+ {
+ $type = new FooSubTypeWithParentInstance();
+ $parentResolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ $resolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+
+ $this->extension1->addType($type);
+
+ $this->resolvedTypeFactory->expects($this->at(0))
+ ->method('createResolvedType')
+ ->with($this->isInstanceOf('Symfony\Component\Form\Tests\Fixtures\FooType'))
+ ->will($this->returnValue($parentResolvedType));
+
+ $this->resolvedTypeFactory->expects($this->at(1))
+ ->method('createResolvedType')
+ ->with($type, array(), $parentResolvedType)
+ ->will($this->returnValue($resolvedType));
+
+ $parentResolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo'));
+
+ $resolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo_sub_type_parent_instance'));
+
+ $this->assertSame($resolvedType, $this->registry->getType('foo_sub_type_parent_instance'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testGetTypeThrowsExceptionIfParentNotFound()
+ {
+ $type = new FooSubType();
+
+ $this->extension1->addType($type);
+
+ $this->registry->getType($type);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
+ */
+ public function testGetTypeThrowsExceptionIfTypeNotFound()
+ {
+ $this->registry->getType('bar');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testGetTypeThrowsExceptionIfNoString()
+ {
+ $this->registry->getType(array());
+ }
+
+ public function testHasTypeAfterLoadingFromExtension()
+ {
+ $type = new FooType();
+ $resolvedType = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+
+ $this->resolvedTypeFactory->expects($this->once())
+ ->method('createResolvedType')
+ ->with($type)
+ ->will($this->returnValue($resolvedType));
+
+ $resolvedType->expects($this->any())
+ ->method('getName')
+ ->will($this->returnValue('foo'));
+
+ $this->assertFalse($this->registry->hasType('foo'));
+
+ $this->extension2->addType($type);
+
+ $this->assertTrue($this->registry->hasType('foo'));
+ }
+
+ public function testGetTypeGuesser()
+ {
+ $expectedGuesser = new FormTypeGuesserChain(array($this->guesser1, $this->guesser2));
+
+ $this->assertEquals($expectedGuesser, $this->registry->getTypeGuesser());
+
+ $registry = new FormRegistry(
+ array($this->getMock('Symfony\Component\Form\FormExtensionInterface')),
+ $this->resolvedTypeFactory);
+
+ $this->assertNull($registry->getTypeGuesser());
+ }
+
+ public function testGetExtensions()
+ {
+ $expectedExtensions = array($this->extension1, $this->extension2);
+
+ $this->assertEquals($expectedExtensions, $this->registry->getExtensions());
+ }
+}
--- /dev/null
+<?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\Form\Test;
+
+class FormRendererTest extends \PHPUnit_Framework_TestCase
+{
+ public function testHumanize()
+ {
+ $renderer = $this->getMockBuilder('Symfony\Component\Form\FormRenderer')
+ ->setMethods(null)
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+
+ $this->assertEquals('Is active', $renderer->humanize('is_active'));
+ $this->assertEquals('Is active', $renderer->humanize('isActive'));
+ }
+}
--- /dev/null
+<?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\Form\Tests\Guess;
+
+use Symfony\Component\Form\Guess\Guess;
+
+class TestGuess extends Guess {}
+
+class GuessTest extends \PHPUnit_Framework_TestCase
+{
+ public function testGetBestGuessReturnsGuessWithHighestConfidence()
+ {
+ $guess1 = new TestGuess(Guess::MEDIUM_CONFIDENCE);
+ $guess2 = new TestGuess(Guess::LOW_CONFIDENCE);
+ $guess3 = new TestGuess(Guess::HIGH_CONFIDENCE);
+
+ $this->assertSame($guess3, Guess::getBestGuess(array($guess1, $guess2, $guess3)));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testGuessExpectsValidConfidence()
+ {
+ new TestGuess(5);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\NativeRequestHandler;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class NativeRequestHandlerTest extends AbstractRequestHandlerTest
+{
+ private static $serverBackup;
+
+ public static function setUpBeforeClass()
+ {
+ self::$serverBackup = $_SERVER;
+ }
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $_GET = array();
+ $_POST = array();
+ $_FILES = array();
+ $_SERVER = array(
+ // PHPUnit needs this entry
+ 'SCRIPT_NAME' => self::$serverBackup['SCRIPT_NAME'],
+ );
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $_GET = array();
+ $_POST = array();
+ $_FILES = array();
+ $_SERVER = self::$serverBackup;
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
+ */
+ public function testRequestShouldBeNull()
+ {
+ $this->requestHandler->handleRequest($this->getMockForm('name', 'GET'), 'request');
+ }
+
+ public function testMethodOverrideHeaderTakesPrecedenceIfPost()
+ {
+ $form = $this->getMockForm('param1', 'PUT');
+
+ $this->setRequestData('POST', array(
+ 'param1' => 'DATA',
+ ));
+
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT';
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with('DATA');
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ public function testConvertEmptyUploadedFilesToNull()
+ {
+ $form = $this->getMockForm('param1', 'POST', false);
+
+ $this->setRequestData('POST', array(), array('param1' => array(
+ 'name' => '',
+ 'type' => '',
+ 'tmp_name' => '',
+ 'error' => UPLOAD_ERR_NO_FILE,
+ 'size' => 0
+ )));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with($this->identicalTo(null));
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ public function testFixBuggyFilesArray()
+ {
+ $form = $this->getMockForm('param1', 'POST', false);
+
+ $this->setRequestData('POST', array(), array('param1' => array(
+ 'name' => array(
+ 'field' => 'upload.txt',
+ ),
+ 'type' => array(
+ 'field' => 'text/plain',
+ ),
+ 'tmp_name' => array(
+ 'field' => 'owfdskjasdfsa',
+ ),
+ 'error' => array(
+ 'field' => UPLOAD_ERR_OK,
+ ),
+ 'size' => array(
+ 'field' => 100,
+ ),
+ )));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with(array(
+ 'field' => array(
+ 'name' => 'upload.txt',
+ 'type' => 'text/plain',
+ 'tmp_name' => 'owfdskjasdfsa',
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => 100,
+ ),
+ ));
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ public function testFixBuggyNestedFilesArray()
+ {
+ $form = $this->getMockForm('param1', 'POST');
+
+ $this->setRequestData('POST', array(), array('param1' => array(
+ 'name' => array(
+ 'field' => array('subfield' => 'upload.txt'),
+ ),
+ 'type' => array(
+ 'field' => array('subfield' => 'text/plain'),
+ ),
+ 'tmp_name' => array(
+ 'field' => array('subfield' => 'owfdskjasdfsa'),
+ ),
+ 'error' => array(
+ 'field' => array('subfield' => UPLOAD_ERR_OK),
+ ),
+ 'size' => array(
+ 'field' => array('subfield' => 100),
+ ),
+ )));
+
+ $form->expects($this->once())
+ ->method('submit')
+ ->with(array(
+ 'field' => array(
+ 'subfield' => array(
+ 'name' => 'upload.txt',
+ 'type' => 'text/plain',
+ 'tmp_name' => 'owfdskjasdfsa',
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => 100,
+ ),
+ ),
+ ));
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ public function testMethodOverrideHeaderIgnoredIfNotPost()
+ {
+ $form = $this->getMockForm('param1', 'POST');
+
+ $this->setRequestData('GET', array(
+ 'param1' => 'DATA',
+ ));
+
+ $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PUT';
+
+ $form->expects($this->never())
+ ->method('submit');
+
+ $this->requestHandler->handleRequest($form, $this->request);
+ }
+
+ protected function setRequestData($method, $data, $files = array())
+ {
+ if ('GET' === $method) {
+ $_GET = $data;
+ $_FILES = array();
+ } else {
+ $_POST = $data;
+ $_FILES = $files;
+ }
+
+ $_SERVER = array(
+ 'REQUEST_METHOD' => $method,
+ // PHPUnit needs this entry
+ 'SCRIPT_NAME' => self::$serverBackup['SCRIPT_NAME'],
+ );
+ }
+
+ protected function getRequestHandler()
+ {
+ return new NativeRequestHandler();
+ }
+
+ protected function getMockFile()
+ {
+ return array(
+ 'name' => 'upload.txt',
+ 'type' => 'text/plain',
+ 'tmp_name' => 'owfdskjasdfsa',
+ 'error' => UPLOAD_ERR_OK,
+ 'size' => 100,
+ );
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\ResolvedFormType;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\Form;
+use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dispatcher;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $factory;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $dataMapper;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\OptionsResolver\OptionsResolver')) {
+ $this->markTestSkipped('The "OptionsResolver" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+ $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ $this->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface');
+ }
+
+ public function testCreateBuilder()
+ {
+ if (version_compare(\PHPUnit_Runner_Version::id(), '3.7', '<')) {
+ $this->markTestSkipped('This test requires PHPUnit 3.7.');
+ }
+
+ $parentType = $this->getMockFormType();
+ $type = $this->getMockFormType();
+ $extension1 = $this->getMockFormTypeExtension();
+ $extension2 = $this->getMockFormTypeExtension();
+
+ $parentResolvedType = new ResolvedFormType($parentType);
+ $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType);
+
+ $test = $this;
+ $i = 0;
+
+ $assertIndex = function ($index) use (&$i, $test) {
+ return function () use (&$i, $test, $index) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals($index, $i, 'Executed at index '.$index);
+
+ ++$i;
+ };
+ };
+
+ $assertIndexAndAddOption = function ($index, $option, $default) use ($assertIndex) {
+ $assertIndex = $assertIndex($index);
+
+ return function (OptionsResolverInterface $resolver) use ($assertIndex, $index, $option, $default) {
+ $assertIndex();
+
+ $resolver->setDefaults(array($option => $default));
+ };
+ };
+
+ // First the default options are generated for the super type
+ $parentType->expects($this->once())
+ ->method('setDefaultOptions')
+ ->will($this->returnCallback($assertIndexAndAddOption(0, 'a', 'a_default')));
+
+ // The form type itself
+ $type->expects($this->once())
+ ->method('setDefaultOptions')
+ ->will($this->returnCallback($assertIndexAndAddOption(1, 'b', 'b_default')));
+
+ // And its extensions
+ $extension1->expects($this->once())
+ ->method('setDefaultOptions')
+ ->will($this->returnCallback($assertIndexAndAddOption(2, 'c', 'c_default')));
+
+ $extension2->expects($this->once())
+ ->method('setDefaultOptions')
+ ->will($this->returnCallback($assertIndexAndAddOption(3, 'd', 'd_default')));
+
+ $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom');
+ $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default');
+
+ // Then the form is built for the super type
+ $parentType->expects($this->once())
+ ->method('buildForm')
+ ->with($this->anything(), $resolvedOptions)
+ ->will($this->returnCallback($assertIndex(4)));
+
+ // Then the type itself
+ $type->expects($this->once())
+ ->method('buildForm')
+ ->with($this->anything(), $resolvedOptions)
+ ->will($this->returnCallback($assertIndex(5)));
+
+ // Then its extensions
+ $extension1->expects($this->once())
+ ->method('buildForm')
+ ->with($this->anything(), $resolvedOptions)
+ ->will($this->returnCallback($assertIndex(6)));
+
+ $extension2->expects($this->once())
+ ->method('buildForm')
+ ->with($this->anything(), $resolvedOptions)
+ ->will($this->returnCallback($assertIndex(7)));
+
+ $factory = $this->getMockFormFactory();
+ $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions);
+
+ $this->assertSame($resolvedType, $builder->getType());
+ }
+
+ public function testCreateView()
+ {
+ $parentType = $this->getMockFormType();
+ $type = $this->getMockFormType();
+ $field1Type = $this->getMockFormType();
+ $field2Type = $this->getMockFormType();
+ $extension1 = $this->getMockFormTypeExtension();
+ $extension2 = $this->getMockFormTypeExtension();
+
+ $parentResolvedType = new ResolvedFormType($parentType);
+ $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType);
+ $field1ResolvedType = new ResolvedFormType($field1Type);
+ $field2ResolvedType = new ResolvedFormType($field2Type);
+
+ $options = array('a' => '1', 'b' => '2');
+ $form = $this->getBuilder('name', $options)
+ ->setCompound(true)
+ ->setDataMapper($this->dataMapper)
+ ->setType($resolvedType)
+ ->add($this->getBuilder('foo')->setType($field1ResolvedType))
+ ->add($this->getBuilder('bar')->setType($field2ResolvedType))
+ ->getForm();
+
+ $test = $this;
+ $i = 0;
+
+ $assertIndexAndNbOfChildViews = function ($index, $nbOfChildViews) use (&$i, $test) {
+ return function (FormView $view) use (&$i, $test, $index, $nbOfChildViews) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals($index, $i, 'Executed at index '.$index);
+ $test->assertCount($nbOfChildViews, $view);
+
+ ++$i;
+ };
+ };
+
+ // First the super type
+ $parentType->expects($this->once())
+ ->method('buildView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(0, 0)));
+
+ // Then the type itself
+ $type->expects($this->once())
+ ->method('buildView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(1, 0)));
+
+ // Then its extensions
+ $extension1->expects($this->once())
+ ->method('buildView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(2, 0)));
+
+ $extension2->expects($this->once())
+ ->method('buildView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(3, 0)));
+
+ // Now the first child form
+ $field1Type->expects($this->once())
+ ->method('buildView')
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(4, 0)));
+ $field1Type->expects($this->once())
+ ->method('finishView')
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(5, 0)));
+
+ // And the second child form
+ $field2Type->expects($this->once())
+ ->method('buildView')
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(6, 0)));
+ $field2Type->expects($this->once())
+ ->method('finishView')
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(7, 0)));
+
+ // Again first the parent
+ $parentType->expects($this->once())
+ ->method('finishView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(8, 2)));
+
+ // Then the type itself
+ $type->expects($this->once())
+ ->method('finishView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(9, 2)));
+
+ // Then its extensions
+ $extension1->expects($this->once())
+ ->method('finishView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(10, 2)));
+
+ $extension2->expects($this->once())
+ ->method('finishView')
+ ->with($this->anything(), $form, $options)
+ ->will($this->returnCallback($assertIndexAndNbOfChildViews(11, 2)));
+
+ $parentView = new FormView();
+ $view = $resolvedType->createView($form, $parentView);
+
+ $this->assertSame($parentView, $view->parent);
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getMockFormType()
+ {
+ return $this->getMock('Symfony\Component\Form\FormTypeInterface');
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getMockFormTypeExtension()
+ {
+ return $this->getMock('Symfony\Component\Form\FormTypeExtensionInterface');
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject
+ */
+ private function getMockFormFactory()
+ {
+ return $this->getMock('Symfony\Component\Form\FormFactoryInterface');
+ }
+
+ /**
+ * @param string $name
+ * @param array $options
+ *
+ * @return FormBuilder
+ */
+ protected function getBuilder($name = 'name', array $options = array())
+ {
+ return new FormBuilder($name, null, $this->dispatcher, $this->factory, $options);
+ }
+}
--- /dev/null
+<?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\Form\Tests;
+
+use Symfony\Component\Form\Form;
+use Symfony\Component\Form\FormEvent;
+use Symfony\Component\Form\FormEvents;
+use Symfony\Component\PropertyAccess\PropertyPath;
+use Symfony\Component\Form\FormConfigBuilder;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
+use Symfony\Component\Form\Tests\Fixtures\FixedFilterListener;
+
+class SimpleFormTest_Countable implements \Countable
+{
+ private $count;
+
+ public function __construct($count)
+ {
+ $this->count = $count;
+ }
+
+ public function count()
+ {
+ return $this->count;
+ }
+}
+
+class SimpleFormTest_Traversable implements \IteratorAggregate
+{
+ private $iterator;
+
+ public function __construct($count)
+ {
+ $this->iterator = new \ArrayIterator($count > 0 ? array_fill(0, $count, 'Foo') : array());
+ }
+
+ public function getIterator()
+ {
+ return $this->iterator;
+ }
+}
+
+class SimpleFormTest extends AbstractFormTest
+{
+ public function testDataIsInitializedToConfiguredValue()
+ {
+ $model = new FixedDataTransformer(array(
+ 'default' => 'foo',
+ ));
+ $view = new FixedDataTransformer(array(
+ 'foo' => 'bar',
+ ));
+
+ $config = new FormConfigBuilder('name', null, $this->dispatcher);
+ $config->addViewTransformer($view);
+ $config->addModelTransformer($model);
+ $config->setData('default');
+ $form = new Form($config);
+
+ $this->assertSame('default', $form->getData());
+ $this->assertSame('foo', $form->getNormData());
+ $this->assertSame('bar', $form->getViewData());
+ }
+
+ // https://github.com/symfony/symfony/commit/d4f4038f6daf7cf88ca7c7ab089473cce5ebf7d8#commitcomment-1632879
+ public function testDataIsInitializedFromSubmit()
+ {
+ $mock = $this->getMockBuilder('\stdClass')
+ ->setMethods(array('preSetData', 'preSubmit'))
+ ->getMock();
+ $mock->expects($this->at(0))
+ ->method('preSetData');
+ $mock->expects($this->at(1))
+ ->method('preSubmit');
+
+ $config = new FormConfigBuilder('name', null, $this->dispatcher);
+ $config->addEventListener(FormEvents::PRE_SET_DATA, array($mock, 'preSetData'));
+ $config->addEventListener(FormEvents::PRE_SUBMIT, array($mock, 'preSubmit'));
+ $form = new Form($config);
+
+ // no call to setData() or similar where the object would be
+ // initialized otherwise
+
+ $form->submit('foobar');
+ }
+
+ // https://github.com/symfony/symfony/pull/7789
+ public function testFalseIsConvertedToNull()
+ {
+ $mock = $this->getMockBuilder('\stdClass')
+ ->setMethods(array('preBind'))
+ ->getMock();
+ $mock->expects($this->once())
+ ->method('preBind')
+ ->with($this->callback(function ($event) {
+ return null === $event->getData();
+ }));
+
+ $config = new FormConfigBuilder('name', null, $this->dispatcher);
+ $config->addEventListener(FormEvents::PRE_BIND, array($mock, 'preBind'));
+ $form = new Form($config);
+
+ $form->bind(false);
+
+ $this->assertTrue($form->isValid());
+ $this->assertNull($form->getData());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\AlreadySubmittedException
+ */
+ public function testSubmitThrowsExceptionIfAlreadySubmitted()
+ {
+ $this->form->submit(array());
+ $this->form->submit(array());
+ }
+
+ public function testSubmitIsIgnoredIfDisabled()
+ {
+ $form = $this->getBuilder()
+ ->setDisabled(true)
+ ->setData('initial')
+ ->getForm();
+
+ $form->submit('new');
+
+ $this->assertEquals('initial', $form->getData());
+ $this->assertTrue($form->isSubmitted());
+ }
+
+ public function testNeverRequiredIfParentNotRequired()
+ {
+ $parent = $this->getBuilder()->setRequired(false)->getForm();
+ $child = $this->getBuilder()->setRequired(true)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertFalse($child->isRequired());
+ }
+
+ public function testRequired()
+ {
+ $parent = $this->getBuilder()->setRequired(true)->getForm();
+ $child = $this->getBuilder()->setRequired(true)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertTrue($child->isRequired());
+ }
+
+ public function testNotRequired()
+ {
+ $parent = $this->getBuilder()->setRequired(true)->getForm();
+ $child = $this->getBuilder()->setRequired(false)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertFalse($child->isRequired());
+ }
+
+ public function testAlwaysDisabledIfParentDisabled()
+ {
+ $parent = $this->getBuilder()->setDisabled(true)->getForm();
+ $child = $this->getBuilder()->setDisabled(false)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertTrue($child->isDisabled());
+ }
+
+ public function testDisabled()
+ {
+ $parent = $this->getBuilder()->setDisabled(false)->getForm();
+ $child = $this->getBuilder()->setDisabled(true)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertTrue($child->isDisabled());
+ }
+
+ public function testNotDisabled()
+ {
+ $parent = $this->getBuilder()->setDisabled(false)->getForm();
+ $child = $this->getBuilder()->setDisabled(false)->getForm();
+
+ $child->setParent($parent);
+
+ $this->assertFalse($child->isDisabled());
+ }
+
+ public function testGetRootReturnsRootOfParent()
+ {
+ $parent = $this->getMockForm();
+ $parent->expects($this->once())
+ ->method('getRoot')
+ ->will($this->returnValue('ROOT'));
+
+ $this->form->setParent($parent);
+
+ $this->assertEquals('ROOT', $this->form->getRoot());
+ }
+
+ public function testGetRootReturnsSelfIfNoParent()
+ {
+ $this->assertSame($this->form, $this->form->getRoot());
+ }
+
+ public function testEmptyIfEmptyArray()
+ {
+ $this->form->setData(array());
+
+ $this->assertTrue($this->form->isEmpty());
+ }
+
+ public function testEmptyIfEmptyCountable()
+ {
+ $this->form = new Form(new FormConfigBuilder('name', __NAMESPACE__.'\SimpleFormTest_Countable', $this->dispatcher));
+
+ $this->form->setData(new SimpleFormTest_Countable(0));
+
+ $this->assertTrue($this->form->isEmpty());
+ }
+
+ public function testNotEmptyIfFilledCountable()
+ {
+ $this->form = new Form(new FormConfigBuilder('name', __NAMESPACE__.'\SimpleFormTest_Countable', $this->dispatcher));
+
+ $this->form->setData(new SimpleFormTest_Countable(1));
+
+ $this->assertFalse($this->form->isEmpty());
+ }
+
+ public function testEmptyIfEmptyTraversable()
+ {
+ $this->form = new Form(new FormConfigBuilder('name', __NAMESPACE__.'\SimpleFormTest_Traversable', $this->dispatcher));
+
+ $this->form->setData(new SimpleFormTest_Traversable(0));
+
+ $this->assertTrue($this->form->isEmpty());
+ }
+
+ public function testNotEmptyIfFilledTraversable()
+ {
+ $this->form = new Form(new FormConfigBuilder('name', __NAMESPACE__.'\SimpleFormTest_Traversable', $this->dispatcher));
+
+ $this->form->setData(new SimpleFormTest_Traversable(1));
+
+ $this->assertFalse($this->form->isEmpty());
+ }
+
+ public function testEmptyIfNull()
+ {
+ $this->form->setData(null);
+
+ $this->assertTrue($this->form->isEmpty());
+ }
+
+ public function testEmptyIfEmptyString()
+ {
+ $this->form->setData('');
+
+ $this->assertTrue($this->form->isEmpty());
+ }
+
+ public function testNotEmptyIfText()
+ {
+ $this->form->setData('foobar');
+
+ $this->assertFalse($this->form->isEmpty());
+ }
+
+ public function testValidIfSubmitted()
+ {
+ $form = $this->getBuilder()->getForm();
+ $form->submit('foobar');
+
+ $this->assertTrue($form->isValid());
+ }
+
+ public function testValidIfSubmittedAndDisabled()
+ {
+ $form = $this->getBuilder()->setDisabled(true)->getForm();
+ $form->submit('foobar');
+
+ $this->assertTrue($form->isValid());
+ }
+
+ public function testNotValidIfNotSubmitted()
+ {
+ $this->assertFalse($this->form->isValid());
+ }
+
+ public function testNotValidIfErrors()
+ {
+ $form = $this->getBuilder()->getForm();
+ $form->submit('foobar');
+ $form->addError(new FormError('Error!'));
+
+ $this->assertFalse($form->isValid());
+ }
+
+ public function testHasErrors()
+ {
+ $this->form->addError(new FormError('Error!'));
+
+ $this->assertCount(1, $this->form->getErrors());
+ }
+
+ public function testHasNoErrors()
+ {
+ $this->assertCount(0, $this->form->getErrors());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\AlreadySubmittedException
+ */
+ public function testSetParentThrowsExceptionIfAlreadySubmitted()
+ {
+ $this->form->submit(array());
+ $this->form->setParent($this->getBuilder('parent')->getForm());
+ }
+
+ public function testSubmitted()
+ {
+ $form = $this->getBuilder()->getForm();
+ $form->submit('foobar');
+
+ $this->assertTrue($form->isSubmitted());
+ }
+
+ public function testNotSubmitted()
+ {
+ $this->assertFalse($this->form->isSubmitted());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\AlreadySubmittedException
+ */
+ public function testSetDataThrowsExceptionIfAlreadySubmitted()
+ {
+ $this->form->submit(array());
+ $this->form->setData(null);
+ }
+
+ public function testSetDataClonesObjectIfNotByReference()
+ {
+ $data = new \stdClass();
+ $form = $this->getBuilder('name', null, '\stdClass')->setByReference(false)->getForm();
+ $form->setData($data);
+
+ $this->assertNotSame($data, $form->getData());
+ $this->assertEquals($data, $form->getData());
+ }
+
+ public function testSetDataDoesNotCloneObjectIfByReference()
+ {
+ $data = new \stdClass();
+ $form = $this->getBuilder('name', null, '\stdClass')->setByReference(true)->getForm();
+ $form->setData($data);
+
+ $this->assertSame($data, $form->getData());
+ }
+
+ public function testSetDataExecutesTransformationChain()
+ {
+ // use real event dispatcher now
+ $form = $this->getBuilder('name', new EventDispatcher())
+ ->addEventSubscriber(new FixedFilterListener(array(
+ 'preSetData' => array(
+ 'app' => 'filtered',
+ ),
+ )))
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'filtered' => 'norm',
+ )))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'norm' => 'client',
+ )))
+ ->getForm();
+
+ $form->setData('app');
+
+ $this->assertEquals('filtered', $form->getData());
+ $this->assertEquals('norm', $form->getNormData());
+ $this->assertEquals('client', $form->getViewData());
+ }
+
+ public function testSetDataExecutesViewTransformersInOrder()
+ {
+ $form = $this->getBuilder()
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'first' => 'second',
+ )))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'second' => 'third',
+ )))
+ ->getForm();
+
+ $form->setData('first');
+
+ $this->assertEquals('third', $form->getViewData());
+ }
+
+ public function testSetDataExecutesModelTransformersInReverseOrder()
+ {
+ $form = $this->getBuilder()
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'second' => 'third',
+ )))
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'first' => 'second',
+ )))
+ ->getForm();
+
+ $form->setData('first');
+
+ $this->assertEquals('third', $form->getNormData());
+ }
+
+ /*
+ * When there is no data transformer, the data must have the same format
+ * in all three representations
+ */
+ public function testSetDataConvertsScalarToStringIfNoTransformer()
+ {
+ $form = $this->getBuilder()->getForm();
+
+ $form->setData(1);
+
+ $this->assertSame('1', $form->getData());
+ $this->assertSame('1', $form->getNormData());
+ $this->assertSame('1', $form->getViewData());
+ }
+
+ /*
+ * Data in client format should, if possible, always be a string to
+ * facilitate differentiation between '0' and ''
+ */
+ public function testSetDataConvertsScalarToStringIfOnlyModelTransformer()
+ {
+ $form = $this->getBuilder()
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 1 => 23,
+ )))
+ ->getForm();
+
+ $form->setData(1);
+
+ $this->assertSame(1, $form->getData());
+ $this->assertSame(23, $form->getNormData());
+ $this->assertSame('23', $form->getViewData());
+ }
+
+ /*
+ * NULL remains NULL in app and norm format to remove the need to treat
+ * empty values and NULL explicitly in the application
+ */
+ public function testSetDataConvertsNullToStringIfNoTransformer()
+ {
+ $form = $this->getBuilder()->getForm();
+
+ $form->setData(null);
+
+ $this->assertNull($form->getData());
+ $this->assertNull($form->getNormData());
+ $this->assertSame('', $form->getViewData());
+ }
+
+ public function testSetDataIsIgnoredIfDataIsLocked()
+ {
+ $form = $this->getBuilder()
+ ->setData('default')
+ ->setDataLocked(true)
+ ->getForm();
+
+ $form->setData('foobar');
+
+ $this->assertSame('default', $form->getData());
+ }
+
+ public function testSubmitConvertsEmptyToNullIfNoTransformer()
+ {
+ $form = $this->getBuilder()->getForm();
+
+ $form->submit('');
+
+ $this->assertNull($form->getData());
+ $this->assertNull($form->getNormData());
+ $this->assertSame('', $form->getViewData());
+ }
+
+ public function testSubmitExecutesTransformationChain()
+ {
+ // use real event dispatcher now
+ $form = $this->getBuilder('name', new EventDispatcher())
+ ->addEventSubscriber(new FixedFilterListener(array(
+ 'preSubmit' => array(
+ 'client' => 'filteredclient',
+ ),
+ 'onSubmit' => array(
+ 'norm' => 'filterednorm',
+ ),
+ )))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ // direction is reversed!
+ 'norm' => 'filteredclient',
+ 'filterednorm' => 'cleanedclient'
+ )))
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ // direction is reversed!
+ 'app' => 'filterednorm',
+ )))
+ ->getForm();
+
+ $form->submit('client');
+
+ $this->assertEquals('app', $form->getData());
+ $this->assertEquals('filterednorm', $form->getNormData());
+ $this->assertEquals('cleanedclient', $form->getViewData());
+ }
+
+ public function testSubmitExecutesViewTransformersInReverseOrder()
+ {
+ $form = $this->getBuilder()
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'third' => 'second',
+ )))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'second' => 'first',
+ )))
+ ->getForm();
+
+ $form->submit('first');
+
+ $this->assertEquals('third', $form->getNormData());
+ }
+
+ public function testSubmitExecutesModelTransformersInOrder()
+ {
+ $form = $this->getBuilder()
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'second' => 'first',
+ )))
+ ->addModelTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'third' => 'second',
+ )))
+ ->getForm();
+
+ $form->submit('first');
+
+ $this->assertEquals('third', $form->getData());
+ }
+
+ public function testSynchronizedByDefault()
+ {
+ $this->assertTrue($this->form->isSynchronized());
+ }
+
+ public function testSynchronizedAfterSubmission()
+ {
+ $this->form->submit('foobar');
+
+ $this->assertTrue($this->form->isSynchronized());
+ }
+
+ public function testNotSynchronizedIfViewReverseTransformationFailed()
+ {
+ $transformer = $this->getDataTransformer();
+ $transformer->expects($this->once())
+ ->method('reverseTransform')
+ ->will($this->throwException(new TransformationFailedException()));
+
+ $form = $this->getBuilder()
+ ->addViewTransformer($transformer)
+ ->getForm();
+
+ $form->submit('foobar');
+
+ $this->assertFalse($form->isSynchronized());
+ }
+
+ public function testNotSynchronizedIfModelReverseTransformationFailed()
+ {
+ $transformer = $this->getDataTransformer();
+ $transformer->expects($this->once())
+ ->method('reverseTransform')
+ ->will($this->throwException(new TransformationFailedException()));
+
+ $form = $this->getBuilder()
+ ->addModelTransformer($transformer)
+ ->getForm();
+
+ $form->submit('foobar');
+
+ $this->assertFalse($form->isSynchronized());
+ }
+
+ public function testEmptyDataCreatedBeforeTransforming()
+ {
+ $form = $this->getBuilder()
+ ->setEmptyData('foo')
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ // direction is reversed!
+ 'bar' => 'foo',
+ )))
+ ->getForm();
+
+ $form->submit('');
+
+ $this->assertEquals('bar', $form->getData());
+ }
+
+ public function testEmptyDataFromClosure()
+ {
+ $test = $this;
+ $form = $this->getBuilder()
+ ->setEmptyData(function ($form) use ($test) {
+ // the form instance is passed to the closure to allow use
+ // of form data when creating the empty value
+ $test->assertInstanceOf('Symfony\Component\Form\FormInterface', $form);
+
+ return 'foo';
+ })
+ ->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ // direction is reversed!
+ 'bar' => 'foo',
+ )))
+ ->getForm();
+
+ $form->submit('');
+
+ $this->assertEquals('bar', $form->getData());
+ }
+
+ public function testSubmitResetsErrors()
+ {
+ $this->form->addError(new FormError('Error!'));
+ $this->form->submit('foobar');
+
+ $this->assertSame(array(), $this->form->getErrors());
+ }
+
+ public function testCreateView()
+ {
+ $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ $view = $this->getMock('Symfony\Component\Form\FormView');
+ $form = $this->getBuilder()->setType($type)->getForm();
+
+ $type->expects($this->once())
+ ->method('createView')
+ ->with($form)
+ ->will($this->returnValue($view));
+
+ $this->assertSame($view, $form->createView());
+ }
+
+ public function testCreateViewWithParent()
+ {
+ $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ $view = $this->getMock('Symfony\Component\Form\FormView');
+ $parentForm = $this->getMock('Symfony\Component\Form\Test\FormInterface');
+ $parentView = $this->getMock('Symfony\Component\Form\FormView');
+ $form = $this->getBuilder()->setType($type)->getForm();
+ $form->setParent($parentForm);
+
+ $parentForm->expects($this->once())
+ ->method('createView')
+ ->will($this->returnValue($parentView));
+
+ $type->expects($this->once())
+ ->method('createView')
+ ->with($form, $parentView)
+ ->will($this->returnValue($view));
+
+ $this->assertSame($view, $form->createView());
+ }
+
+ public function testCreateViewWithExplicitParent()
+ {
+ $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface');
+ $view = $this->getMock('Symfony\Component\Form\FormView');
+ $parentView = $this->getMock('Symfony\Component\Form\FormView');
+ $form = $this->getBuilder()->setType($type)->getForm();
+
+ $type->expects($this->once())
+ ->method('createView')
+ ->with($form, $parentView)
+ ->will($this->returnValue($view));
+
+ $this->assertSame($view, $form->createView($parentView));
+ }
+
+ public function testGetErrorsAsString()
+ {
+ $this->form->addError(new FormError('Error!'));
+
+ $this->assertEquals("ERROR: Error!\n", $this->form->getErrorsAsString());
+ }
+
+ public function testFormCanHaveEmptyName()
+ {
+ $form = $this->getBuilder('')->getForm();
+
+ $this->assertEquals('', $form->getName());
+ }
+
+ public function testSetNullParentWorksWithEmptyName()
+ {
+ $form = $this->getBuilder('')->getForm();
+ $form->setParent(null);
+
+ $this->assertNull($form->getParent());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\LogicException
+ * @expectedExceptionMessage A form with an empty name cannot have a parent form.
+ */
+ public function testFormCannotHaveEmptyNameNotInRootLevel()
+ {
+ $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->add($this->getBuilder(''))
+ ->getForm();
+ }
+
+ public function testGetPropertyPathReturnsConfiguredPath()
+ {
+ $form = $this->getBuilder()->setPropertyPath('address.street')->getForm();
+
+ $this->assertEquals(new PropertyPath('address.street'), $form->getPropertyPath());
+ }
+
+ // see https://github.com/symfony/symfony/issues/3903
+ public function testGetPropertyPathDefaultsToNameIfParentHasDataClass()
+ {
+ $parent = $this->getBuilder(null, null, 'stdClass')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getBuilder('name')->getForm();
+ $parent->add($form);
+
+ $this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
+ }
+
+ // see https://github.com/symfony/symfony/issues/3903
+ public function testGetPropertyPathDefaultsToIndexedNameIfParentDataClassIsNull()
+ {
+ $parent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $form = $this->getBuilder('name')->getForm();
+ $parent->add($form);
+
+ $this->assertEquals(new PropertyPath('[name]'), $form->getPropertyPath());
+ }
+
+ public function testGetPropertyPathDefaultsToNameIfFirstParentWithoutInheritDataHasDataClass()
+ {
+ $grandParent = $this->getBuilder(null, null, 'stdClass')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $parent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setInheritData(true)
+ ->getForm();
+ $form = $this->getBuilder('name')->getForm();
+ $grandParent->add($parent);
+ $parent->add($form);
+
+ $this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
+ }
+
+ public function testGetPropertyPathDefaultsToIndexedNameIfDataClassOfFirstParentWithoutInheritDataIsNull()
+ {
+ $grandParent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->getForm();
+ $parent = $this->getBuilder()
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setInheritData(true)
+ ->getForm();
+ $form = $this->getBuilder('name')->getForm();
+ $grandParent->add($parent);
+ $parent->add($form);
+
+ $this->assertEquals(new PropertyPath('[name]'), $form->getPropertyPath());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\LogicException
+ */
+ public function testViewDataMustNotBeObjectIfDataClassIsNull()
+ {
+ $config = new FormConfigBuilder('name', null, $this->dispatcher);
+ $config->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => new \stdClass(),
+ )));
+ $form = new Form($config);
+
+ $form->setData('foo');
+ }
+
+ public function testViewDataMayBeArrayAccessIfDataClassIsNull()
+ {
+ $arrayAccess = $this->getMock('\ArrayAccess');
+ $config = new FormConfigBuilder('name', null, $this->dispatcher);
+ $config->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => $arrayAccess,
+ )));
+ $form = new Form($config);
+
+ $form->setData('foo');
+
+ $this->assertSame($arrayAccess, $form->getViewData());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\LogicException
+ */
+ public function testViewDataMustBeObjectIfDataClassIsSet()
+ {
+ $config = new FormConfigBuilder('name', 'stdClass', $this->dispatcher);
+ $config->addViewTransformer(new FixedDataTransformer(array(
+ '' => '',
+ 'foo' => array('bar' => 'baz'),
+ )));
+ $form = new Form($config);
+
+ $form->setData('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testSetDataCannotInvokeItself()
+ {
+ // Cycle detection to prevent endless loops
+ $config = new FormConfigBuilder('name', 'stdClass', $this->dispatcher);
+ $config->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
+ $event->getForm()->setData('bar');
+ });
+ $form = new Form($config);
+
+ $form->setData('foo');
+ }
+
+ public function testSubmittingWrongDataIsIgnored()
+ {
+ $test = $this;
+
+ $child = $this->getBuilder('child', $this->dispatcher);
+ $child->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($test) {
+ // child form doesn't receive the wrong data that is submitted on parent
+ $test->assertNull($event->getData());
+ });
+
+ $parent = $this->getBuilder('parent', new EventDispatcher())
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->add($child)
+ ->getForm();
+
+ $parent->submit('not-an-array');
+ }
+
+ public function testHandleRequestForwardsToRequestHandler()
+ {
+ $handler = $this->getMock('Symfony\Component\Form\RequestHandlerInterface');
+
+ $form = $this->getBuilder()
+ ->setRequestHandler($handler)
+ ->getForm();
+
+ $handler->expects($this->once())
+ ->method('handleRequest')
+ ->with($this->identicalTo($form), 'REQUEST');
+
+ $this->assertSame($form, $form->handleRequest('REQUEST'));
+ }
+
+ public function testFormInheritsParentData()
+ {
+ $child = $this->getBuilder('child')
+ ->setInheritData(true);
+
+ $parent = $this->getBuilder('parent')
+ ->setCompound(true)
+ ->setDataMapper($this->getDataMapper())
+ ->setData('foo')
+ ->addModelTransformer(new FixedDataTransformer(array(
+ 'foo' => 'norm[foo]',
+ )))
+ ->addViewTransformer(new FixedDataTransformer(array(
+ 'norm[foo]' => 'view[foo]',
+ )))
+ ->add($child)
+ ->getForm();
+
+ $this->assertSame('foo', $parent->get('child')->getData());
+ $this->assertSame('norm[foo]', $parent->get('child')->getNormData());
+ $this->assertSame('view[foo]', $parent->get('child')->getViewData());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testInheritDataDisallowsSetData()
+ {
+ $form = $this->getBuilder()
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->setData('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testGetDataRequiresParentToBeSetIfInheritData()
+ {
+ $form = $this->getBuilder()
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->getData();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testGetNormDataRequiresParentToBeSetIfInheritData()
+ {
+ $form = $this->getBuilder()
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->getNormData();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testGetViewDataRequiresParentToBeSetIfInheritData()
+ {
+ $form = $this->getBuilder()
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->getViewData();
+ }
+
+ public function testPostSubmitDataIsNullIfInheritData()
+ {
+ $test = $this;
+ $form = $this->getBuilder()
+ ->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($test) {
+ $test->assertNull($event->getData());
+ })
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->submit('foo');
+ }
+
+ public function testSubmitIsNeverFiredIfInheritData()
+ {
+ $test = $this;
+ $form = $this->getBuilder()
+ ->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use ($test) {
+ $test->fail('The SUBMIT event should not be fired');
+ })
+ ->setInheritData(true)
+ ->getForm();
+
+ $form->submit('foo');
+ }
+
+ public function testInitializeSetsDefaultData()
+ {
+ $config = $this->getBuilder()->setData('DEFAULT')->getFormConfig();
+ $form = $this->getMock('Symfony\Component\Form\Form', array('setData'), array($config));
+
+ $form->expects($this->once())
+ ->method('setData')
+ ->with($this->identicalTo('DEFAULT'));
+
+ /* @var Form $form */
+ $form->initialize();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Form\Exception\RuntimeException
+ */
+ public function testInitializeFailsIfParent()
+ {
+ $parent = $this->getBuilder()->setRequired(false)->getForm();
+ $child = $this->getBuilder()->setRequired(true)->getForm();
+
+ $child->setParent($parent);
+
+ $child->initialize();
+ }
+
+ protected function createForm()
+ {
+ return $this->getBuilder()->getForm();
+ }
+}
--- /dev/null
+<?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\Form\Util;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormUtil
+{
+ /**
+ * This class should not be instantiated
+ */
+ private function __construct() {}
+
+ /**
+ * Returns whether the given data is empty.
+ *
+ * This logic is reused multiple times throughout the processing of
+ * a form and needs to be consistent. PHP's keyword `empty` cannot
+ * be used as it also considers 0 and "0" to be empty.
+ *
+ * @param mixed $data
+ *
+ * @return Boolean
+ */
+ public static function isEmpty($data)
+ {
+ // Should not do a check for array() === $data!!!
+ // This method is used in occurrences where arrays are
+ // not considered to be empty, ever.
+ return null === $data || '' === $data;
+ }
+}
--- /dev/null
+<?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\Form\Util;
+
+/**
+ * Iterator that returns only forms from a form tree that do not inherit their
+ * parent data.
+ *
+ * If the iterator encounters a form that inherits its parent data, it enters
+ * the form and traverses its children as well.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class InheritDataAwareIterator extends VirtualFormAwareIterator
+{
+ /**
+ * Creates a new iterator.
+ *
+ * @param \Symfony\Component\Form\FormInterface[] $forms An array
+ */
+ public function __construct(array $forms)
+ {
+ // Skip the deprecation error
+ \ArrayIterator::__construct($forms);
+ }
+}
--- /dev/null
+<?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\Form\Util;
+
+/**
+ * Iterator that returns only forms from a form tree that do not inherit their
+ * parent data.
+ *
+ * If the iterator encounters a form that inherits its parent data, it enters
+ * the form and traverses its children as well.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link InheritDataAwareIterator} instead.
+ */
+class VirtualFormAwareIterator extends \ArrayIterator implements \RecursiveIterator
+{
+ /**
+ * Creates a new iterator.
+ *
+ * @param \Symfony\Component\Form\FormInterface[] $forms An array
+ */
+ public function __construct(array $forms)
+ {
+ // Uncomment this as soon as the deprecation note should be shown
+ // trigger_error('VirtualFormAwareIterator is deprecated since version 2.3 and will be removed in 3.0. Use InheritDataAwareIterator instead.', E_USER_DEPRECATED);
+
+ parent::__construct($forms);
+ }
+
+ public function getChildren()
+ {
+ return new static($this->current()->all());
+ }
+
+ public function hasChildren()
+ {
+ return $this->current()->getConfig()->getInheritData();
+ }
+}
--- /dev/null
+{
+ "name": "symfony/form",
+ "type": "library",
+ "description": "Symfony Form 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",
+ "symfony/event-dispatcher": "~2.1",
+ "symfony/intl": "~2.3",
+ "symfony/options-resolver": "~2.1",
+ "symfony/property-access": "~2.2"
+ },
+ "require-dev": {
+ "symfony/validator": "~2.2",
+ "symfony/http-foundation": "~2.2"
+ },
+ "suggest": {
+ "symfony/validator": "",
+ "symfony/http-foundation": ""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Form\\": "" }
+ },
+ "target-dir": "Symfony/Component/Form",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Form Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Tests</directory>
+ <directory>./vendor</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
--- /dev/null
+<?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\Icu;
+
+use Symfony\Component\Intl\ResourceBundle\CurrencyBundle;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface;
+
+/**
+ * A stub implementation of {@link \Symfony\Component\Intl\ResourceBundle\CurrencyBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuCurrencyBundle extends CurrencyBundle
+{
+ public function __construct(StructuredBundleReaderInterface $reader)
+ {
+ parent::__construct(realpath(IcuData::getResourceDirectory() . '/curr'), $reader);
+ }
+}
--- /dev/null
+<?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\Icu;
+
+use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuData
+{
+ /**
+ * Returns the version of the bundled ICU data.
+ *
+ * @return string The version string.
+ */
+ public static function getVersion()
+ {
+ return trim(file_get_contents(__DIR__ . '/Resources/data/version.txt'));
+ }
+
+ /**
+ * Returns whether the ICU data is stubbed.
+ *
+ * @return Boolean Returns true if the ICU data is stubbed, false if it is
+ * loaded from ICU .res files.
+ */
+ public static function isStubbed()
+ {
+ return true;
+ }
+
+ /**
+ * Returns the path to the directory where the resource bundles are stored.
+ *
+ * @return string The absolute path to the resource directory.
+ */
+ public static function getResourceDirectory()
+ {
+ return realpath(__DIR__ . '/Resources/data');
+ }
+
+ /**
+ * Returns a reader for reading resource bundles in this component.
+ *
+ * @return \Symfony\Component\Intl\ResourceBundle\Reader\BundleReaderInterface
+ */
+ public static function getBundleReader()
+ {
+ return new PhpBundleReader();
+ }
+
+ /**
+ * This class must not be instantiated.
+ */
+ private function __construct() {}
+}
--- /dev/null
+<?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\Icu;
+
+use Symfony\Component\Intl\ResourceBundle\LanguageBundle;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface;
+
+/**
+ * A stub implementation of {@link \Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuLanguageBundle extends LanguageBundle
+{
+ public function __construct(StructuredBundleReaderInterface $reader)
+ {
+ parent::__construct(realpath(IcuData::getResourceDirectory() . '/lang'), $reader);
+ }
+}
--- /dev/null
+<?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\Icu;
+
+use Symfony\Component\Intl\ResourceBundle\LocaleBundle;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface;
+
+/**
+ * A stub implementation of {@link \Symfony\Component\Intl\ResourceBundle\LocaleBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuLocaleBundle extends LocaleBundle
+{
+ public function __construct(StructuredBundleReaderInterface $reader)
+ {
+ parent::__construct(realpath(IcuData::getResourceDirectory() . '/locales'), $reader);
+ }
+}
--- /dev/null
+<?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\Icu;
+
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface;
+use Symfony\Component\Intl\ResourceBundle\RegionBundle;
+
+/**
+ * A stub implementation of {@link \Symfony\Component\Intl\ResourceBundle\RegionBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuRegionBundle extends RegionBundle
+{
+ public function __construct(StructuredBundleReaderInterface $reader)
+ {
+ parent::__construct(realpath(IcuData::getResourceDirectory() . '/region'), $reader);
+ }
+}
--- /dev/null
+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.
--- /dev/null
+Icu Component
+=============
+
+Contains data of the ICU library in a specific version.
+
+You should not directly use this component. Use it through the API of the
+[Intl component] [1] instead.
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Icu/
+ $ composer.phar install --dev
+ $ phpunit
+
+[1]: https://github.com/symfony/Intl
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Currencies' => array(
+ 'XUA' => array(
+ 0 => 'ADB Unit of Account',
+ 1 => 'XUA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AFN' => array(
+ 0 => 'Afghan Afghani',
+ 1 => 'AFN',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'AFA' => array(
+ 0 => 'Afghan Afghani (1927-2002)',
+ 1 => 'AFA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ALL' => array(
+ 0 => 'Albanian Lek',
+ 1 => 'ALL',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ALK' => array(
+ 0 => 'Albanian Lek (1946-1965)',
+ 1 => 'ALK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'DZD' => array(
+ 0 => 'Algerian Dinar',
+ 1 => 'DZD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ADP' => array(
+ 0 => 'Andorran Peseta',
+ 1 => 'ADP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'AOA' => array(
+ 0 => 'Angolan Kwanza',
+ 1 => 'AOA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AOK' => array(
+ 0 => 'Angolan Kwanza (1977-1991)',
+ 1 => 'AOK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AON' => array(
+ 0 => 'Angolan New Kwanza (1990-2000)',
+ 1 => 'AON',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AOR' => array(
+ 0 => 'Angolan Readjusted Kwanza (1995-1999)',
+ 1 => 'AOR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ARA' => array(
+ 0 => 'Argentine Austral',
+ 1 => 'ARA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ARS' => array(
+ 0 => 'Argentine Peso',
+ 1 => 'ARS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ARM' => array(
+ 0 => 'Argentine Peso (1881-1970)',
+ 1 => 'ARM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ARP' => array(
+ 0 => 'Argentine Peso (1983-1985)',
+ 1 => 'ARP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ARL' => array(
+ 0 => 'Argentine Peso Ley (1970-1983)',
+ 1 => 'ARL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AMD' => array(
+ 0 => 'Armenian Dram',
+ 1 => 'AMD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'AWG' => array(
+ 0 => 'Aruban Florin',
+ 1 => 'AWG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AUD' => array(
+ 0 => 'Australian Dollar',
+ 1 => 'A$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ATS' => array(
+ 0 => 'Austrian Schilling',
+ 1 => 'ATS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AZN' => array(
+ 0 => 'Azerbaijani Manat',
+ 1 => 'AZN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AZM' => array(
+ 0 => 'Azerbaijani Manat (1993-2006)',
+ 1 => 'AZM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BSD' => array(
+ 0 => 'Bahamian Dollar',
+ 1 => 'BSD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BHD' => array(
+ 0 => 'Bahraini Dinar',
+ 1 => 'BHD',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'BDT' => array(
+ 0 => 'Bangladeshi Taka',
+ 1 => 'BDT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BBD' => array(
+ 0 => 'Barbadian Dollar',
+ 1 => 'BBD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BYB' => array(
+ 0 => 'Belarusian New Ruble (1994-1999)',
+ 1 => 'BYB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BYR' => array(
+ 0 => 'Belarusian Ruble',
+ 1 => 'BYR',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'BEF' => array(
+ 0 => 'Belgian Franc',
+ 1 => 'BEF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BEC' => array(
+ 0 => 'Belgian Franc (convertible)',
+ 1 => 'BEC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BEL' => array(
+ 0 => 'Belgian Franc (financial)',
+ 1 => 'BEL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BZD' => array(
+ 0 => 'Belize Dollar',
+ 1 => 'BZD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BMD' => array(
+ 0 => 'Bermudan Dollar',
+ 1 => 'BMD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BTN' => array(
+ 0 => 'Bhutanese Ngultrum',
+ 1 => 'BTN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BOB' => array(
+ 0 => 'Bolivian Boliviano',
+ 1 => 'BOB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BOL' => array(
+ 0 => 'Bolivian Boliviano (1863-1963)',
+ 1 => 'BOL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BOV' => array(
+ 0 => 'Bolivian Mvdol',
+ 1 => 'BOV',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BOP' => array(
+ 0 => 'Bolivian Peso',
+ 1 => 'BOP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BAM' => array(
+ 0 => 'Bosnia-Herzegovina Convertible Mark',
+ 1 => 'BAM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BAD' => array(
+ 0 => 'Bosnia-Herzegovina Dinar (1992-1994)',
+ 1 => 'BAD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BAN' => array(
+ 0 => 'Bosnia-Herzegovina New Dinar (1994-1997)',
+ 1 => 'BAN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BWP' => array(
+ 0 => 'Botswanan Pula',
+ 1 => 'BWP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRC' => array(
+ 0 => 'Brazilian Cruzado (1986-1989)',
+ 1 => 'BRC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRZ' => array(
+ 0 => 'Brazilian Cruzeiro (1942-1967)',
+ 1 => 'BRZ',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRE' => array(
+ 0 => 'Brazilian Cruzeiro (1990-1993)',
+ 1 => 'BRE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRR' => array(
+ 0 => 'Brazilian Cruzeiro (1993-1994)',
+ 1 => 'BRR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRN' => array(
+ 0 => 'Brazilian New Cruzado (1989-1990)',
+ 1 => 'BRN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRB' => array(
+ 0 => 'Brazilian New Cruzeiro (1967-1986)',
+ 1 => 'BRB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BRL' => array(
+ 0 => 'Brazilian Real',
+ 1 => 'R$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GBP' => array(
+ 0 => 'British Pound Sterling',
+ 1 => '£',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BND' => array(
+ 0 => 'Brunei Dollar',
+ 1 => 'BND',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BGL' => array(
+ 0 => 'Bulgarian Hard Lev',
+ 1 => 'BGL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BGN' => array(
+ 0 => 'Bulgarian Lev',
+ 1 => 'BGN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BGO' => array(
+ 0 => 'Bulgarian Lev (1879-1952)',
+ 1 => 'BGO',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BGM' => array(
+ 0 => 'Bulgarian Socialist Lev',
+ 1 => 'BGM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BUK' => array(
+ 0 => 'Burmese Kyat',
+ 1 => 'BUK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'BIF' => array(
+ 0 => 'Burundian Franc',
+ 1 => 'BIF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'KHR' => array(
+ 0 => 'Cambodian Riel',
+ 1 => 'KHR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CAD' => array(
+ 0 => 'Canadian Dollar',
+ 1 => 'CA$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CVE' => array(
+ 0 => 'Cape Verdean Escudo',
+ 1 => 'CVE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KYD' => array(
+ 0 => 'Cayman Islands Dollar',
+ 1 => 'KYD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XOF' => array(
+ 0 => 'CFA Franc BCEAO',
+ 1 => 'CFA',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'XAF' => array(
+ 0 => 'CFA Franc BEAC',
+ 1 => 'FCFA',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'XPF' => array(
+ 0 => 'CFP Franc',
+ 1 => 'CFPF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'CLE' => array(
+ 0 => 'Chilean Escudo',
+ 1 => 'CLE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CLP' => array(
+ 0 => 'Chilean Peso',
+ 1 => 'CLP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'CLF' => array(
+ 0 => 'Chilean Unit of Account (UF)',
+ 1 => 'CLF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'CNX' => array(
+ 0 => 'Chinese People’s Bank Dollar',
+ 1 => 'CNX',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CNY' => array(
+ 0 => 'Chinese Yuan',
+ 1 => 'CN¥',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'COP' => array(
+ 0 => 'Colombian Peso',
+ 1 => 'COP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'COU' => array(
+ 0 => 'Colombian Real Value Unit',
+ 1 => 'COU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KMF' => array(
+ 0 => 'Comorian Franc',
+ 1 => 'KMF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'CDF' => array(
+ 0 => 'Congolese Franc',
+ 1 => 'CDF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CRC' => array(
+ 0 => 'Costa Rican Colón',
+ 1 => 'CRC',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'HRD' => array(
+ 0 => 'Croatian Dinar',
+ 1 => 'HRD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'HRK' => array(
+ 0 => 'Croatian Kuna',
+ 1 => 'HRK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CUC' => array(
+ 0 => 'Cuban Convertible Peso',
+ 1 => 'CUC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CUP' => array(
+ 0 => 'Cuban Peso',
+ 1 => 'CUP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CYP' => array(
+ 0 => 'Cypriot Pound',
+ 1 => 'CYP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CZK' => array(
+ 0 => 'Czech Republic Koruna',
+ 1 => 'CZK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CSK' => array(
+ 0 => 'Czechoslovak Hard Koruna',
+ 1 => 'CSK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'DKK' => array(
+ 0 => 'Danish Krone',
+ 1 => 'DKK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'DJF' => array(
+ 0 => 'Djiboutian Franc',
+ 1 => 'DJF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'DOP' => array(
+ 0 => 'Dominican Peso',
+ 1 => 'DOP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NLG' => array(
+ 0 => 'Dutch Guilder',
+ 1 => 'NLG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XCD' => array(
+ 0 => 'East Caribbean Dollar',
+ 1 => 'EC$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'DDM' => array(
+ 0 => 'East German Mark',
+ 1 => 'DDM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ECS' => array(
+ 0 => 'Ecuadorian Sucre',
+ 1 => 'ECS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ECV' => array(
+ 0 => 'Ecuadorian Unit of Constant Value',
+ 1 => 'ECV',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'EGP' => array(
+ 0 => 'Egyptian Pound',
+ 1 => 'EGP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GQE' => array(
+ 0 => 'Equatorial Guinean Ekwele',
+ 1 => 'GQE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ERN' => array(
+ 0 => 'Eritrean Nakfa',
+ 1 => 'ERN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'EEK' => array(
+ 0 => 'Estonian Kroon',
+ 1 => 'EEK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ETB' => array(
+ 0 => 'Ethiopian Birr',
+ 1 => 'ETB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'EUR' => array(
+ 0 => 'Euro',
+ 1 => '€',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XBA' => array(
+ 0 => 'European Composite Unit',
+ 1 => 'XBA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XEU' => array(
+ 0 => 'European Currency Unit',
+ 1 => 'XEU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XBB' => array(
+ 0 => 'European Monetary Unit',
+ 1 => 'XBB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XBC' => array(
+ 0 => 'European Unit of Account (XBC)',
+ 1 => 'XBC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XBD' => array(
+ 0 => 'European Unit of Account (XBD)',
+ 1 => 'XBD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'FKP' => array(
+ 0 => 'Falkland Islands Pound',
+ 1 => 'FKP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'FJD' => array(
+ 0 => 'Fijian Dollar',
+ 1 => 'FJD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'FIM' => array(
+ 0 => 'Finnish Markka',
+ 1 => 'FIM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'FRF' => array(
+ 0 => 'French Franc',
+ 1 => 'FRF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XFO' => array(
+ 0 => 'French Gold Franc',
+ 1 => 'XFO',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XFU' => array(
+ 0 => 'French UIC-Franc',
+ 1 => 'XFU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GMD' => array(
+ 0 => 'Gambian Dalasi',
+ 1 => 'GMD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GEK' => array(
+ 0 => 'Georgian Kupon Larit',
+ 1 => 'GEK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GEL' => array(
+ 0 => 'Georgian Lari',
+ 1 => 'GEL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'DEM' => array(
+ 0 => 'German Mark',
+ 1 => 'DEM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GHS' => array(
+ 0 => 'Ghanaian Cedi',
+ 1 => 'GHS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GHC' => array(
+ 0 => 'Ghanaian Cedi (1979-2007)',
+ 1 => 'GHC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GIP' => array(
+ 0 => 'Gibraltar Pound',
+ 1 => 'GIP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XAU' => array(
+ 0 => 'Gold',
+ 1 => 'XAU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GRD' => array(
+ 0 => 'Greek Drachma',
+ 1 => 'GRD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GTQ' => array(
+ 0 => 'Guatemalan Quetzal',
+ 1 => 'GTQ',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GWP' => array(
+ 0 => 'Guinea-Bissau Peso',
+ 1 => 'GWP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GNF' => array(
+ 0 => 'Guinean Franc',
+ 1 => 'GNF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'GNS' => array(
+ 0 => 'Guinean Syli',
+ 1 => 'GNS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GYD' => array(
+ 0 => 'Guyanaese Dollar',
+ 1 => 'GYD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'HTG' => array(
+ 0 => 'Haitian Gourde',
+ 1 => 'HTG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'HNL' => array(
+ 0 => 'Honduran Lempira',
+ 1 => 'HNL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'HKD' => array(
+ 0 => 'Hong Kong Dollar',
+ 1 => 'HK$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'HUF' => array(
+ 0 => 'Hungarian Forint',
+ 1 => 'HUF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ISK' => array(
+ 0 => 'Icelandic Króna',
+ 1 => 'ISK',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ISJ' => array(
+ 0 => 'Icelandic Króna (1918-1981)',
+ 1 => 'ISJ',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'INR' => array(
+ 0 => 'Indian Rupee',
+ 1 => '₹',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'IDR' => array(
+ 0 => 'Indonesian Rupiah',
+ 1 => 'IDR',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'IRR' => array(
+ 0 => 'Iranian Rial',
+ 1 => 'IRR',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'IQD' => array(
+ 0 => 'Iraqi Dinar',
+ 1 => 'IQD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'IEP' => array(
+ 0 => 'Irish Pound',
+ 1 => 'IEP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ILS' => array(
+ 0 => 'Israeli New Sheqel',
+ 1 => '₪',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ILP' => array(
+ 0 => 'Israeli Pound',
+ 1 => 'ILP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ILR' => array(
+ 0 => 'Israeli Sheqel (1980-1985)',
+ 1 => 'ILR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ITL' => array(
+ 0 => 'Italian Lira',
+ 1 => 'ITL',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'JMD' => array(
+ 0 => 'Jamaican Dollar',
+ 1 => 'JMD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'JPY' => array(
+ 0 => 'Japanese Yen',
+ 1 => '¥',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'JOD' => array(
+ 0 => 'Jordanian Dinar',
+ 1 => 'JOD',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'KZT' => array(
+ 0 => 'Kazakhstani Tenge',
+ 1 => 'KZT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KES' => array(
+ 0 => 'Kenyan Shilling',
+ 1 => 'KES',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KWD' => array(
+ 0 => 'Kuwaiti Dinar',
+ 1 => 'KWD',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'KGS' => array(
+ 0 => 'Kyrgystani Som',
+ 1 => 'KGS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LAK' => array(
+ 0 => 'Laotian Kip',
+ 1 => 'LAK',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'LVL' => array(
+ 0 => 'Latvian Lats',
+ 1 => 'LVL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LVR' => array(
+ 0 => 'Latvian Ruble',
+ 1 => 'LVR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LBP' => array(
+ 0 => 'Lebanese Pound',
+ 1 => 'LBP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'LSL' => array(
+ 0 => 'Lesotho Loti',
+ 1 => 'LSL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LRD' => array(
+ 0 => 'Liberian Dollar',
+ 1 => 'LRD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LYD' => array(
+ 0 => 'Libyan Dinar',
+ 1 => 'LYD',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'LTL' => array(
+ 0 => 'Lithuanian Litas',
+ 1 => 'LTL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LTT' => array(
+ 0 => 'Lithuanian Talonas',
+ 1 => 'LTT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LUL' => array(
+ 0 => 'Luxembourg Financial Franc',
+ 1 => 'LUL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LUC' => array(
+ 0 => 'Luxembourgian Convertible Franc',
+ 1 => 'LUC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LUF' => array(
+ 0 => 'Luxembourgian Franc',
+ 1 => 'LUF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MOP' => array(
+ 0 => 'Macanese Pataca',
+ 1 => 'MOP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MKD' => array(
+ 0 => 'Macedonian Denar',
+ 1 => 'MKD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MKN' => array(
+ 0 => 'Macedonian Denar (1992-1993)',
+ 1 => 'MKN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MGA' => array(
+ 0 => 'Malagasy Ariary',
+ 1 => 'MGA',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MGF' => array(
+ 0 => 'Malagasy Franc',
+ 1 => 'MGF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MWK' => array(
+ 0 => 'Malawian Kwacha',
+ 1 => 'MWK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MYR' => array(
+ 0 => 'Malaysian Ringgit',
+ 1 => 'MYR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MVR' => array(
+ 0 => 'Maldivian Rufiyaa',
+ 1 => 'MVR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MVP' => array(
+ 0 => 'Maldivian Rupee',
+ 1 => 'MVP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MLF' => array(
+ 0 => 'Malian Franc',
+ 1 => 'MLF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MTL' => array(
+ 0 => 'Maltese Lira',
+ 1 => 'MTL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MTP' => array(
+ 0 => 'Maltese Pound',
+ 1 => 'MTP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MRO' => array(
+ 0 => 'Mauritanian Ouguiya',
+ 1 => 'MRO',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MUR' => array(
+ 0 => 'Mauritian Rupee',
+ 1 => 'MUR',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MXV' => array(
+ 0 => 'Mexican Investment Unit',
+ 1 => 'MXV',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MXN' => array(
+ 0 => 'Mexican Peso',
+ 1 => 'MX$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MXP' => array(
+ 0 => 'Mexican Silver Peso (1861-1992)',
+ 1 => 'MXP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MDC' => array(
+ 0 => 'Moldovan Cupon',
+ 1 => 'MDC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MDL' => array(
+ 0 => 'Moldovan Leu',
+ 1 => 'MDL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MCF' => array(
+ 0 => 'Monegasque Franc',
+ 1 => 'MCF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MNT' => array(
+ 0 => 'Mongolian Tugrik',
+ 1 => 'MNT',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'MAD' => array(
+ 0 => 'Moroccan Dirham',
+ 1 => 'MAD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MAF' => array(
+ 0 => 'Moroccan Franc',
+ 1 => 'MAF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MZE' => array(
+ 0 => 'Mozambican Escudo',
+ 1 => 'MZE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MZN' => array(
+ 0 => 'Mozambican Metical',
+ 1 => 'MZN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MZM' => array(
+ 0 => 'Mozambican Metical (1980-2006)',
+ 1 => 'MZM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'MMK' => array(
+ 0 => 'Myanma Kyat',
+ 1 => 'MMK',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'NAD' => array(
+ 0 => 'Namibian Dollar',
+ 1 => 'NAD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NPR' => array(
+ 0 => 'Nepalese Rupee',
+ 1 => 'NPR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ANG' => array(
+ 0 => 'Netherlands Antillean Guilder',
+ 1 => 'ANG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TWD' => array(
+ 0 => 'New Taiwan Dollar',
+ 1 => 'NT$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NZD' => array(
+ 0 => 'New Zealand Dollar',
+ 1 => 'NZ$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NIO' => array(
+ 0 => 'Nicaraguan Córdoba',
+ 1 => 'NIO',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NIC' => array(
+ 0 => 'Nicaraguan Córdoba (1988-1991)',
+ 1 => 'NIC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'NGN' => array(
+ 0 => 'Nigerian Naira',
+ 1 => 'NGN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KPW' => array(
+ 0 => 'North Korean Won',
+ 1 => 'KPW',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'NOK' => array(
+ 0 => 'Norwegian Krone',
+ 1 => 'NOK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'OMR' => array(
+ 0 => 'Omani Rial',
+ 1 => 'OMR',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'PKR' => array(
+ 0 => 'Pakistani Rupee',
+ 1 => 'PKR',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'XPD' => array(
+ 0 => 'Palladium',
+ 1 => 'XPD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PAB' => array(
+ 0 => 'Panamanian Balboa',
+ 1 => 'PAB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PGK' => array(
+ 0 => 'Papua New Guinean Kina',
+ 1 => 'PGK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PYG' => array(
+ 0 => 'Paraguayan Guarani',
+ 1 => 'PYG',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'PEI' => array(
+ 0 => 'Peruvian Inti',
+ 1 => 'PEI',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PEN' => array(
+ 0 => 'Peruvian Nuevo Sol',
+ 1 => 'PEN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PES' => array(
+ 0 => 'Peruvian Sol (1863-1965)',
+ 1 => 'PES',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PHP' => array(
+ 0 => 'Philippine Peso',
+ 1 => 'PHP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XPT' => array(
+ 0 => 'Platinum',
+ 1 => 'XPT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PLN' => array(
+ 0 => 'Polish Zloty',
+ 1 => 'PLN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PLZ' => array(
+ 0 => 'Polish Zloty (1950-1995)',
+ 1 => 'PLZ',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'PTE' => array(
+ 0 => 'Portuguese Escudo',
+ 1 => 'PTE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'GWE' => array(
+ 0 => 'Portuguese Guinea Escudo',
+ 1 => 'GWE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'QAR' => array(
+ 0 => 'Qatari Rial',
+ 1 => 'QAR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RHD' => array(
+ 0 => 'Rhodesian Dollar',
+ 1 => 'RHD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XRE' => array(
+ 0 => 'RINET Funds',
+ 1 => 'XRE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RON' => array(
+ 0 => 'Romanian Leu',
+ 1 => 'RON',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ROL' => array(
+ 0 => 'Romanian Leu (1952-2006)',
+ 1 => 'ROL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RUB' => array(
+ 0 => 'Russian Ruble',
+ 1 => 'RUB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RUR' => array(
+ 0 => 'Russian Ruble (1991-1998)',
+ 1 => 'RUR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RWF' => array(
+ 0 => 'Rwandan Franc',
+ 1 => 'RWF',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'SHP' => array(
+ 0 => 'Saint Helena Pound',
+ 1 => 'SHP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SVC' => array(
+ 0 => 'Salvadoran Colón',
+ 1 => 'SVC',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'WST' => array(
+ 0 => 'Samoan Tala',
+ 1 => 'WST',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'STD' => array(
+ 0 => 'São Tomé and Príncipe Dobra',
+ 1 => 'STD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'SAR' => array(
+ 0 => 'Saudi Riyal',
+ 1 => 'SAR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'RSD' => array(
+ 0 => 'Serbian Dinar',
+ 1 => 'RSD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'CSD' => array(
+ 0 => 'Serbian Dinar (2002-2006)',
+ 1 => 'CSD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SCR' => array(
+ 0 => 'Seychellois Rupee',
+ 1 => 'SCR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SLL' => array(
+ 0 => 'Sierra Leonean Leone',
+ 1 => 'SLL',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'XAG' => array(
+ 0 => 'Silver',
+ 1 => 'XAG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SGD' => array(
+ 0 => 'Singapore Dollar',
+ 1 => 'SGD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SKK' => array(
+ 0 => 'Slovak Koruna',
+ 1 => 'SKK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SIT' => array(
+ 0 => 'Slovenian Tolar',
+ 1 => 'SIT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SBD' => array(
+ 0 => 'Solomon Islands Dollar',
+ 1 => 'SBD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SOS' => array(
+ 0 => 'Somali Shilling',
+ 1 => 'SOS',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ZAR' => array(
+ 0 => 'South African Rand',
+ 1 => 'ZAR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ZAL' => array(
+ 0 => 'South African Rand (financial)',
+ 1 => 'ZAL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KRH' => array(
+ 0 => 'South Korean Hwan (1953-1962)',
+ 1 => 'KRH',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'KRW' => array(
+ 0 => 'South Korean Won',
+ 1 => '₩',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'KRO' => array(
+ 0 => 'South Korean Won (1945-1953)',
+ 1 => 'KRO',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SSP' => array(
+ 0 => 'South Sudanese Pound',
+ 1 => 'SSP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SUR' => array(
+ 0 => 'Soviet Rouble',
+ 1 => 'SUR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ESP' => array(
+ 0 => 'Spanish Peseta',
+ 1 => 'ESP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ESA' => array(
+ 0 => 'Spanish Peseta (A account)',
+ 1 => 'ESA',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ESB' => array(
+ 0 => 'Spanish Peseta (convertible account)',
+ 1 => 'ESB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XDR' => array(
+ 0 => 'Special Drawing Rights',
+ 1 => 'XDR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'LKR' => array(
+ 0 => 'Sri Lankan Rupee',
+ 1 => 'LKR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XSU' => array(
+ 0 => 'Sucre',
+ 1 => 'XSU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SDD' => array(
+ 0 => 'Sudanese Dinar (1992-2007)',
+ 1 => 'SDD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SDG' => array(
+ 0 => 'Sudanese Pound',
+ 1 => 'SDG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SDP' => array(
+ 0 => 'Sudanese Pound (1957-1998)',
+ 1 => 'SDP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SRD' => array(
+ 0 => 'Surinamese Dollar',
+ 1 => 'SRD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SRG' => array(
+ 0 => 'Surinamese Guilder',
+ 1 => 'SRG',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SZL' => array(
+ 0 => 'Swazi Lilangeni',
+ 1 => 'SZL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'SEK' => array(
+ 0 => 'Swedish Krona',
+ 1 => 'SEK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CHF' => array(
+ 0 => 'Swiss Franc',
+ 1 => 'CHF',
+ 2 => 2,
+ 3 => 5,
+ ),
+ 'SYP' => array(
+ 0 => 'Syrian Pound',
+ 1 => 'SYP',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'TJR' => array(
+ 0 => 'Tajikistani Ruble',
+ 1 => 'TJR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TJS' => array(
+ 0 => 'Tajikistani Somoni',
+ 1 => 'TJS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TZS' => array(
+ 0 => 'Tanzanian Shilling',
+ 1 => 'TZS',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'XTS' => array(
+ 0 => 'Testing Currency Code',
+ 1 => 'XTS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'THB' => array(
+ 0 => 'Thai Baht',
+ 1 => '฿',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TPE' => array(
+ 0 => 'Timorese Escudo',
+ 1 => 'TPE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TOP' => array(
+ 0 => 'Tongan Paʻanga',
+ 1 => 'TOP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TTD' => array(
+ 0 => 'Trinidad and Tobago Dollar',
+ 1 => 'TTD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TND' => array(
+ 0 => 'Tunisian Dinar',
+ 1 => 'TND',
+ 2 => 3,
+ 3 => 0,
+ ),
+ 'TRY' => array(
+ 0 => 'Turkish Lira',
+ 1 => 'TRY',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TRL' => array(
+ 0 => 'Turkish Lira (1922-2005)',
+ 1 => 'TRL',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'TMT' => array(
+ 0 => 'Turkmenistani Manat',
+ 1 => 'TMT',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'TMM' => array(
+ 0 => 'Turkmenistani Manat (1993-2009)',
+ 1 => 'TMM',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'UGX' => array(
+ 0 => 'Ugandan Shilling',
+ 1 => 'UGX',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'UGS' => array(
+ 0 => 'Ugandan Shilling (1966-1987)',
+ 1 => 'UGS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UAH' => array(
+ 0 => 'Ukrainian Hryvnia',
+ 1 => 'UAH',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UAK' => array(
+ 0 => 'Ukrainian Karbovanets',
+ 1 => 'UAK',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'AED' => array(
+ 0 => 'United Arab Emirates Dirham',
+ 1 => 'AED',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'XXX' => array(
+ 0 => 'Unknown Currency',
+ 1 => 'XXX',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UYU' => array(
+ 0 => 'Uruguayan Peso',
+ 1 => 'UYU',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UYP' => array(
+ 0 => 'Uruguayan Peso (1975-1993)',
+ 1 => 'UYP',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UYI' => array(
+ 0 => 'Uruguayan Peso (Indexed Units)',
+ 1 => 'UYI',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'USD' => array(
+ 0 => 'US Dollar',
+ 1 => '$',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'USN' => array(
+ 0 => 'US Dollar (Next day)',
+ 1 => 'USN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'USS' => array(
+ 0 => 'US Dollar (Same day)',
+ 1 => 'USS',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'UZS' => array(
+ 0 => 'Uzbekistan Som',
+ 1 => 'UZS',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'VUV' => array(
+ 0 => 'Vanuatu Vatu',
+ 1 => 'VUV',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'VEF' => array(
+ 0 => 'Venezuelan Bolívar',
+ 1 => 'VEF',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'VEB' => array(
+ 0 => 'Venezuelan Bolívar (1871-2008)',
+ 1 => 'VEB',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'VND' => array(
+ 0 => 'Vietnamese Dong',
+ 1 => '₫',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'VNN' => array(
+ 0 => 'Vietnamese Dong (1978-1985)',
+ 1 => 'VNN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CHE' => array(
+ 0 => 'WIR Euro',
+ 1 => 'CHE',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'CHW' => array(
+ 0 => 'WIR Franc',
+ 1 => 'CHW',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'YDD' => array(
+ 0 => 'Yemeni Dinar',
+ 1 => 'YDD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'YER' => array(
+ 0 => 'Yemeni Rial',
+ 1 => 'YER',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'YUN' => array(
+ 0 => 'Yugoslavian Convertible Dinar (1990-1992)',
+ 1 => 'YUN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'YUD' => array(
+ 0 => 'Yugoslavian Hard Dinar (1966-1990)',
+ 1 => 'YUD',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'YUM' => array(
+ 0 => 'Yugoslavian New Dinar (1994-2002)',
+ 1 => 'YUM',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'YUR' => array(
+ 0 => 'Yugoslavian Reformed Dinar (1992-1993)',
+ 1 => 'YUR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ZRN' => array(
+ 0 => 'Zairean New Zaire (1993-1998)',
+ 1 => 'ZRN',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ZRZ' => array(
+ 0 => 'Zairean Zaire (1971-1993)',
+ 1 => 'ZRZ',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ZMK' => array(
+ 0 => 'Zambian Kwacha',
+ 1 => 'ZMK',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ZWD' => array(
+ 0 => 'Zimbabwean Dollar (1980-2008)',
+ 1 => 'ZWD',
+ 2 => 0,
+ 3 => 0,
+ ),
+ 'ZWR' => array(
+ 0 => 'Zimbabwean Dollar (2008)',
+ 1 => 'ZWR',
+ 2 => 2,
+ 3 => 0,
+ ),
+ 'ZWL' => array(
+ 0 => 'Zimbabwean Dollar (2009)',
+ 1 => 'ZWL',
+ 2 => 2,
+ 3 => 0,
+ ),
+ ),
+);
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Languages' => array(
+ 'ab' => 'Abkhazian',
+ 'ace' => 'Achinese',
+ 'ach' => 'Acoli',
+ 'ada' => 'Adangme',
+ 'ady' => 'Adyghe',
+ 'aa' => 'Afar',
+ 'afh' => 'Afrihili',
+ 'af' => 'Afrikaans',
+ 'afa' => 'Afro-Asiatic Language',
+ 'agq' => 'Aghem',
+ 'ain' => 'Ainu',
+ 'ak' => 'Akan',
+ 'akk' => 'Akkadian',
+ 'bss' => 'Akoose',
+ 'sq' => 'Albanian',
+ 'ale' => 'Aleut',
+ 'alg' => 'Algonquian Language',
+ 'tut' => 'Altaic Language',
+ 'am' => 'Amharic',
+ 'egy' => 'Ancient Egyptian',
+ 'grc' => 'Ancient Greek',
+ 'anp' => 'Angika',
+ 'apa' => 'Apache Language',
+ 'ar' => 'Arabic',
+ 'an' => 'Aragonese',
+ 'arc' => 'Aramaic',
+ 'arp' => 'Arapaho',
+ 'arw' => 'Arawak',
+ 'hy' => 'Armenian',
+ 'rup' => 'Aromanian',
+ 'art' => 'Artificial Language',
+ 'as' => 'Assamese',
+ 'ast' => 'Asturian',
+ 'asa' => 'Asu',
+ 'ath' => 'Athapascan Language',
+ 'cch' => 'Atsam',
+ 'en_AU' => 'Australian English',
+ 'aus' => 'Australian Language',
+ 'de_AT' => 'Austrian German',
+ 'map' => 'Austronesian Language',
+ 'av' => 'Avaric',
+ 'ae' => 'Avestan',
+ 'awa' => 'Awadhi',
+ 'ay' => 'Aymara',
+ 'az' => 'Azerbaijani',
+ 'ksf' => 'Bafia',
+ 'bfd' => 'Bafut',
+ 'ban' => 'Balinese',
+ 'bat' => 'Baltic Language',
+ 'bal' => 'Baluchi',
+ 'bm' => 'Bambara',
+ 'bai' => 'Bamileke Language',
+ 'bax' => 'Bamun',
+ 'bad' => 'Banda',
+ 'bnt' => 'Bantu',
+ 'bas' => 'Basaa',
+ 'ba' => 'Bashkir',
+ 'eu' => 'Basque',
+ 'btk' => 'Batak',
+ 'bej' => 'Beja',
+ 'be' => 'Belarusian',
+ 'bem' => 'Bemba',
+ 'bez' => 'Bena',
+ 'bn' => 'Bengali',
+ 'ber' => 'Berber',
+ 'bho' => 'Bhojpuri',
+ 'bh' => 'Bihari',
+ 'bik' => 'Bikol',
+ 'bin' => 'Bini',
+ 'bi' => 'Bislama',
+ 'byn' => 'Blin',
+ 'zbl' => 'Blissymbols',
+ 'brx' => 'Bodo',
+ 'bs' => 'Bosnian',
+ 'bra' => 'Braj',
+ 'pt_BR' => 'Brazilian Portuguese',
+ 'br' => 'Breton',
+ 'en_GB' => 'British English',
+ 'bug' => 'Buginese',
+ 'bg' => 'Bulgarian',
+ 'bum' => 'Bulu',
+ 'bua' => 'Buriat',
+ 'my' => 'Burmese',
+ 'cad' => 'Caddo',
+ 'en_CA' => 'Canadian English',
+ 'fr_CA' => 'Canadian French',
+ 'yue' => 'Cantonese',
+ 'car' => 'Carib',
+ 'ca' => 'Catalan',
+ 'cau' => 'Caucasian Language',
+ 'cay' => 'Cayuga',
+ 'ceb' => 'Cebuano',
+ 'cel' => 'Celtic Language',
+ 'cai' => 'Central American Indian Language',
+ 'tzm' => 'Central Atlas Tamazight',
+ 'shu' => 'Chadian Arabic',
+ 'chg' => 'Chagatai',
+ 'cmc' => 'Chamic Language',
+ 'ch' => 'Chamorro',
+ 'ce' => 'Chechen',
+ 'chr' => 'Cherokee',
+ 'chy' => 'Cheyenne',
+ 'chb' => 'Chibcha',
+ 'cgg' => 'Chiga',
+ 'zh' => 'Chinese',
+ 'chn' => 'Chinook Jargon',
+ 'chp' => 'Chipewyan',
+ 'cho' => 'Choctaw',
+ 'cu' => 'Church Slavic',
+ 'chk' => 'Chuukese',
+ 'cv' => 'Chuvash',
+ 'nwc' => 'Classical Newari',
+ 'syc' => 'Classical Syriac',
+ 'ksh' => 'Colognian',
+ 'swb' => 'Comorian',
+ 'swc' => 'Congo Swahili',
+ 'cop' => 'Coptic',
+ 'kw' => 'Cornish',
+ 'co' => 'Corsican',
+ 'cr' => 'Cree',
+ 'mus' => 'Creek',
+ 'crp' => 'Creole or Pidgin',
+ 'crh' => 'Crimean Turkish',
+ 'hr' => 'Croatian',
+ 'cus' => 'Cushitic Language',
+ 'cs' => 'Czech',
+ 'dak' => 'Dakota',
+ 'da' => 'Danish',
+ 'dar' => 'Dargwa',
+ 'day' => 'Dayak',
+ 'dzg' => 'Dazaga',
+ 'del' => 'Delaware',
+ 'din' => 'Dinka',
+ 'dv' => 'Divehi',
+ 'doi' => 'Dogri',
+ 'dgr' => 'Dogrib',
+ 'dra' => 'Dravidian Language',
+ 'dua' => 'Duala',
+ 'nl' => 'Dutch',
+ 'dyu' => 'Dyula',
+ 'dz' => 'Dzongkha',
+ 'frs' => 'Eastern Frisian',
+ 'efi' => 'Efik',
+ 'eka' => 'Ekajuk',
+ 'elx' => 'Elamite',
+ 'ebu' => 'Embu',
+ 'en' => 'English',
+ 'cpe' => 'English-based Creole or Pidgin',
+ 'myv' => 'Erzya',
+ 'eo' => 'Esperanto',
+ 'et' => 'Estonian',
+ 'pt_PT' => 'European Portuguese',
+ 'es_ES' => 'European Spanish',
+ 'ee' => 'Ewe',
+ 'ewo' => 'Ewondo',
+ 'fan' => 'Fang',
+ 'fat' => 'Fanti',
+ 'fo' => 'Faroese',
+ 'fj' => 'Fijian',
+ 'fil' => 'Filipino',
+ 'fi' => 'Finnish',
+ 'fiu' => 'Finno-Ugrian Language',
+ 'nl_BE' => 'Flemish',
+ 'fon' => 'Fon',
+ 'fr' => 'French',
+ 'cpf' => 'French-based Creole or Pidgin',
+ 'fur' => 'Friulian',
+ 'ff' => 'Fulah',
+ 'gaa' => 'Ga',
+ 'gl' => 'Galician',
+ 'lg' => 'Ganda',
+ 'gay' => 'Gayo',
+ 'gba' => 'Gbaya',
+ 'gez' => 'Geez',
+ 'ka' => 'Georgian',
+ 'de' => 'German',
+ 'gem' => 'Germanic Language',
+ 'bbj' => 'Ghomala',
+ 'gil' => 'Gilbertese',
+ 'gon' => 'Gondi',
+ 'gor' => 'Gorontalo',
+ 'got' => 'Gothic',
+ 'grb' => 'Grebo',
+ 'el' => 'Greek',
+ 'gn' => 'Guarani',
+ 'gu' => 'Gujarati',
+ 'guz' => 'Gusii',
+ 'gwi' => 'Gwichʼin',
+ 'hai' => 'Haida',
+ 'ht' => 'Haitian',
+ 'ha' => 'Hausa',
+ 'haw' => 'Hawaiian',
+ 'he' => 'Hebrew',
+ 'hz' => 'Herero',
+ 'hil' => 'Hiligaynon',
+ 'him' => 'Himachali',
+ 'hi' => 'Hindi',
+ 'ho' => 'Hiri Motu',
+ 'hit' => 'Hittite',
+ 'hmn' => 'Hmong',
+ 'hu' => 'Hungarian',
+ 'hup' => 'Hupa',
+ 'iba' => 'Iban',
+ 'ibb' => 'Ibibio',
+ 'is' => 'Icelandic',
+ 'io' => 'Ido',
+ 'ig' => 'Igbo',
+ 'ijo' => 'Ijo',
+ 'ilo' => 'Iloko',
+ 'smn' => 'Inari Sami',
+ 'inc' => 'Indic Language',
+ 'ine' => 'Indo-European Language',
+ 'id' => 'Indonesian',
+ 'inh' => 'Ingush',
+ 'ia' => 'Interlingua',
+ 'ie' => 'Interlingue',
+ 'iu' => 'Inuktitut',
+ 'ik' => 'Inupiaq',
+ 'ira' => 'Iranian Language',
+ 'ga' => 'Irish',
+ 'iro' => 'Iroquoian Language',
+ 'it' => 'Italian',
+ 'ja' => 'Japanese',
+ 'jv' => 'Javanese',
+ 'kaj' => 'Jju',
+ 'dyo' => 'Jola-Fonyi',
+ 'jrb' => 'Judeo-Arabic',
+ 'jpr' => 'Judeo-Persian',
+ 'kbd' => 'Kabardian',
+ 'kea' => 'Kabuverdianu',
+ 'kab' => 'Kabyle',
+ 'kac' => 'Kachin',
+ 'kkj' => 'Kako',
+ 'kl' => 'Kalaallisut',
+ 'kln' => 'Kalenjin',
+ 'xal' => 'Kalmyk',
+ 'kam' => 'Kamba',
+ 'kbl' => 'Kanembu',
+ 'kn' => 'Kannada',
+ 'kr' => 'Kanuri',
+ 'kaa' => 'Kara-Kalpak',
+ 'krc' => 'Karachay-Balkar',
+ 'krl' => 'Karelian',
+ 'kar' => 'Karen',
+ 'ks' => 'Kashmiri',
+ 'csb' => 'Kashubian',
+ 'kaw' => 'Kawi',
+ 'kk' => 'Kazakh',
+ 'kha' => 'Khasi',
+ 'km' => 'Khmer',
+ 'khi' => 'Khoisan Language',
+ 'kho' => 'Khotanese',
+ 'ki' => 'Kikuyu',
+ 'kmb' => 'Kimbundu',
+ 'rw' => 'Kinyarwanda',
+ 'ky' => 'Kirghiz',
+ 'tlh' => 'Klingon',
+ 'bkm' => 'Kom',
+ 'kv' => 'Komi',
+ 'kg' => 'Kongo',
+ 'kok' => 'Konkani',
+ 'ko' => 'Korean',
+ 'kfo' => 'Koro',
+ 'kos' => 'Kosraean',
+ 'khq' => 'Koyra Chiini',
+ 'ses' => 'Koyraboro Senni',
+ 'kpe' => 'Kpelle',
+ 'kro' => 'Kru',
+ 'kj' => 'Kuanyama',
+ 'kum' => 'Kumyk',
+ 'ku' => 'Kurdish',
+ 'kru' => 'Kurukh',
+ 'kut' => 'Kutenai',
+ 'nmg' => 'Kwasio',
+ 'lad' => 'Ladino',
+ 'lah' => 'Lahnda',
+ 'lam' => 'Lamba',
+ 'lag' => 'Langi',
+ 'lo' => 'Lao',
+ 'la' => 'Latin',
+ 'es_419' => 'Latin American Spanish',
+ 'lv' => 'Latvian',
+ 'lez' => 'Lezghian',
+ 'li' => 'Limburgish',
+ 'ln' => 'Lingala',
+ 'lt' => 'Lithuanian',
+ 'jbo' => 'Lojban',
+ 'nds' => 'Low German',
+ 'dsb' => 'Lower Sorbian',
+ 'loz' => 'Lozi',
+ 'lu' => 'Luba-Katanga',
+ 'lua' => 'Luba-Lulua',
+ 'lui' => 'Luiseno',
+ 'smj' => 'Lule Sami',
+ 'lun' => 'Lunda',
+ 'luo' => 'Luo',
+ 'lb' => 'Luxembourgish',
+ 'luy' => 'Luyia',
+ 'mde' => 'Maba',
+ 'mk' => 'Macedonian',
+ 'jmc' => 'Machame',
+ 'mad' => 'Madurese',
+ 'maf' => 'Mafa',
+ 'mag' => 'Magahi',
+ 'mai' => 'Maithili',
+ 'mak' => 'Makasar',
+ 'mgh' => 'Makhuwa-Meetto',
+ 'kde' => 'Makonde',
+ 'mg' => 'Malagasy',
+ 'ms' => 'Malay',
+ 'ml' => 'Malayalam',
+ 'mt' => 'Maltese',
+ 'mnc' => 'Manchu',
+ 'mdr' => 'Mandar',
+ 'man' => 'Mandingo',
+ 'mni' => 'Manipuri',
+ 'mno' => 'Manobo Language',
+ 'gv' => 'Manx',
+ 'mi' => 'Maori',
+ 'arn' => 'Mapuche',
+ 'mr' => 'Marathi',
+ 'chm' => 'Mari',
+ 'mh' => 'Marshallese',
+ 'mwr' => 'Marwari',
+ 'mas' => 'Masai',
+ 'myn' => 'Mayan Language',
+ 'byv' => 'Medumba',
+ 'men' => 'Mende',
+ 'mer' => 'Meru',
+ 'mgo' => 'Meta\'',
+ 'mic' => 'Micmac',
+ 'dum' => 'Middle Dutch',
+ 'enm' => 'Middle English',
+ 'frm' => 'Middle French',
+ 'gmh' => 'Middle High German',
+ 'mga' => 'Middle Irish',
+ 'min' => 'Minangkabau',
+ 'mwl' => 'Mirandese',
+ 'mis' => 'Miscellaneous Language',
+ 'lus' => 'Mizo',
+ 'ar_001' => 'Modern Standard Arabic',
+ 'moh' => 'Mohawk',
+ 'mdf' => 'Moksha',
+ 'mo' => 'Moldavian',
+ 'mkh' => 'Mon-Khmer Language',
+ 'lol' => 'Mongo',
+ 'mn' => 'Mongolian',
+ 'mfe' => 'Morisyen',
+ 'mos' => 'Mossi',
+ 'mun' => 'Munda Language',
+ 'mua' => 'Mundang',
+ 'mye' => 'Myene',
+ 'nqo' => 'N’Ko',
+ 'nah' => 'Nahuatl',
+ 'naq' => 'Nama',
+ 'na' => 'Nauru',
+ 'nv' => 'Navajo',
+ 'ng' => 'Ndonga',
+ 'nap' => 'Neapolitan',
+ 'ne' => 'Nepali',
+ 'new' => 'Newari',
+ 'sba' => 'Ngambay',
+ 'nnh' => 'Ngiemboon',
+ 'jgo' => 'Ngomba',
+ 'nia' => 'Nias',
+ 'nic' => 'Niger-Kordofanian Language',
+ 'ssa' => 'Nilo-Saharan Language',
+ 'niu' => 'Niuean',
+ 'zxx' => 'No linguistic content',
+ 'nog' => 'Nogai',
+ 'nai' => 'North American Indian Language',
+ 'nd' => 'North Ndebele',
+ 'frr' => 'Northern Frisian',
+ 'se' => 'Northern Sami',
+ 'nso' => 'Northern Sotho',
+ 'no' => 'Norwegian',
+ 'nb' => 'Norwegian Bokmål',
+ 'nn' => 'Norwegian Nynorsk',
+ 'nub' => 'Nubian Language',
+ 'nus' => 'Nuer',
+ 'nym' => 'Nyamwezi',
+ 'ny' => 'Nyanja',
+ 'nyn' => 'Nyankole',
+ 'tog' => 'Nyasa Tonga',
+ 'nyo' => 'Nyoro',
+ 'nzi' => 'Nzima',
+ 'oc' => 'Occitan',
+ 'oj' => 'Ojibwa',
+ 'ang' => 'Old English',
+ 'fro' => 'Old French',
+ 'goh' => 'Old High German',
+ 'sga' => 'Old Irish',
+ 'non' => 'Old Norse',
+ 'peo' => 'Old Persian',
+ 'pro' => 'Old Provençal',
+ 'or' => 'Oriya',
+ 'om' => 'Oromo',
+ 'osa' => 'Osage',
+ 'os' => 'Ossetic',
+ 'oto' => 'Otomian Language',
+ 'ota' => 'Ottoman Turkish',
+ 'pal' => 'Pahlavi',
+ 'pau' => 'Palauan',
+ 'pi' => 'Pali',
+ 'pam' => 'Pampanga',
+ 'pag' => 'Pangasinan',
+ 'pap' => 'Papiamento',
+ 'paa' => 'Papuan Language',
+ 'ps' => 'Pashto',
+ 'fa' => 'Persian',
+ 'phi' => 'Philippine Language',
+ 'phn' => 'Phoenician',
+ 'pon' => 'Pohnpeian',
+ 'pl' => 'Polish',
+ 'pt' => 'Portuguese',
+ 'cpp' => 'Portuguese-based Creole or Pidgin',
+ 'pra' => 'Prakrit Language',
+ 'pa' => 'Punjabi',
+ 'qu' => 'Quechua',
+ 'raj' => 'Rajasthani',
+ 'rap' => 'Rapanui',
+ 'rar' => 'Rarotongan',
+ 'roa' => 'Romance Language',
+ 'ro' => 'Romanian',
+ 'rm' => 'Romansh',
+ 'rom' => 'Romany',
+ 'rof' => 'Rombo',
+ 'root' => 'Root',
+ 'rn' => 'Rundi',
+ 'ru' => 'Russian',
+ 'rwk' => 'Rwa',
+ 'ssy' => 'Saho',
+ 'sah' => 'Sakha',
+ 'sal' => 'Salishan Language',
+ 'sam' => 'Samaritan Aramaic',
+ 'saq' => 'Samburu',
+ 'smi' => 'Sami Language',
+ 'sm' => 'Samoan',
+ 'sad' => 'Sandawe',
+ 'sg' => 'Sango',
+ 'sbp' => 'Sangu',
+ 'sa' => 'Sanskrit',
+ 'sat' => 'Santali',
+ 'sc' => 'Sardinian',
+ 'sas' => 'Sasak',
+ 'sco' => 'Scots',
+ 'gd' => 'Scottish Gaelic',
+ 'sel' => 'Selkup',
+ 'sem' => 'Semitic Language',
+ 'seh' => 'Sena',
+ 'see' => 'Seneca',
+ 'sr' => 'Serbian',
+ 'sh' => 'Serbo-Croatian',
+ 'srr' => 'Serer',
+ 'ksb' => 'Shambala',
+ 'shn' => 'Shan',
+ 'sn' => 'Shona',
+ 'ii' => 'Sichuan Yi',
+ 'scn' => 'Sicilian',
+ 'sid' => 'Sidamo',
+ 'sgn' => 'Sign Language',
+ 'bla' => 'Siksika',
+ 'zh_Hans' => 'Simplified Chinese',
+ 'sd' => 'Sindhi',
+ 'si' => 'Sinhala',
+ 'sit' => 'Sino-Tibetan Language',
+ 'sio' => 'Siouan Language',
+ 'sms' => 'Skolt Sami',
+ 'den' => 'Slave',
+ 'sla' => 'Slavic Language',
+ 'sk' => 'Slovak',
+ 'sl' => 'Slovenian',
+ 'xog' => 'Soga',
+ 'sog' => 'Sogdien',
+ 'so' => 'Somali',
+ 'son' => 'Songhai',
+ 'snk' => 'Soninke',
+ 'ckb' => 'Sorani Kurdish',
+ 'wen' => 'Sorbian Language',
+ 'sai' => 'South American Indian Language',
+ 'nr' => 'South Ndebele',
+ 'alt' => 'Southern Altai',
+ 'sma' => 'Southern Sami',
+ 'st' => 'Southern Sotho',
+ 'es' => 'Spanish',
+ 'srn' => 'Sranan Tongo',
+ 'suk' => 'Sukuma',
+ 'sux' => 'Sumerian',
+ 'su' => 'Sundanese',
+ 'sus' => 'Susu',
+ 'sw' => 'Swahili',
+ 'ss' => 'Swati',
+ 'sv' => 'Swedish',
+ 'fr_CH' => 'Swiss French',
+ 'gsw' => 'Swiss German',
+ 'de_CH' => 'Swiss High German',
+ 'syr' => 'Syriac',
+ 'shi' => 'Tachelhit',
+ 'tl' => 'Tagalog',
+ 'ty' => 'Tahitian',
+ 'tai' => 'Tai Language',
+ 'dav' => 'Taita',
+ 'tg' => 'Tajik',
+ 'tmh' => 'Tamashek',
+ 'ta' => 'Tamil',
+ 'trv' => 'Taroko',
+ 'twq' => 'Tasawaq',
+ 'tt' => 'Tatar',
+ 'te' => 'Telugu',
+ 'ter' => 'Tereno',
+ 'teo' => 'Teso',
+ 'tet' => 'Tetum',
+ 'th' => 'Thai',
+ 'bo' => 'Tibetan',
+ 'tig' => 'Tigre',
+ 'ti' => 'Tigrinya',
+ 'tem' => 'Timne',
+ 'tiv' => 'Tiv',
+ 'tli' => 'Tlingit',
+ 'tpi' => 'Tok Pisin',
+ 'tkl' => 'Tokelau',
+ 'to' => 'Tongan',
+ 'zh_Hant' => 'Traditional Chinese',
+ 'tsi' => 'Tsimshian',
+ 'ts' => 'Tsonga',
+ 'tn' => 'Tswana',
+ 'tum' => 'Tumbuka',
+ 'tup' => 'Tupi Language',
+ 'tr' => 'Turkish',
+ 'tk' => 'Turkmen',
+ 'tvl' => 'Tuvalu',
+ 'tyv' => 'Tuvinian',
+ 'tw' => 'Twi',
+ 'kcg' => 'Tyap',
+ 'en_US' => 'U.S. English',
+ 'udm' => 'Udmurt',
+ 'uga' => 'Ugaritic',
+ 'ug' => 'Uighur',
+ 'uk' => 'Ukrainian',
+ 'umb' => 'Umbundu',
+ 'und' => 'Unknown Language',
+ 'hsb' => 'Upper Sorbian',
+ 'ur' => 'Urdu',
+ 'uz' => 'Uzbek',
+ 'vai' => 'Vai',
+ 've' => 'Venda',
+ 'vi' => 'Vietnamese',
+ 'vo' => 'Volapük',
+ 'vot' => 'Votic',
+ 'vun' => 'Vunjo',
+ 'wak' => 'Wakashan Language',
+ 'wa' => 'Walloon',
+ 'wae' => 'Walser',
+ 'war' => 'Waray',
+ 'was' => 'Washo',
+ 'cy' => 'Welsh',
+ 'fy' => 'Western Frisian',
+ 'wal' => 'Wolaytta',
+ 'wo' => 'Wolof',
+ 'xh' => 'Xhosa',
+ 'yav' => 'Yangben',
+ 'yao' => 'Yao',
+ 'yap' => 'Yapese',
+ 'ybb' => 'Yemba',
+ 'yi' => 'Yiddish',
+ 'yo' => 'Yoruba',
+ 'ypk' => 'Yupik Language',
+ 'znd' => 'Zande',
+ 'zap' => 'Zapotec',
+ 'dje' => 'Zarma',
+ 'zza' => 'Zaza',
+ 'zen' => 'Zenaga',
+ 'za' => 'Zhuang',
+ 'zu' => 'Zulu',
+ 'zun' => 'Zuni',
+ ),
+ 'Scripts' => array(
+ 'Afak' => 'Afaka',
+ 'Hluw' => 'Anatolian Hieroglyphs',
+ 'Arab' => 'Arabic',
+ 'Armn' => 'Armenian',
+ 'Avst' => 'Avestan',
+ 'Bali' => 'Balinese',
+ 'Bamu' => 'Bamum',
+ 'Bass' => 'Bassa Vah',
+ 'Batk' => 'Batak',
+ 'Beng' => 'Bengali',
+ 'Blis' => 'Blissymbols',
+ 'Phlv' => 'Book Pahlavi',
+ 'Bopo' => 'Bopomofo',
+ 'Brah' => 'Brahmi',
+ 'Brai' => 'Braille',
+ 'Bugi' => 'Buginese',
+ 'Buhd' => 'Buhid',
+ 'Cari' => 'Carian',
+ 'Cakm' => 'Chakma',
+ 'Cham' => 'Cham',
+ 'Cher' => 'Cherokee',
+ 'Cirt' => 'Cirth',
+ 'Zyyy' => 'Common',
+ 'Copt' => 'Coptic',
+ 'Cprt' => 'Cypriot',
+ 'Cyrl' => 'Cyrillic',
+ 'Dsrt' => 'Deseret',
+ 'Deva' => 'Devanagari',
+ 'Dupl' => 'Duployan shorthand',
+ 'Syrn' => 'Eastern Syriac',
+ 'Egyd' => 'Egyptian demotic',
+ 'Egyh' => 'Egyptian hieratic',
+ 'Egyp' => 'Egyptian hieroglyphs',
+ 'Syre' => 'Estrangelo Syriac',
+ 'Ethi' => 'Ethiopic',
+ 'Latf' => 'Fraktur Latin',
+ 'Lisu' => 'Fraser',
+ 'Latg' => 'Gaelic Latin',
+ 'Geor' => 'Georgian',
+ 'Geok' => 'Georgian Khutsuri',
+ 'Glag' => 'Glagolitic',
+ 'Goth' => 'Gothic',
+ 'Gran' => 'Grantha',
+ 'Grek' => 'Greek',
+ 'Gujr' => 'Gujarati',
+ 'Guru' => 'Gurmukhi',
+ 'Hani' => 'Han',
+ 'Hang' => 'Hangul',
+ 'Hano' => 'Hanunoo',
+ 'Hebr' => 'Hebrew',
+ 'Hira' => 'Hiragana',
+ 'Armi' => 'Imperial Aramaic',
+ 'Inds' => 'Indus',
+ 'Zinh' => 'Inherited',
+ 'Phli' => 'Inscriptional Pahlavi',
+ 'Prti' => 'Inscriptional Parthian',
+ 'Jpan' => 'Japanese',
+ 'Hrkt' => 'Japanese syllabaries',
+ 'Java' => 'Javanese',
+ 'Jurc' => 'Jurchen',
+ 'Kthi' => 'Kaithi',
+ 'Knda' => 'Kannada',
+ 'Kana' => 'Katakana',
+ 'Kali' => 'Kayah Li',
+ 'Khar' => 'Kharoshthi',
+ 'Khmr' => 'Khmer',
+ 'Khoj' => 'Khojki',
+ 'Sind' => 'Khudawadi',
+ 'Kore' => 'Korean',
+ 'Kpel' => 'Kpelle',
+ 'Lana' => 'Lanna',
+ 'Laoo' => 'Lao',
+ 'Latn' => 'Latin',
+ 'Lepc' => 'Lepcha',
+ 'Limb' => 'Limbu',
+ 'Lina' => 'Linear A',
+ 'Linb' => 'Linear B',
+ 'Loma' => 'Loma',
+ 'Lyci' => 'Lycian',
+ 'Lydi' => 'Lydian',
+ 'Mlym' => 'Malayalam',
+ 'Mand' => 'Mandaean',
+ 'Mani' => 'Manichaean',
+ 'Zmth' => 'Mathematical Notation',
+ 'Maya' => 'Mayan hieroglyphs',
+ 'Mtei' => 'Meitei Mayek',
+ 'Mend' => 'Mende',
+ 'Mero' => 'Meroitic',
+ 'Merc' => 'Meroitic Cursive',
+ 'Mong' => 'Mongolian',
+ 'Moon' => 'Moon',
+ 'Mroo' => 'Mro',
+ 'Mymr' => 'Myanmar',
+ 'Nkoo' => 'N’Ko',
+ 'Nbat' => 'Nabataean',
+ 'Nkgb' => 'Naxi Geba',
+ 'Talu' => 'New Tai Lue',
+ 'Nshu' => 'Nüshu',
+ 'Ogam' => 'Ogham',
+ 'Olck' => 'Ol Chiki',
+ 'Cyrs' => 'Old Church Slavonic Cyrillic',
+ 'Hung' => 'Old Hungarian',
+ 'Ital' => 'Old Italic',
+ 'Narb' => 'Old North Arabian',
+ 'Perm' => 'Old Permic',
+ 'Xpeo' => 'Old Persian',
+ 'Sarb' => 'Old South Arabian',
+ 'Orya' => 'Oriya',
+ 'Orkh' => 'Orkhon',
+ 'Osma' => 'Osmanya',
+ 'Hmng' => 'Pahawh Hmong',
+ 'Palm' => 'Palmyrene',
+ 'Phag' => 'Phags-pa',
+ 'Phnx' => 'Phoenician',
+ 'Plrd' => 'Pollard Phonetic',
+ 'Phlp' => 'Psalter Pahlavi',
+ 'Rjng' => 'Rejang',
+ 'Roro' => 'Rongorongo',
+ 'Runr' => 'Runic',
+ 'Samr' => 'Samaritan',
+ 'Sara' => 'Sarati',
+ 'Saur' => 'Saurashtra',
+ 'Shrd' => 'Sharada',
+ 'Shaw' => 'Shavian',
+ 'Sgnw' => 'SignWriting',
+ 'Hans' => 'Simplified',
+ 'Sinh' => 'Sinhala',
+ 'Sora' => 'Sora Sompeng',
+ 'Xsux' => 'Sumero-Akkadian Cuneiform',
+ 'Sund' => 'Sundanese',
+ 'Sylo' => 'Syloti Nagri',
+ 'Zsym' => 'Symbols',
+ 'Syrc' => 'Syriac',
+ 'Tglg' => 'Tagalog',
+ 'Tagb' => 'Tagbanwa',
+ 'Tale' => 'Tai Le',
+ 'Tavt' => 'Tai Viet',
+ 'Takr' => 'Takri',
+ 'Taml' => 'Tamil',
+ 'Tang' => 'Tangut',
+ 'Telu' => 'Telugu',
+ 'Teng' => 'Tengwar',
+ 'Thaa' => 'Thaana',
+ 'Thai' => 'Thai',
+ 'Tibt' => 'Tibetan',
+ 'Tfng' => 'Tifinagh',
+ 'Tirh' => 'Tirhuta',
+ 'Hant' => 'Traditional',
+ 'Ugar' => 'Ugaritic',
+ 'Cans' => 'Unified Canadian Aboriginal Syllabics',
+ 'Zzzz' => 'Unknown Script',
+ 'Zxxx' => 'Unwritten',
+ 'Vaii' => 'Vai',
+ 'Wara' => 'Varang Kshiti',
+ 'Visp' => 'Visible Speech',
+ 'Syrj' => 'Western Syriac',
+ 'Wole' => 'Woleai',
+ 'Yiii' => 'Yi',
+ ),
+);
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Locales' => array(
+ 'af' => 'Afrikaans',
+ 'af_NA' => 'Afrikaans (Namibia)',
+ 'agq' => 'Aghem',
+ 'ak' => 'Akan',
+ 'sq' => 'Albanian',
+ 'am' => 'Amharic',
+ 'ar' => 'Arabic',
+ 'ar_DZ' => 'Arabic (Algeria)',
+ 'ar_IQ' => 'Arabic (Iraq)',
+ 'ar_JO' => 'Arabic (Jordan)',
+ 'ar_LB' => 'Arabic (Lebanon)',
+ 'ar_LY' => 'Arabic (Libya)',
+ 'ar_MR' => 'Arabic (Mauritania)',
+ 'ar_MA' => 'Arabic (Morocco)',
+ 'ar_PS' => 'Arabic (Palestinian Territories)',
+ 'ar_QA' => 'Arabic (Qatar)',
+ 'ar_SA' => 'Arabic (Saudi Arabia)',
+ 'ar_SY' => 'Arabic (Syria)',
+ 'ar_TN' => 'Arabic (Tunisia)',
+ 'ar_EH' => 'Arabic (Western Sahara)',
+ 'ar_YE' => 'Arabic (Yemen)',
+ 'hy' => 'Armenian',
+ 'as' => 'Assamese',
+ 'asa' => 'Asu',
+ 'az' => 'Azerbaijani',
+ 'az_Cyrl' => 'Azerbaijani (Cyrillic)',
+ 'az_Latn' => 'Azerbaijani (Latin)',
+ 'ksf' => 'Bafia',
+ 'bm' => 'Bambara',
+ 'bas' => 'Basaa',
+ 'eu' => 'Basque',
+ 'be' => 'Belarusian',
+ 'bem' => 'Bemba',
+ 'bez' => 'Bena',
+ 'bn' => 'Bengali',
+ 'bn_IN' => 'Bengali (India)',
+ 'brx' => 'Bodo',
+ 'bs' => 'Bosnian',
+ 'bs_Cyrl' => 'Bosnian (Cyrillic)',
+ 'br' => 'Breton',
+ 'bg' => 'Bulgarian',
+ 'my' => 'Burmese',
+ 'my_MM' => 'Burmese (Myanmar [Burma])',
+ 'ca' => 'Catalan',
+ 'tzm' => 'Central Atlas Tamazight',
+ 'tzm_Latn' => 'Central Atlas Tamazight (Latin)',
+ 'chr' => 'Cherokee',
+ 'chr_US' => 'Cherokee (United States)',
+ 'cgg' => 'Chiga',
+ 'zh' => 'Chinese',
+ 'zh_Hans_HK' => 'Chinese (Simplified, Hong Kong SAR China)',
+ 'zh_Hans_MO' => 'Chinese (Simplified, Macau SAR China)',
+ 'zh_Hans_SG' => 'Chinese (Simplified, Singapore)',
+ 'zh_Hans' => 'Chinese (Simplified)',
+ 'zh_Hant_HK' => 'Chinese (Traditional, Hong Kong SAR China)',
+ 'zh_Hant_MO' => 'Chinese (Traditional, Macau SAR China)',
+ 'zh_Hant' => 'Chinese (Traditional)',
+ 'swc' => 'Congo Swahili',
+ 'kw' => 'Cornish',
+ 'hr' => 'Croatian',
+ 'cs' => 'Czech',
+ 'da' => 'Danish',
+ 'dua' => 'Duala',
+ 'nl' => 'Dutch',
+ 'nl_BE' => 'Dutch (Belgium)',
+ 'dz' => 'Dzongkha',
+ 'ebu' => 'Embu',
+ 'en' => 'English',
+ 'en_AU' => 'English (Australia)',
+ 'en_BE' => 'English (Belgium)',
+ 'en_BZ' => 'English (Belize)',
+ 'en_BW' => 'English (Botswana)',
+ 'en_CA' => 'English (Canada)',
+ 'en_GI' => 'English (Gibraltar)',
+ 'en_GG' => 'English (Guernsey)',
+ 'en_HK' => 'English (Hong Kong SAR China)',
+ 'en_IN' => 'English (India)',
+ 'en_IE' => 'English (Ireland)',
+ 'en_IM' => 'English (Isle of Man)',
+ 'en_JM' => 'English (Jamaica)',
+ 'en_JE' => 'English (Jersey)',
+ 'en_LR' => 'English (Liberia)',
+ 'en_MT' => 'English (Malta)',
+ 'en_NA' => 'English (Namibia)',
+ 'en_NZ' => 'English (New Zealand)',
+ 'en_PK' => 'English (Pakistan)',
+ 'en_PH' => 'English (Philippines)',
+ 'en_PR' => 'English (Puerto Rico)',
+ 'en_SG' => 'English (Singapore)',
+ 'en_ZA' => 'English (South Africa)',
+ 'en_TT' => 'English (Trinidad and Tobago)',
+ 'en_GB' => 'English (United Kingdom)',
+ 'en_US' => 'English (United States)',
+ 'en_ZW' => 'English (Zimbabwe)',
+ 'eo' => 'Esperanto',
+ 'et' => 'Estonian',
+ 'ee' => 'Ewe',
+ 'ewo' => 'Ewondo',
+ 'fo' => 'Faroese',
+ 'fil' => 'Filipino',
+ 'fil_PH' => 'Filipino (Philippines)',
+ 'fi' => 'Finnish',
+ 'fr' => 'French',
+ 'fr_BE' => 'French (Belgium)',
+ 'fr_CA' => 'French (Canada)',
+ 'fr_LU' => 'French (Luxembourg)',
+ 'fr_CH' => 'French (Switzerland)',
+ 'ff' => 'Fulah',
+ 'gl' => 'Galician',
+ 'lg' => 'Ganda',
+ 'ka' => 'Georgian',
+ 'de' => 'German',
+ 'de_AT' => 'German (Austria)',
+ 'de_LI' => 'German (Liechtenstein)',
+ 'de_CH' => 'German (Switzerland)',
+ 'el' => 'Greek',
+ 'el_CY' => 'Greek (Cyprus)',
+ 'gu' => 'Gujarati',
+ 'guz' => 'Gusii',
+ 'ha' => 'Hausa',
+ 'ha_Latn' => 'Hausa (Latin)',
+ 'haw' => 'Hawaiian',
+ 'haw_US' => 'Hawaiian (United States)',
+ 'he' => 'Hebrew',
+ 'hi' => 'Hindi',
+ 'hu' => 'Hungarian',
+ 'is' => 'Icelandic',
+ 'ig' => 'Igbo',
+ 'id' => 'Indonesian',
+ 'ga' => 'Irish',
+ 'it' => 'Italian',
+ 'it_CH' => 'Italian (Switzerland)',
+ 'ja' => 'Japanese',
+ 'dyo' => 'Jola-Fonyi',
+ 'kea' => 'Kabuverdianu',
+ 'kab' => 'Kabyle',
+ 'kl' => 'Kalaallisut',
+ 'kln' => 'Kalenjin',
+ 'kam' => 'Kamba',
+ 'kn' => 'Kannada',
+ 'ks' => 'Kashmiri',
+ 'ks_Arab' => 'Kashmiri (Arabic)',
+ 'kk' => 'Kazakh',
+ 'kk_Cyrl' => 'Kazakh (Cyrillic)',
+ 'km' => 'Khmer',
+ 'ki' => 'Kikuyu',
+ 'rw' => 'Kinyarwanda',
+ 'kok' => 'Konkani',
+ 'ko' => 'Korean',
+ 'khq' => 'Koyra Chiini',
+ 'ses' => 'Koyraboro Senni',
+ 'nmg' => 'Kwasio',
+ 'lag' => 'Langi',
+ 'lo' => 'Lao',
+ 'lv' => 'Latvian',
+ 'ln' => 'Lingala',
+ 'lt' => 'Lithuanian',
+ 'lu' => 'Luba-Katanga',
+ 'luo' => 'Luo',
+ 'luy' => 'Luyia',
+ 'mk' => 'Macedonian',
+ 'jmc' => 'Machame',
+ 'mgh' => 'Makhuwa-Meetto',
+ 'kde' => 'Makonde',
+ 'mg' => 'Malagasy',
+ 'ms' => 'Malay',
+ 'ms_BN' => 'Malay (Brunei)',
+ 'ml' => 'Malayalam',
+ 'mt' => 'Maltese',
+ 'gv' => 'Manx',
+ 'mr' => 'Marathi',
+ 'mas' => 'Masai',
+ 'mer' => 'Meru',
+ 'mgo' => 'Meta\'',
+ 'mfe' => 'Morisyen',
+ 'mua' => 'Mundang',
+ 'naq' => 'Nama',
+ 'ne' => 'Nepali',
+ 'ne_IN' => 'Nepali (India)',
+ 'jgo' => 'Ngomba',
+ 'nd' => 'North Ndebele',
+ 'nb' => 'Norwegian Bokmål',
+ 'nn' => 'Norwegian Nynorsk',
+ 'nus' => 'Nuer',
+ 'nyn' => 'Nyankole',
+ 'or' => 'Oriya',
+ 'om' => 'Oromo',
+ 'ps' => 'Pashto',
+ 'fa' => 'Persian',
+ 'fa_AF' => 'Persian (Afghanistan)',
+ 'pl' => 'Polish',
+ 'pt' => 'Portuguese',
+ 'pt_AO' => 'Portuguese (Angola)',
+ 'pt_CV' => 'Portuguese (Cape Verde)',
+ 'pt_GW' => 'Portuguese (Guinea-Bissau)',
+ 'pt_MO' => 'Portuguese (Macau SAR China)',
+ 'pt_MZ' => 'Portuguese (Mozambique)',
+ 'pt_PT' => 'Portuguese (Portugal)',
+ 'pt_ST' => 'Portuguese (São Tomé and Príncipe)',
+ 'pt_TL' => 'Portuguese (Timor-Leste)',
+ 'pa' => 'Punjabi',
+ 'pa_Arab' => 'Punjabi (Arabic)',
+ 'pa_Guru' => 'Punjabi (Gurmukhi)',
+ 'ro' => 'Romanian',
+ 'rm' => 'Romansh',
+ 'rof' => 'Rombo',
+ 'rn' => 'Rundi',
+ 'ru' => 'Russian',
+ 'ru_UA' => 'Russian (Ukraine)',
+ 'rwk' => 'Rwa',
+ 'saq' => 'Samburu',
+ 'sg' => 'Sango',
+ 'sbp' => 'Sangu',
+ 'seh' => 'Sena',
+ 'sr' => 'Serbian',
+ 'sr_Cyrl_BA' => 'Serbian (Cyrillic, Bosnia and Herzegovina)',
+ 'sr_Cyrl' => 'Serbian (Cyrillic)',
+ 'sr_Latn_ME' => 'Serbian (Latin, Montenegro)',
+ 'sr_Latn' => 'Serbian (Latin)',
+ 'ksb' => 'Shambala',
+ 'sn' => 'Shona',
+ 'ii' => 'Sichuan Yi',
+ 'si' => 'Sinhala',
+ 'sk' => 'Slovak',
+ 'sl' => 'Slovenian',
+ 'xog' => 'Soga',
+ 'so' => 'Somali',
+ 'es' => 'Spanish',
+ 'es_AR' => 'Spanish (Argentina)',
+ 'es_BO' => 'Spanish (Bolivia)',
+ 'es_CL' => 'Spanish (Chile)',
+ 'es_CO' => 'Spanish (Colombia)',
+ 'es_CR' => 'Spanish (Costa Rica)',
+ 'es_CU' => 'Spanish (Cuba)',
+ 'es_DO' => 'Spanish (Dominican Republic)',
+ 'es_EC' => 'Spanish (Ecuador)',
+ 'es_SV' => 'Spanish (El Salvador)',
+ 'es_GQ' => 'Spanish (Equatorial Guinea)',
+ 'es_GT' => 'Spanish (Guatemala)',
+ 'es_HN' => 'Spanish (Honduras)',
+ 'es_MX' => 'Spanish (Mexico)',
+ 'es_NI' => 'Spanish (Nicaragua)',
+ 'es_PA' => 'Spanish (Panama)',
+ 'es_PY' => 'Spanish (Paraguay)',
+ 'es_PE' => 'Spanish (Peru)',
+ 'es_PH' => 'Spanish (Philippines)',
+ 'es_PR' => 'Spanish (Puerto Rico)',
+ 'es_US' => 'Spanish (United States)',
+ 'es_UY' => 'Spanish (Uruguay)',
+ 'es_VE' => 'Spanish (Venezuela)',
+ 'sw' => 'Swahili',
+ 'sw_KE' => 'Swahili (Kenya)',
+ 'sv' => 'Swedish',
+ 'sv_FI' => 'Swedish (Finland)',
+ 'gsw' => 'Swiss German',
+ 'shi' => 'Tachelhit',
+ 'shi_Latn' => 'Tachelhit (Latin)',
+ 'shi_Tfng' => 'Tachelhit (Tifinagh)',
+ 'dav' => 'Taita',
+ 'ta' => 'Tamil',
+ 'ta_MY' => 'Tamil (Malaysia)',
+ 'ta_SG' => 'Tamil (Singapore)',
+ 'twq' => 'Tasawaq',
+ 'te' => 'Telugu',
+ 'teo' => 'Teso',
+ 'th' => 'Thai',
+ 'bo' => 'Tibetan',
+ 'ti' => 'Tigrinya',
+ 'ti_ER' => 'Tigrinya (Eritrea)',
+ 'to' => 'Tongan',
+ 'tr' => 'Turkish',
+ 'uk' => 'Ukrainian',
+ 'ur' => 'Urdu',
+ 'ur_IN' => 'Urdu (India)',
+ 'uz' => 'Uzbek',
+ 'uz_Arab' => 'Uzbek (Arabic)',
+ 'uz_Cyrl' => 'Uzbek (Cyrillic)',
+ 'uz_Latn' => 'Uzbek (Latin)',
+ 'vai' => 'Vai',
+ 'vai_Latn_LR' => 'Vai (Latin, Liberia)',
+ 'vai_Latn' => 'Vai (Latin)',
+ 'vai_Vaii_LR' => 'Vai (Vai, Liberia)',
+ 'vai_Vaii' => 'Vai (Vai)',
+ 'vi' => 'Vietnamese',
+ 'vun' => 'Vunjo',
+ 'cy' => 'Welsh',
+ 'yav' => 'Yangben',
+ 'yo' => 'Yoruba',
+ 'dje' => 'Zarma',
+ 'zu' => 'Zulu',
+ ),
+);
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Countries' => array(
+ 'AF' => 'Afghanistan',
+ 'AX' => 'Åland Islands',
+ 'AL' => 'Albania',
+ 'DZ' => 'Algeria',
+ 'AS' => 'American Samoa',
+ 'AD' => 'Andorra',
+ 'AO' => 'Angola',
+ 'AI' => 'Anguilla',
+ 'AQ' => 'Antarctica',
+ 'AG' => 'Antigua and Barbuda',
+ 'AR' => 'Argentina',
+ 'AM' => 'Armenia',
+ 'AW' => 'Aruba',
+ 'AC' => 'Ascension Island',
+ 'AU' => 'Australia',
+ 'AT' => 'Austria',
+ 'AZ' => 'Azerbaijan',
+ 'BS' => 'Bahamas',
+ 'BH' => 'Bahrain',
+ 'BD' => 'Bangladesh',
+ 'BB' => 'Barbados',
+ 'BY' => 'Belarus',
+ 'BE' => 'Belgium',
+ 'BZ' => 'Belize',
+ 'BJ' => 'Benin',
+ 'BM' => 'Bermuda',
+ 'BT' => 'Bhutan',
+ 'BO' => 'Bolivia',
+ 'BA' => 'Bosnia and Herzegovina',
+ 'BW' => 'Botswana',
+ 'BV' => 'Bouvet Island',
+ 'BR' => 'Brazil',
+ 'IO' => 'British Indian Ocean Territory',
+ 'VG' => 'British Virgin Islands',
+ 'BN' => 'Brunei',
+ 'BG' => 'Bulgaria',
+ 'BF' => 'Burkina Faso',
+ 'BI' => 'Burundi',
+ 'KH' => 'Cambodia',
+ 'CM' => 'Cameroon',
+ 'CA' => 'Canada',
+ 'IC' => 'Canary Islands',
+ 'CV' => 'Cape Verde',
+ 'BQ' => 'Caribbean Netherlands',
+ 'KY' => 'Cayman Islands',
+ 'CF' => 'Central African Republic',
+ 'EA' => 'Ceuta and Melilla',
+ 'TD' => 'Chad',
+ 'CL' => 'Chile',
+ 'CN' => 'China',
+ 'CX' => 'Christmas Island',
+ 'CP' => 'Clipperton Island',
+ 'CC' => 'Cocos [Keeling] Islands',
+ 'CO' => 'Colombia',
+ 'KM' => 'Comoros',
+ 'CG' => 'Congo - Brazzaville',
+ 'CD' => 'Congo - Kinshasa',
+ 'CK' => 'Cook Islands',
+ 'CR' => 'Costa Rica',
+ 'CI' => 'Côte d’Ivoire',
+ 'HR' => 'Croatia',
+ 'CU' => 'Cuba',
+ 'CW' => 'Curaçao',
+ 'CY' => 'Cyprus',
+ 'CZ' => 'Czech Republic',
+ 'DK' => 'Denmark',
+ 'DG' => 'Diego Garcia',
+ 'DJ' => 'Djibouti',
+ 'DM' => 'Dominica',
+ 'DO' => 'Dominican Republic',
+ 'EC' => 'Ecuador',
+ 'EG' => 'Egypt',
+ 'SV' => 'El Salvador',
+ 'GQ' => 'Equatorial Guinea',
+ 'ER' => 'Eritrea',
+ 'EE' => 'Estonia',
+ 'ET' => 'Ethiopia',
+ 'EU' => 'European Union',
+ 'FK' => 'Falkland Islands',
+ 'FO' => 'Faroe Islands',
+ 'FJ' => 'Fiji',
+ 'FI' => 'Finland',
+ 'FR' => 'France',
+ 'GF' => 'French Guiana',
+ 'PF' => 'French Polynesia',
+ 'TF' => 'French Southern Territories',
+ 'GA' => 'Gabon',
+ 'GM' => 'Gambia',
+ 'GE' => 'Georgia',
+ 'DE' => 'Germany',
+ 'GH' => 'Ghana',
+ 'GI' => 'Gibraltar',
+ 'GR' => 'Greece',
+ 'GL' => 'Greenland',
+ 'GD' => 'Grenada',
+ 'GP' => 'Guadeloupe',
+ 'GU' => 'Guam',
+ 'GT' => 'Guatemala',
+ 'GG' => 'Guernsey',
+ 'GN' => 'Guinea',
+ 'GW' => 'Guinea-Bissau',
+ 'GY' => 'Guyana',
+ 'HT' => 'Haiti',
+ 'HM' => 'Heard Island and McDonald Islands',
+ 'HN' => 'Honduras',
+ 'HK' => 'Hong Kong SAR China',
+ 'HU' => 'Hungary',
+ 'IS' => 'Iceland',
+ 'IN' => 'India',
+ 'ID' => 'Indonesia',
+ 'IR' => 'Iran',
+ 'IQ' => 'Iraq',
+ 'IE' => 'Ireland',
+ 'IM' => 'Isle of Man',
+ 'IL' => 'Israel',
+ 'IT' => 'Italy',
+ 'JM' => 'Jamaica',
+ 'JP' => 'Japan',
+ 'JE' => 'Jersey',
+ 'JO' => 'Jordan',
+ 'KZ' => 'Kazakhstan',
+ 'KE' => 'Kenya',
+ 'KI' => 'Kiribati',
+ 'KW' => 'Kuwait',
+ 'KG' => 'Kyrgyzstan',
+ 'LA' => 'Laos',
+ 'LV' => 'Latvia',
+ 'LB' => 'Lebanon',
+ 'LS' => 'Lesotho',
+ 'LR' => 'Liberia',
+ 'LY' => 'Libya',
+ 'LI' => 'Liechtenstein',
+ 'LT' => 'Lithuania',
+ 'LU' => 'Luxembourg',
+ 'MO' => 'Macau SAR China',
+ 'MK' => 'Macedonia',
+ 'MG' => 'Madagascar',
+ 'MW' => 'Malawi',
+ 'MY' => 'Malaysia',
+ 'MV' => 'Maldives',
+ 'ML' => 'Mali',
+ 'MT' => 'Malta',
+ 'MH' => 'Marshall Islands',
+ 'MQ' => 'Martinique',
+ 'MR' => 'Mauritania',
+ 'MU' => 'Mauritius',
+ 'YT' => 'Mayotte',
+ 'MX' => 'Mexico',
+ 'FM' => 'Micronesia',
+ 'MD' => 'Moldova',
+ 'MC' => 'Monaco',
+ 'MN' => 'Mongolia',
+ 'ME' => 'Montenegro',
+ 'MS' => 'Montserrat',
+ 'MA' => 'Morocco',
+ 'MZ' => 'Mozambique',
+ 'MM' => 'Myanmar [Burma]',
+ 'NA' => 'Namibia',
+ 'NR' => 'Nauru',
+ 'NP' => 'Nepal',
+ 'NL' => 'Netherlands',
+ 'AN' => 'Netherlands Antilles',
+ 'NC' => 'New Caledonia',
+ 'NZ' => 'New Zealand',
+ 'NI' => 'Nicaragua',
+ 'NE' => 'Niger',
+ 'NG' => 'Nigeria',
+ 'NU' => 'Niue',
+ 'NF' => 'Norfolk Island',
+ 'KP' => 'North Korea',
+ 'MP' => 'Northern Mariana Islands',
+ 'NO' => 'Norway',
+ 'OM' => 'Oman',
+ 'QO' => 'Outlying Oceania',
+ 'PK' => 'Pakistan',
+ 'PW' => 'Palau',
+ 'PS' => 'Palestinian Territories',
+ 'PA' => 'Panama',
+ 'PG' => 'Papua New Guinea',
+ 'PY' => 'Paraguay',
+ 'PE' => 'Peru',
+ 'PH' => 'Philippines',
+ 'PN' => 'Pitcairn Islands',
+ 'PL' => 'Poland',
+ 'PT' => 'Portugal',
+ 'PR' => 'Puerto Rico',
+ 'QA' => 'Qatar',
+ 'RE' => 'Réunion',
+ 'RO' => 'Romania',
+ 'RU' => 'Russia',
+ 'RW' => 'Rwanda',
+ 'BL' => 'Saint Barthélemy',
+ 'SH' => 'Saint Helena',
+ 'KN' => 'Saint Kitts and Nevis',
+ 'LC' => 'Saint Lucia',
+ 'MF' => 'Saint Martin',
+ 'PM' => 'Saint Pierre and Miquelon',
+ 'VC' => 'Saint Vincent and the Grenadines',
+ 'WS' => 'Samoa',
+ 'SM' => 'San Marino',
+ 'ST' => 'São Tomé and Príncipe',
+ 'SA' => 'Saudi Arabia',
+ 'SN' => 'Senegal',
+ 'RS' => 'Serbia',
+ 'SC' => 'Seychelles',
+ 'SL' => 'Sierra Leone',
+ 'SG' => 'Singapore',
+ 'SX' => 'Sint Maarten',
+ 'SK' => 'Slovakia',
+ 'SI' => 'Slovenia',
+ 'SB' => 'Solomon Islands',
+ 'SO' => 'Somalia',
+ 'ZA' => 'South Africa',
+ 'GS' => 'South Georgia and the South Sandwich Islands',
+ 'KR' => 'South Korea',
+ 'SS' => 'South Sudan',
+ 'ES' => 'Spain',
+ 'LK' => 'Sri Lanka',
+ 'SD' => 'Sudan',
+ 'SR' => 'Suriname',
+ 'SJ' => 'Svalbard and Jan Mayen',
+ 'SZ' => 'Swaziland',
+ 'SE' => 'Sweden',
+ 'CH' => 'Switzerland',
+ 'SY' => 'Syria',
+ 'TW' => 'Taiwan',
+ 'TJ' => 'Tajikistan',
+ 'TZ' => 'Tanzania',
+ 'TH' => 'Thailand',
+ 'TL' => 'Timor-Leste',
+ 'TG' => 'Togo',
+ 'TK' => 'Tokelau',
+ 'TO' => 'Tonga',
+ 'TT' => 'Trinidad and Tobago',
+ 'TA' => 'Tristan da Cunha',
+ 'TN' => 'Tunisia',
+ 'TR' => 'Turkey',
+ 'TM' => 'Turkmenistan',
+ 'TC' => 'Turks and Caicos Islands',
+ 'TV' => 'Tuvalu',
+ 'UM' => 'U.S. Outlying Islands',
+ 'VI' => 'U.S. Virgin Islands',
+ 'UG' => 'Uganda',
+ 'UA' => 'Ukraine',
+ 'AE' => 'United Arab Emirates',
+ 'GB' => 'United Kingdom',
+ 'US' => 'United States',
+ 'UY' => 'Uruguay',
+ 'UZ' => 'Uzbekistan',
+ 'VU' => 'Vanuatu',
+ 'VA' => 'Vatican City',
+ 'VE' => 'Venezuela',
+ 'VN' => 'Vietnam',
+ 'WF' => 'Wallis and Futuna',
+ 'EH' => 'Western Sahara',
+ 'YE' => 'Yemen',
+ 'ZM' => 'Zambia',
+ 'ZW' => 'Zimbabwe',
+ ),
+);
--- /dev/null
+<?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\Icu\Tests;
+
+use Symfony\Component\Icu\IcuCurrencyBundle;
+use Symfony\Component\Icu\IcuLanguageBundle;
+use Symfony\Component\Icu\IcuLocaleBundle;
+use Symfony\Component\Icu\IcuRegionBundle;
+use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader;
+
+/**
+ * Verifies that the data files can actually be read.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuIntegrationTest extends \PHPUnit_Framework_TestCase
+{
+ public function testCurrencyBundle()
+ {
+ $bundle = new IcuCurrencyBundle(new StructuredBundleReader(new PhpBundleReader()));
+
+ $this->assertSame('€', $bundle->getCurrencySymbol('EUR', 'en'));
+ }
+
+ public function testLanguageBundle()
+ {
+ $bundle = new IcuLanguageBundle(new StructuredBundleReader(new PhpBundleReader()));
+
+ $this->assertSame('German', $bundle->getLanguageName('de', null, 'en'));
+ }
+
+ public function testLocaleBundle()
+ {
+ $bundle = new IcuLocaleBundle(new StructuredBundleReader(new PhpBundleReader()));
+
+ $this->assertSame('Azerbaijani', $bundle->getLocaleName('az', 'en'));
+ }
+
+ public function testRegionBundle()
+ {
+ $bundle = new IcuRegionBundle(new StructuredBundleReader(new PhpBundleReader()));
+
+ $this->assertSame('United Kingdom', $bundle->getCountryName('GB', 'en'));
+ }
+}
--- /dev/null
+{
+ "name": "symfony/icu",
+ "type": "library",
+ "description": "Contains an excerpt of the ICU data and classes to load it.",
+ "keywords": ["icu", "intl"],
+ "homepage": "http://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/intl": "~2.3"
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Icu\\": "" }
+ },
+ "target-dir": "Symfony/Component/Icu"
+}
--- /dev/null
+<?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 Icu Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Tests</directory>
+ <directory>./vendor</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
--- /dev/null
+Contributing to the Intl component
+==================================
+
+A very good way of contributing to the Intl component is by updating the
+included data for the ICU version you have installed on your system.
+
+Preparation
+-----------
+
+To prepare, you need to install the development dependencies of the component.
+
+ $ cd /path/to/Symfony/Component/Intl
+ $ composer.phar install --dev
+
+Determining your ICU version
+---------------------------
+
+The ICU version installed in your PHP environment can be found by running
+icu-version.php:
+
+ $ php Resources/bin/icu-version.php
+
+Updating the ICU data
+---------------------
+
+To update the data files, run the update-icu-component.php script:
+
+ $ php Resources/bin/update-icu-component.php
+
+The script needs the binaries "svn" and "make" to be available on your system.
+It will download the latest version of the ICU sources for the ICU version
+installed in your PHP environment. The script will then compile the "genrb"
+binary and use it to compile the ICU data files to binaries. The binaries are
+copied to the Resources/ directory of the Icu component found in the
+vendor/symfony/icu/ directory.
+
+Updating the stub data
+----------------------
+
+In the previous step you updated the Icu component for the ICU version
+installed on your system. If you are using the latest ICU version, you should
+also create the stub data files which will be used by people who don't have
+the intl extension installed.
+
+To update the stub files, run the update-stubs.php script:
+
+ $ php Resources/bin/update-stubs.php
+
+The script will fail if you don't have the latest ICU version. If you want to
+upgrade the ICU version, adjust the return value of the
+`Intl::getIcuStubVersion()` before you run the script.
+
+The script creates copies of the binary resource bundles in the Icu component
+and stores them in the Resources/ directory of the Intl component. The copies
+are made for the locale "en" only and are stored in .php files, so that they
+can be read even if the intl extension is not available.
+
+Creating a pull request
+-----------------------
+
+You need to create up to two pull requests:
+
+* If you updated the Icu component, you need to push that change and create a
+ pull request in the `symfony/Icu` repository. Make sure to submit the pull
+ request to the correct master branch. If you updated the ICU data for version
+ 4.8, your pull request goes to branch `48-master`, for version 49 to
+ `49-master` and so on.
+
+* If you updated the stub files of the Intl component, you need to push that
+ change and create a pull request in the `symfony/symfony` repository. The
+ pull request should be based on the `master` branch.
+
+Combining .res files to a .dat-package
+--------------------------------------
+
+The individual *.res files can be combined into a single .dat-file.
+Unfortunately, PHP's `ResourceBundle` class is currently not able to handle
+.dat-files.
+
+Once it is, the following steps have to be followed to build the .dat-file:
+
+1. Package the resource bundles into a single file
+
+ $ find . -name *.res | sed -e "s/\.\///g" > packagelist.txt
+ $ pkgdata -p region -T build -d . packagelist.txt
+
+2. Clean up
+
+ $ rm -rf build packagelist.txt
+
+3. You can now move region.dat to replace the version bundled with Symfony2.
--- /dev/null
+<?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\Intl\Collator;
+
+use Symfony\Component\Intl\Exception\MethodNotImplementedException;
+use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\Locale\Locale;
+
+/**
+ * Replacement for PHP's native {@link \Collator} class.
+ *
+ * The only methods currently supported in this class are:
+ *
+ * - {@link \__construct}
+ * - {@link create}
+ * - {@link asort}
+ * - {@link getErrorCode}
+ * - {@link getErrorMessage}
+ * - {@link getLocale}
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Collator
+{
+ /* Attribute constants */
+ const FRENCH_COLLATION = 0;
+ const ALTERNATE_HANDLING = 1;
+ const CASE_FIRST = 2;
+ const CASE_LEVEL = 3;
+ const NORMALIZATION_MODE = 4;
+ const STRENGTH = 5;
+ const HIRAGANA_QUATERNARY_MODE = 6;
+ const NUMERIC_COLLATION = 7;
+
+ /* Attribute constants values */
+ const DEFAULT_VALUE = -1;
+
+ const PRIMARY = 0;
+ const SECONDARY = 1;
+ const TERTIARY = 2;
+ const DEFAULT_STRENGTH = 2;
+ const QUATERNARY = 3;
+ const IDENTICAL = 15;
+
+ const OFF = 16;
+ const ON = 17;
+
+ const SHIFTED = 20;
+ const NON_IGNORABLE = 21;
+
+ const LOWER_FIRST = 24;
+ const UPPER_FIRST = 25;
+
+ /* Sorting options */
+ const SORT_REGULAR = 0;
+ const SORT_NUMERIC = 2;
+ const SORT_STRING = 1;
+
+ /**
+ * Constructor
+ *
+ * @param string $locale The locale code. The only currently supported locale is "en".
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ */
+ public function __construct($locale)
+ {
+ if ('en' != $locale) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported');
+ }
+ }
+
+ /**
+ * Static constructor
+ *
+ * @param string $locale The locale code. The only currently supported locale is "en".
+ *
+ * @return Collator
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ */
+ public static function create($locale)
+ {
+ return new self($locale);
+ }
+
+ /**
+ * Sort array maintaining index association
+ *
+ * @param array &$array Input array
+ * @param integer $sortFlag Flags for sorting, can be one of the following:
+ * Collator::SORT_REGULAR - compare items normally (don't change types)
+ * Collator::SORT_NUMERIC - compare items numerically
+ * Collator::SORT_STRING - compare items as strings
+ *
+ * @return Boolean True on success or false on failure
+ */
+ public function asort(&$array, $sortFlag = self::SORT_REGULAR)
+ {
+ $intlToPlainFlagMap = array(
+ self::SORT_REGULAR => \SORT_REGULAR,
+ self::SORT_NUMERIC => \SORT_NUMERIC,
+ self::SORT_STRING => \SORT_STRING,
+ );
+
+ $plainSortFlag = isset($intlToPlainFlagMap[$sortFlag]) ? $intlToPlainFlagMap[$sortFlag] : self::SORT_REGULAR;
+
+ return asort($array, $plainSortFlag);
+ }
+
+ /**
+ * Not supported. Compare two Unicode strings
+ *
+ * @param string $str1 The first string to compare
+ * @param string $str2 The second string to compare
+ *
+ * @return Boolean|int Return the comparison result or false on failure:
+ * 1 if $str1 is greater than $str2
+ * 0 if $str1 is equal than $str2
+ * -1 if $str1 is less than $str2
+ *
+ * @see http://www.php.net/manual/en/collator.compare.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function compare($str1, $str2)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Get a value of an integer collator attribute
+ *
+ * @param int $attr An attribute specifier, one of the attribute constants
+ *
+ * @return Boolean|int The attribute value on success or false on error
+ *
+ * @see http://www.php.net/manual/en/collator.getattribute.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getAttribute($attr)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Returns collator's last error code. Always returns the U_ZERO_ERROR class constant value
+ *
+ * @return int The error code from last collator call
+ */
+ public function getErrorCode()
+ {
+ return IntlGlobals::U_ZERO_ERROR;
+ }
+
+ /**
+ * Returns collator's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value
+ *
+ * @return string The error message from last collator call
+ */
+ public function getErrorMessage()
+ {
+ return 'U_ZERO_ERROR';
+ }
+
+ /**
+ * Returns the collator's locale
+ *
+ * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE)
+ *
+ * @return string The locale used to create the collator. Currently always
+ * returns "en".
+ */
+ public function getLocale($type = Locale::ACTUAL_LOCALE)
+ {
+ return 'en';
+ }
+
+ /**
+ * Not supported. Get sorting key for a string
+ *
+ * @param string $string The string to produce the key from
+ *
+ * @return string The collation key for $string
+ *
+ * @see http://www.php.net/manual/en/collator.getsortkey.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getSortKey($string)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Get current collator's strength
+ *
+ * @return Boolean|int The current collator's strength or false on failure
+ *
+ * @see http://www.php.net/manual/en/collator.getstrength.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getStrength()
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Set a collator's attribute
+ *
+ * @param int $attr An attribute specifier, one of the attribute constants
+ * @param int $val The attribute value, one of the attribute value constants
+ *
+ * @return Boolean True on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/collator.setattribute.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setAttribute($attr, $val)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Set the collator's strength
+ *
+ * @param int $strength Strength to set, possible values:
+ * Collator::PRIMARY
+ * Collator::SECONDARY
+ * Collator::TERTIARY
+ * Collator::QUATERNARY
+ * Collator::IDENTICAL
+ * Collator::DEFAULT
+ *
+ * @return Boolean True on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/collator.setstrength.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setStrength($strength)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Sort array using specified collator and sort keys
+ *
+ * @param array &$arr Array of strings to sort
+ *
+ * @return Boolean True on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/collator.sortwithsortkeys.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function sortWithSortKeys(&$arr)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Sort array using specified collator
+ *
+ * @param array &$arr Array of string to sort
+ * @param int $sortFlag Optional sorting type, one of the following:
+ * Collator::SORT_REGULAR
+ * Collator::SORT_NUMERIC
+ * Collator::SORT_STRING
+ *
+ * @return Boolean True on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/collator.sort.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function sort(&$arr, $sortFlag = self::SORT_REGULAR)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for AM/PM markers format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class AmPmTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ return $dateTime->format('A');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 'AM|PM';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'marker' => $matched
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for day of week format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class DayOfWeekTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $dayOfWeek = $dateTime->format('l');
+ switch ($length) {
+ case 4:
+ return $dayOfWeek;
+ case 5:
+ return $dayOfWeek[0];
+ default:
+ return substr($dayOfWeek, 0, 3);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ switch ($length) {
+ case 4:
+ return 'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday';
+ case 5:
+ return '[MTWFS]';
+ default:
+ return 'Mon|Tue|Wed|Thu|Fri|Sat|Sun';
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array();
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for day of year format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class DayOfYearTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $dayOfYear = $dateTime->format('z') + 1;
+
+ return $this->padLeft($dayOfYear, $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return '\d{'.$length.'}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array();
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for day format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class DayTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ return $this->padLeft($dateTime->format('j'), $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'day' => (int) $matched,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+use Symfony\Component\Intl\Exception\NotImplementedException;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\DateFormatter\DateFormat\MonthTransformer;
+
+/**
+ * Parser and formatter for date formats
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class FullTransformer
+{
+ private $quoteMatch = "'(?:[^']+|'')*'";
+ private $implementedChars = 'MLydQqhDEaHkKmsz';
+ private $notImplementedChars = 'GYuwWFgecSAZvVW';
+ private $regExp;
+
+ /**
+ * @var Transformer[]
+ */
+ private $transformers;
+
+ private $pattern;
+ private $timezone;
+
+ /**
+ * Constructor
+ *
+ * @param string $pattern The pattern to be used to format and/or parse values
+ * @param string $timezone The timezone to perform the date/time calculations
+ */
+ public function __construct($pattern, $timezone)
+ {
+ $this->pattern = $pattern;
+ $this->timezone = $timezone;
+
+ $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars);
+ $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars);
+ $this->regExp = "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/";
+
+ $this->transformers = array(
+ 'M' => new MonthTransformer(),
+ 'L' => new MonthTransformer(),
+ 'y' => new YearTransformer(),
+ 'd' => new DayTransformer(),
+ 'q' => new QuarterTransformer(),
+ 'Q' => new QuarterTransformer(),
+ 'h' => new Hour1201Transformer(),
+ 'D' => new DayOfYearTransformer(),
+ 'E' => new DayOfWeekTransformer(),
+ 'a' => new AmPmTransformer(),
+ 'H' => new Hour2400Transformer(),
+ 'K' => new Hour1200Transformer(),
+ 'k' => new Hour2401Transformer(),
+ 'm' => new MinuteTransformer(),
+ 's' => new SecondTransformer(),
+ 'z' => new TimeZoneTransformer(),
+ );
+ }
+
+ /**
+ * Return the array of Transformer objects
+ *
+ * @return Transformer[] Associative array of Transformer objects (format char => Transformer)
+ */
+ public function getTransformers()
+ {
+ return $this->transformers;
+ }
+
+ /**
+ * Format a DateTime using ICU dateformat pattern
+ *
+ * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value
+ *
+ * @return string The formatted value
+ */
+ public function format(\DateTime $dateTime)
+ {
+ $that = $this;
+
+ $formatted = preg_replace_callback($this->regExp, function($matches) use ($that, $dateTime) {
+ return $that->formatReplace($matches[0], $dateTime);
+ }, $this->pattern);
+
+ return $formatted;
+ }
+
+ /**
+ * Return the formatted ICU value for the matched date characters
+ *
+ * @param string $dateChars The date characters to be replaced with a formatted ICU value
+ * @param DateTime $dateTime A DateTime object to be used to generate the formatted value
+ *
+ * @return string The formatted value
+ *
+ * @throws NotImplementedException When it encounters a not implemented date character
+ */
+ public function formatReplace($dateChars, $dateTime)
+ {
+ $length = strlen($dateChars);
+
+ if ($this->isQuoteMatch($dateChars)) {
+ return $this->replaceQuoteMatch($dateChars);
+ }
+
+ if (isset($this->transformers[$dateChars[0]])) {
+ $transformer = $this->transformers[$dateChars[0]];
+
+ return $transformer->format($dateTime, $length);
+ }
+
+ // handle unimplemented characters
+ if (false !== strpos($this->notImplementedChars, $dateChars[0])) {
+ throw new NotImplementedException(sprintf("Unimplemented date character '%s' in format '%s'", $dateChars[0], $this->pattern));
+ }
+ }
+
+ /**
+ * Parse a pattern based string to a timestamp value
+ *
+ * @param \DateTime $dateTime A configured DateTime object to use to perform the date calculation
+ * @param string $value String to convert to a time value
+ *
+ * @return int The corresponding Unix timestamp
+ *
+ * @throws \InvalidArgumentException When the value can not be matched with pattern
+ */
+ public function parse(\DateTime $dateTime, $value)
+ {
+ $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern);
+ $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/';
+
+ $options = array();
+
+ if (preg_match($reverseMatchingRegExp, $value, $matches)) {
+ $matches = $this->normalizeArray($matches);
+
+ foreach ($this->transformers as $char => $transformer) {
+ if (isset($matches[$char])) {
+ $length = strlen($matches[$char]['pattern']);
+ $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length));
+ }
+ }
+
+ // reset error code and message
+ IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR);
+
+ return $this->calculateUnixTimestamp($dateTime, $options);
+ }
+
+ // behave like the intl extension
+ IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Date parsing failed');
+
+ return false;
+ }
+
+ /**
+ * Retrieve a regular expression to match with a formatted value.
+ *
+ * @param string $pattern The pattern to create the reverse matching regular expression
+ *
+ * @return string The reverse matching regular expression with named captures being formed by the
+ * transformer index in the $transformer array
+ */
+ public function getReverseMatchingRegExp($pattern)
+ {
+ $that = $this;
+
+ $escapedPattern = preg_quote($pattern, '/');
+
+ // ICU 4.8 recognizes slash ("/") in a value to be parsed as a dash ("-") and vice-versa
+ // when parsing a date/time value
+ $escapedPattern = preg_replace('/\\\[\-|\/]/', '[\/\-]', $escapedPattern);
+
+ $reverseMatchingRegExp = preg_replace_callback($this->regExp, function($matches) use ($that) {
+ $length = strlen($matches[0]);
+ $transformerIndex = $matches[0][0];
+
+ $dateChars = $matches[0];
+ if ($that->isQuoteMatch($dateChars)) {
+ return $that->replaceQuoteMatch($dateChars);
+ }
+
+ $transformers = $that->getTransformers();
+ if (isset($transformers[$transformerIndex])) {
+ $transformer = $transformers[$transformerIndex];
+ $captureName = str_repeat($transformerIndex, $length);
+
+ return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')';
+ }
+ }, $escapedPattern);
+
+ return $reverseMatchingRegExp;
+ }
+
+ /**
+ * Check if the first char of a string is a single quote
+ *
+ * @param string $quoteMatch The string to check
+ *
+ * @return Boolean true if matches, false otherwise
+ */
+ public function isQuoteMatch($quoteMatch)
+ {
+ return ("'" === $quoteMatch[0]);
+ }
+
+ /**
+ * Replaces single quotes at the start or end of a string with two single quotes
+ *
+ * @param string $quoteMatch The string to replace the quotes
+ *
+ * @return string A string with the single quotes replaced
+ */
+ public function replaceQuoteMatch($quoteMatch)
+ {
+ if (preg_match("/^'+$/", $quoteMatch)) {
+ return str_replace("''", "'", $quoteMatch);
+ }
+
+ return str_replace("''", "'", substr($quoteMatch, 1, -1));
+ }
+
+ /**
+ * Builds a chars match regular expression
+ *
+ * @param string $specialChars A string of chars to build the regular expression
+ *
+ * @return string The chars match regular expression
+ */
+ protected function buildCharsMatch($specialChars)
+ {
+ $specialCharsArray = str_split($specialChars);
+
+ $specialCharsMatch = implode('|', array_map(function($char) {
+ return $char.'+';
+ }, $specialCharsArray));
+
+ return $specialCharsMatch;
+ }
+
+ /**
+ * Normalize a preg_replace match array, removing the numeric keys and returning an associative array
+ * with the value and pattern values for the matched Transformer
+ *
+ * @param array $data
+ *
+ * @return array
+ */
+ protected function normalizeArray(array $data)
+ {
+ $ret = array();
+
+ foreach ($data as $key => $value) {
+ if (!is_string($key)) {
+ continue;
+ }
+
+ $ret[$key[0]] = array(
+ 'value' => $value,
+ 'pattern' => $key
+ );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Calculates the Unix timestamp based on the matched values by the reverse matching regular
+ * expression of parse()
+ *
+ * @param \DateTime $dateTime The DateTime object to be used to calculate the timestamp
+ * @param array $options An array with the matched values to be used to calculate the timestamp
+ *
+ * @return Boolean|int The calculated timestamp or false if matched date is invalid
+ */
+ protected function calculateUnixTimestamp(\DateTime $dateTime, array $options)
+ {
+ $options = $this->getDefaultValueForOptions($options);
+
+ $year = $options['year'];
+ $month = $options['month'];
+ $day = $options['day'];
+ $hour = $options['hour'];
+ $hourInstance = $options['hourInstance'];
+ $minute = $options['minute'];
+ $second = $options['second'];
+ $marker = $options['marker'];
+ $timezone = $options['timezone'];
+
+ // If month is false, return immediately (intl behavior)
+ if (false === $month) {
+ IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Date parsing failed');
+
+ return false;
+ }
+
+ // Normalize hour
+ if ($hourInstance instanceof HourTransformer) {
+ $hour = $hourInstance->normalizeHour($hour, $marker);
+ }
+
+ // Set the timezone if different from the default one
+ if (null !== $timezone && $timezone !== $this->timezone) {
+ $dateTime->setTimezone(new \DateTimeZone($timezone));
+ }
+
+ // Normalize yy year
+ preg_match_all($this->regExp, $this->pattern, $matches);
+ if (in_array('yy', $matches[0])) {
+ $dateTime->setTimestamp(time());
+ $year = $year > $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
+ }
+
+ $dateTime->setDate($year, $month, $day);
+ $dateTime->setTime($hour, $minute, $second);
+
+ return $dateTime->getTimestamp();
+ }
+
+ /**
+ * Add sensible default values for missing items in the extracted date/time options array. The values
+ * are base in the beginning of the Unix era
+ *
+ * @param array $options
+ *
+ * @return array
+ */
+ private function getDefaultValueForOptions(array $options)
+ {
+ return array(
+ 'year' => isset($options['year']) ? $options['year'] : 1970,
+ 'month' => isset($options['month']) ? $options['month'] : 1,
+ 'day' => isset($options['day']) ? $options['day'] : 1,
+ 'hour' => isset($options['hour']) ? $options['hour'] : 0,
+ 'hourInstance' => isset($options['hourInstance']) ? $options['hourInstance'] : null,
+ 'minute' => isset($options['minute']) ? $options['minute'] : 0,
+ 'second' => isset($options['second']) ? $options['second'] : 0,
+ 'marker' => isset($options['marker']) ? $options['marker'] : null,
+ 'timezone' => isset($options['timezone']) ? $options['timezone'] : null,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for 12 hour format (0-11)
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class Hour1200Transformer extends HourTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $hourOfDay = $dateTime->format('g');
+ $hourOfDay = '12' == $hourOfDay ? '0' : $hourOfDay;
+
+ return $this->padLeft($hourOfDay, $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalizeHour($hour, $marker = null)
+ {
+ if ('PM' === $marker) {
+ $hour += 12;
+ }
+
+ return $hour;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return '\d{1,2}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'hour' => (int) $matched,
+ 'hourInstance' => $this
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for 12 hour format (1-12)
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class Hour1201Transformer extends HourTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ return $this->padLeft($dateTime->format('g'), $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalizeHour($hour, $marker = null)
+ {
+ if ('PM' !== $marker && 12 === $hour) {
+ $hour = 0;
+ } elseif ('PM' === $marker && 12 !== $hour) {
+ // If PM and hour is not 12 (1-12), sum 12 hour
+ $hour += 12;
+ }
+
+ return $hour;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return '\d{1,2}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'hour' => (int) $matched,
+ 'hourInstance' => $this
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for 24 hour format (0-23)
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class Hour2400Transformer extends HourTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ return $this->padLeft($dateTime->format('G'), $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalizeHour($hour, $marker = null)
+ {
+ if ('AM' == $marker) {
+ $hour = 0;
+ } elseif ('PM' == $marker) {
+ $hour = 12;
+ }
+
+ return $hour;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return '\d{1,2}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'hour' => (int) $matched,
+ 'hourInstance' => $this
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for 24 hour format (1-24)
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class Hour2401Transformer extends HourTransformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $hourOfDay = $dateTime->format('G');
+ $hourOfDay = ('0' == $hourOfDay) ? '24' : $hourOfDay;
+
+ return $this->padLeft($hourOfDay, $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function normalizeHour($hour, $marker = null)
+ {
+ if ((null === $marker && 24 === $hour) || 'AM' == $marker) {
+ $hour = 0;
+ } elseif ('PM' == $marker) {
+ $hour = 12;
+ }
+
+ return $hour;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return '\d{1,2}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'hour' => (int) $matched,
+ 'hourInstance' => $this
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Base class for hour transformers
+ *
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ */
+abstract class HourTransformer extends Transformer
+{
+ /**
+ * Returns a normalized hour value suitable for the hour transformer type
+ *
+ * @param int $hour The hour value
+ * @param string $marker An optional AM/PM marker
+ *
+ * @return int The normalized hour value
+ */
+ abstract public function normalizeHour($hour, $marker = null);
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for minute format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class MinuteTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $minuteOfHour = (int) $dateTime->format('i');
+
+ return $this->padLeft($minuteOfHour, $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'minute' => (int) $matched,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for month format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class MonthTransformer extends Transformer
+{
+ /**
+ * @var array
+ */
+ protected static $months = array(
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ );
+
+ /**
+ * Short months names (first 3 letters)
+ * @var array
+ */
+ protected static $shortMonths = array();
+
+ /**
+ * Flipped $months array, $name => $index
+ * @var array
+ */
+ protected static $flippedMonths = array();
+
+ /**
+ * Flipped $shortMonths array, $name => $index
+ * @var array
+ */
+ protected static $flippedShortMonths = array();
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ if (0 === count(self::$shortMonths)) {
+ self::$shortMonths = array_map(function($month) {
+ return substr($month, 0, 3);
+ }, self::$months);
+
+ self::$flippedMonths = array_flip(self::$months);
+ self::$flippedShortMonths = array_flip(self::$shortMonths);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $matchLengthMap = array(
+ 1 => 'n',
+ 2 => 'm',
+ 3 => 'M',
+ 4 => 'F',
+ );
+
+ if (isset($matchLengthMap[$length])) {
+ return $dateTime->format($matchLengthMap[$length]);
+ }
+
+ if (5 === $length) {
+ return substr($dateTime->format('M'), 0, 1);
+ }
+
+ return $this->padLeft($dateTime->format('m'), $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ switch ($length) {
+ case 1:
+ $regExp = '\d{1,2}';
+ break;
+ case 3:
+ $regExp = implode('|', self::$shortMonths);
+ break;
+ case 4:
+ $regExp = implode('|', self::$months);
+ break;
+ case 5:
+ $regExp = '[JFMASOND]';
+ break;
+ default:
+ $regExp = '\d{'.$length.'}';
+ break;
+ }
+
+ return $regExp;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ if (!is_numeric($matched)) {
+ if (3 === $length) {
+ $matched = self::$flippedShortMonths[$matched] + 1;
+ } elseif (4 === $length) {
+ $matched = self::$flippedMonths[$matched] + 1;
+ } elseif (5 === $length) {
+ // IntlDateFormatter::parse() always returns false for MMMMM or LLLLL
+ $matched = false;
+ }
+ } else {
+ $matched = (int) $matched;
+ }
+
+ return array(
+ 'month' => $matched,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for quarter format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class QuarterTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $month = (int) $dateTime->format('n');
+ $quarter = (int) floor(($month - 1) / 3) + 1;
+ switch ($length) {
+ case 1:
+ case 2:
+ return $this->padLeft($quarter, $length);
+ case 3:
+ return 'Q'.$quarter;
+ default:
+ $map = array(1 => '1st quarter', 2 => '2nd quarter', 3 => '3rd quarter', 4 => '4th quarter');
+
+ return $map[$quarter];
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ switch ($length) {
+ case 1:
+ case 2:
+ return '\d{'.$length.'}';
+ case 3:
+ return 'Q\d';
+ default:
+ return '(?:1st|2nd|3rd|4th) quarter';
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array();
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for the second format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class SecondTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $secondOfMinute = (int) $dateTime->format('s');
+
+ return $this->padLeft($secondOfMinute, $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 1 === $length ? '\d{1,2}' : '\d{'.$length.'}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'second' => (int) $matched,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+use Symfony\Component\Intl\Exception\NotImplementedException;
+
+/**
+ * Parser and formatter for time zone format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class TimeZoneTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ *
+ * @throws NotImplementedException When time zone is different than UTC or GMT (Etc/GMT)
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ $timeZone = substr($dateTime->getTimezone()->getName(), 0, 3);
+
+ if (!in_array($timeZone, array('Etc', 'UTC'))) {
+ throw new NotImplementedException('Time zone different than GMT or UTC is not supported as a formatting output.');
+ }
+
+ // From ICU >= 4.8, the zero offset is not more used, example: GMT instead of GMT+00:00
+ $format = (0 !== (int) $dateTime->format('O')) ? '\G\M\TP' : '\G\M\T';
+
+ return $dateTime->format($format);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 'GMT[+-]\d{2}:?\d{2}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'timezone' => self::getEtcTimeZoneId($matched)
+ );
+ }
+
+ /**
+ * Get an Etc/GMT timezone identifier for the specified timezone
+ *
+ * The PHP documentation for timezones states to not use the 'Other' time zones because them exists
+ * "for backwards compatibility". However all Etc/GMT time zones are in the tz database 'etcetera' file,
+ * which indicates they are not deprecated (neither are old names).
+ *
+ * Only GMT, Etc/Universal, Etc/Zulu, Etc/Greenwich, Etc/GMT-0, Etc/GMT+0 and Etc/GMT0 are old names and
+ * are linked to Etc/GMT or Etc/UTC.
+ *
+ * @param string $formattedTimeZone A GMT timezone string (GMT-03:00, e.g.)
+ *
+ * @return string A timezone identifier
+ *
+ * @see http://php.net/manual/en/timezones.others.php
+ * @see http://www.twinsun.com/tz/tz-link.htm
+ *
+ * @throws NotImplementedException When the GMT time zone have minutes offset different than zero
+ * @throws \InvalidArgumentException When the value can not be matched with pattern
+ */
+ public static function getEtcTimeZoneId($formattedTimeZone)
+ {
+ if (preg_match('/GMT(?P<signal>[+-])(?P<hours>\d{2}):?(?P<minutes>\d{2})/', $formattedTimeZone, $matches)) {
+ $hours = (int) $matches['hours'];
+ $minutes = (int) $matches['minutes'];
+ $signal = $matches['signal'] == '-' ? '+' : '-';
+
+ if (0 < $minutes) {
+ throw new NotImplementedException(sprintf(
+ 'It is not possible to use a GMT time zone with minutes offset different than zero (0). GMT time zone tried: %s.',
+ $formattedTimeZone
+ ));
+ }
+
+ return 'Etc/GMT'.($hours !== 0 ? $signal.$hours : '');
+ }
+
+ throw new \InvalidArgumentException('The GMT time zone \'%s\' does not match with the supported formats GMT[+-]HH:MM or GMT[+-]HHMM.');
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for date formats
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+abstract class Transformer
+{
+ /**
+ * Format a value using a configured DateTime as date/time source
+ *
+ *
+ * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value
+ * @param int $length The formatted value string length
+ *
+ * @return string The formatted value
+ */
+ abstract public function format(\DateTime $dateTime, $length);
+
+ /**
+ * Returns a reverse matching regular expression of a string generated by format()
+ *
+ * @param int $length The length of the value to be reverse matched
+ *
+ * @return string The reverse matching regular expression
+ */
+ abstract public function getReverseMatchingRegExp($length);
+
+ /**
+ * Extract date options from a matched value returned by the processing of the reverse matching
+ * regular expression
+ *
+ * @param string $matched The matched value
+ * @param int $length The length of the Transformer pattern string
+ *
+ * @return array An associative array
+ */
+ abstract public function extractDateOptions($matched, $length);
+
+ /**
+ * Pad a string with zeros to the left
+ *
+ * @param string $value The string to be padded
+ * @param int $length The length to pad
+ *
+ * @return string The padded string
+ */
+ protected function padLeft($value, $length)
+ {
+ return str_pad($value, $length, '0', STR_PAD_LEFT);
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter\DateFormat;
+
+/**
+ * Parser and formatter for year format
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class YearTransformer extends Transformer
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(\DateTime $dateTime, $length)
+ {
+ if (2 === $length) {
+ return $dateTime->format('y');
+ }
+
+ return $this->padLeft($dateTime->format('Y'), $length);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getReverseMatchingRegExp($length)
+ {
+ return 2 === $length ? '\d{2}' : '\d{4}';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extractDateOptions($matched, $length)
+ {
+ return array(
+ 'year' => (int) $matched,
+ );
+ }
+}
--- /dev/null
+<?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\Intl\DateFormatter;
+
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\DateFormatter\DateFormat\FullTransformer;
+use Symfony\Component\Intl\Exception\MethodNotImplementedException;
+use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException;
+use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException;
+use Symfony\Component\Intl\Locale\Locale;
+
+/**
+ * Replacement for PHP's native {@link \IntlDateFormatter} class.
+ *
+ * The only methods currently supported in this class are:
+ *
+ * - {@link __construct}
+ * - {@link create}
+ * - {@link format}
+ * - {@link getCalendar}
+ * - {@link getDateType}
+ * - {@link getErrorCode}
+ * - {@link getErrorMessage}
+ * - {@link getLocale}
+ * - {@link getPattern}
+ * - {@link getTimeType}
+ * - {@link getTimeZoneId}
+ * - {@link isLenient}
+ * - {@link parse}
+ * - {@link setLenient}
+ * - {@link setPattern}
+ * - {@link setTimeZoneId}
+ * - {@link setTimeZone}
+ *
+ * @author Igor Wiedler <igor@wiedler.ch>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IntlDateFormatter
+{
+ /**
+ * The error code from the last operation
+ *
+ * @var integer
+ */
+ protected $errorCode = IntlGlobals::U_ZERO_ERROR;
+
+ /**
+ * The error message from the last operation
+ *
+ * @var string
+ */
+ protected $errorMessage = 'U_ZERO_ERROR';
+
+ /* date/time format types */
+ const NONE = -1;
+ const FULL = 0;
+ const LONG = 1;
+ const MEDIUM = 2;
+ const SHORT = 3;
+
+ /* calendar formats */
+ const TRADITIONAL = 0;
+ const GREGORIAN = 1;
+
+ /**
+ * Patterns used to format the date when no pattern is provided
+ *
+ * @var array
+ */
+ private $defaultDateFormats = array(
+ self::NONE => '',
+ self::FULL => 'EEEE, LLLL d, y',
+ self::LONG => 'LLLL d, y',
+ self::MEDIUM => 'LLL d, y',
+ self::SHORT => 'M/d/yy',
+ );
+
+ /**
+ * Patterns used to format the time when no pattern is provided
+ *
+ * @var array
+ */
+ private $defaultTimeFormats = array(
+ self::FULL => 'h:mm:ss a zzzz',
+ self::LONG => 'h:mm:ss a z',
+ self::MEDIUM => 'h:mm:ss a',
+ self::SHORT => 'h:mm a',
+ );
+
+ /**
+ * @var int
+ */
+ private $datetype;
+
+ /**
+ * @var int
+ */
+ private $timetype;
+
+ /**
+ * @var string
+ */
+ private $pattern;
+
+ /**
+ * @var \DateTimeZone
+ */
+ private $dateTimeZone;
+
+ /**
+ * @var Boolean
+ */
+ private $unitializedTimeZoneId = false;
+
+ /**
+ * @var string
+ */
+ private $timeZoneId;
+
+ /**
+ * Constructor
+ *
+ * @param string $locale The locale code. The only currently supported locale is "en".
+ * @param int $datetype Type of date formatting, one of the format type constants
+ * @param int $timetype Type of time formatting, one of the format type constants
+ * @param string $timezone Timezone identifier
+ * @param int $calendar Calendar to use for formatting or parsing. The only currently
+ * supported value is IntlDateFormatter::GREGORIAN.
+ * @param string $pattern Optional pattern to use when formatting
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.create.php
+ * @see http://userguide.icu-project.org/formatparse/datetime
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed
+ */
+ public function __construct($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null)
+ {
+ if ('en' !== $locale) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported');
+ }
+
+ if (self::GREGORIAN !== $calendar) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'calendar', $calendar, 'Only the GREGORIAN calendar is supported');
+ }
+
+ $this->datetype = $datetype;
+ $this->timetype = $timetype;
+
+ $this->setPattern($pattern);
+ $this->setTimeZoneId($timezone);
+ }
+
+ /**
+ * Static constructor
+ *
+ * @param string $locale The locale code. The only currently supported locale is "en".
+ * @param int $datetype Type of date formatting, one of the format type constants
+ * @param int $timetype Type of time formatting, one of the format type constants
+ * @param string $timezone Timezone identifier
+ * @param int $calendar Calendar to use for formatting or parsing; default is Gregorian.
+ * One of the calendar constants.
+ * @param string $pattern Optional pattern to use when formatting
+ *
+ * @return IntlDateFormatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.create.php
+ * @see http://userguide.icu-project.org/formatparse/datetime
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed
+ */
+ public static function create($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null)
+ {
+ return new self($locale, $datetype, $timetype, $timezone, $calendar, $pattern);
+ }
+
+ /**
+ * Format the date/time value (timestamp) as a string
+ *
+ * @param integer|\DateTime $timestamp The timestamp to format. \DateTime objects
+ * are supported as of PHP 5.3.4.
+ *
+ * @return string|Boolean The formatted value or false if formatting failed.
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.format.php
+ *
+ * @throws MethodArgumentValueNotImplementedException If one of the formatting characters is not implemented
+ */
+ public function format($timestamp)
+ {
+ // intl allows timestamps to be passed as arrays - we don't
+ if (is_array($timestamp)) {
+ $message = version_compare(PHP_VERSION, '5.3.4', '>=') ?
+ 'Only integer unix timestamps and DateTime objects are supported' :
+ 'Only integer unix timestamps are supported';
+
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'timestamp', $timestamp, $message);
+ }
+
+ // behave like the intl extension
+ $argumentError = null;
+ if (version_compare(PHP_VERSION, '5.3.4', '<') && !is_int($timestamp)) {
+ $argumentError = 'datefmt_format: takes either an array or an integer timestamp value ';
+ } elseif (version_compare(PHP_VERSION, '5.3.4', '>=') && !is_int($timestamp) && !$timestamp instanceof \DateTime) {
+ $argumentError = 'datefmt_format: takes either an array or an integer timestamp value or a DateTime object';
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=') && !is_int($timestamp)) {
+ $argumentError = sprintf('datefmt_format: string \'%s\' is not numeric, which would be required for it to be a valid date', $timestamp);
+ }
+ }
+
+ if (null !== $argumentError) {
+ IntlGlobals::setError(IntlGlobals::U_ILLEGAL_ARGUMENT_ERROR, $argumentError);
+ $this->errorCode = IntlGlobals::getErrorCode();
+ $this->errorMessage = IntlGlobals::getErrorMessage();
+
+ return false;
+ }
+
+ // As of PHP 5.3.4, IntlDateFormatter::format() accepts DateTime instances
+ if (version_compare(PHP_VERSION, '5.3.4', '>=') && $timestamp instanceof \DateTime) {
+ $timestamp = $timestamp->getTimestamp();
+ }
+
+ $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId());
+ $formatted = $transformer->format($this->createDateTime($timestamp));
+
+ // behave like the intl extension
+ IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR);
+ $this->errorCode = IntlGlobals::getErrorCode();
+ $this->errorMessage = IntlGlobals::getErrorMessage();
+
+ return $formatted;
+ }
+
+ /**
+ * Not supported. Formats an object
+ *
+ * @param object $object
+ * @param mixed $format
+ * @param string $locale
+ *
+ * @return string The formatted value
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.formatobject.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function formatObject($object, $format = null, $locale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Returns the formatter's calendar
+ *
+ * @return int The calendar being used by the formatter. Currently always returns
+ * IntlDateFormatter::GREGORIAN.
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.getcalendar.php
+ */
+ public function getCalendar()
+ {
+ return self::GREGORIAN;
+ }
+
+ /**
+ * Not supported. Returns the formatter's calendar object
+ *
+ * @return object The calendar's object being used by the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.getcalendarobject.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getCalendarObject()
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Returns the formatter's datetype
+ *
+ * @return int The current value of the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.getdatetype.php
+ */
+ public function getDateType()
+ {
+ return $this->datetype;
+ }
+
+ /**
+ * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value
+ *
+ * @return int The error code from last formatter call
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.geterrorcode.php
+ */
+ public function getErrorCode()
+ {
+ return $this->errorCode;
+ }
+
+ /**
+ * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value
+ *
+ * @return string The error message from last formatter call
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.geterrormessage.php
+ */
+ public function getErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * Returns the formatter's locale
+ *
+ * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE)
+ *
+ * @return string The locale used to create the formatter. Currently always
+ * returns "en".
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.getlocale.php
+ */
+ public function getLocale($type = Locale::ACTUAL_LOCALE)
+ {
+ return 'en';
+ }
+
+ /**
+ * Returns the formatter's pattern
+ *
+ * @return string The pattern string used by the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.getpattern.php
+ */
+ public function getPattern()
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * Returns the formatter's time type
+ *
+ * @return string The time type used by the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.gettimetype.php
+ */
+ public function getTimeType()
+ {
+ return $this->timetype;
+ }
+
+ /**
+ * Returns the formatter's timezone identifier
+ *
+ * @return string The timezone identifier used by the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.gettimezoneid.php
+ */
+ public function getTimeZoneId()
+ {
+ if (!$this->unitializedTimeZoneId) {
+ return $this->timeZoneId;
+ }
+
+ // In PHP 5.5 default timezone depends on `date_default_timezone_get()` method
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ return date_default_timezone_get();
+ }
+
+ return null;
+ }
+
+ /**
+ * Not supported. Returns the formatter's timezone
+ *
+ * @return mixed The timezone used by the formatter
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.gettimezone.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getTimeZone()
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Returns whether the formatter is lenient
+ *
+ * @return Boolean Currently always returns false.
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.islenient.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function isLenient()
+ {
+ return false;
+ }
+
+ /**
+ * Not supported. Parse string to a field-based time value
+ *
+ * @param string $value String to convert to a time value
+ * @param int $position Position at which to start the parsing in $value (zero-based).
+ * If no error occurs before $value is consumed, $parse_pos will
+ * contain -1 otherwise it will contain the position at which parsing
+ * ended. If $parse_pos > strlen($value), the parse fails immediately.
+ *
+ * @return string Localtime compatible array of integers: contains 24 hour clock value in tm_hour field
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.localtime.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function localtime($value, &$position = 0)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Parse string to a timestamp value
+ *
+ * @param string $value String to convert to a time value
+ * @param int $position Not supported. Position at which to start the parsing in $value (zero-based).
+ * If no error occurs before $value is consumed, $parse_pos will
+ * contain -1 otherwise it will contain the position at which parsing
+ * ended. If $parse_pos > strlen($value), the parse fails immediately.
+ *
+ * @return string Parsed value as a timestamp
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.parse.php
+ *
+ * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented
+ */
+ public function parse($value, &$position = null)
+ {
+ // We don't calculate the position when parsing the value
+ if (null !== $position) {
+ throw new MethodArgumentNotImplementedException(__METHOD__, 'position');
+ }
+
+ $dateTime = $this->createDateTime(0);
+ $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId());
+
+ $timestamp = $transformer->parse($dateTime, $value);
+
+ // behave like the intl extension. FullTransformer::parse() set the proper error
+ $this->errorCode = IntlGlobals::getErrorCode();
+ $this->errorMessage = IntlGlobals::getErrorMessage();
+
+ return $timestamp;
+ }
+
+ /**
+ * Not supported. Set the formatter's calendar
+ *
+ * @param string $calendar The calendar to use. Default is IntlDateFormatter::GREGORIAN.
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.setcalendar.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setCalendar($calendar)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Set the leniency of the parser
+ *
+ * Define if the parser is strict or lenient in interpreting inputs that do not match the pattern
+ * exactly. Enabling lenient parsing allows the parser to accept otherwise flawed date or time
+ * patterns, parsing as much as possible to obtain a value. Extra space, unrecognized tokens, or
+ * invalid values ("February 30th") are not accepted.
+ *
+ * @param Boolean $lenient Sets whether the parser is lenient or not. Currently
+ * only false (strict) is supported.
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.setlenient.php
+ *
+ * @throws MethodArgumentValueNotImplementedException When $lenient is true
+ */
+ public function setLenient($lenient)
+ {
+ if ($lenient) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'lenient', $lenient, 'Only the strict parser is supported');
+ }
+
+ return true;
+ }
+
+ /**
+ * Set the formatter's pattern
+ *
+ * @param string $pattern A pattern string in conformance with the ICU IntlDateFormatter documentation
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.setpattern.php
+ * @see http://userguide.icu-project.org/formatparse/datetime
+ */
+ public function setPattern($pattern)
+ {
+ if (null === $pattern) {
+ $pattern = $this->getDefaultPattern();
+ }
+
+ $this->pattern = $pattern;
+
+ return true;
+ }
+
+ /**
+ * Set the formatter's timezone identifier
+ *
+ * @param string $timeZoneId The time zone ID string of the time zone to use.
+ * If NULL or the empty string, the default time zone for the
+ * runtime is used.
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.settimezoneid.php
+ */
+ public function setTimeZoneId($timeZoneId)
+ {
+ if (null === $timeZoneId) {
+ // In PHP 5.5 if $timeZoneId is null it fallbacks to `date_default_timezone_get()` method
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $timeZoneId = date_default_timezone_get();
+ } else {
+ // TODO: changes were made to ext/intl in PHP 5.4.4 release that need to be investigated since it will
+ // use ini's date.timezone when the time zone is not provided. As a not well tested workaround, uses UTC.
+ // See the first two items of the commit message for more information:
+ // https://github.com/php/php-src/commit/eb346ef0f419b90739aadfb6cc7b7436c5b521d9
+ $timeZoneId = getenv('TZ') ?: 'UTC';
+ }
+
+ $this->unitializedTimeZoneId = true;
+ }
+
+ // Backup original passed time zone
+ $timeZone = $timeZoneId;
+
+ // Get an Etc/GMT time zone that is accepted for \DateTimeZone
+ if ('GMT' !== $timeZoneId && 0 === strpos($timeZoneId, 'GMT')) {
+ try {
+ $timeZoneId = DateFormat\TimeZoneTransformer::getEtcTimeZoneId($timeZoneId);
+ } catch (\InvalidArgumentException $e) {
+ // Does nothing, will fallback to UTC
+ }
+ }
+
+ try {
+ $this->dateTimeZone = new \DateTimeZone($timeZoneId);
+ } catch (\Exception $e) {
+ $this->dateTimeZone = new \DateTimeZone('UTC');
+ }
+
+ $this->timeZoneId = $timeZone;
+
+ return true;
+ }
+
+ /**
+ * This method was added in PHP 5.5 as replacement for `setTimeZoneId()`
+ *
+ * @param mixed $timeZone
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/intldateformatter.settimezone.php
+ */
+ public function setTimeZone($timeZone)
+ {
+ return $this->setTimeZoneId($timeZone);
+ }
+
+ /**
+ * Create and returns a DateTime object with the specified timestamp and with the
+ * current time zone
+ *
+ * @param int $timestamp
+ *
+ * @return \DateTime
+ */
+ protected function createDateTime($timestamp)
+ {
+ $dateTime = new \DateTime();
+ $dateTime->setTimestamp($timestamp);
+ $dateTime->setTimezone($this->dateTimeZone);
+
+ return $dateTime;
+ }
+
+ /**
+ * Returns a pattern string based in the datetype and timetype values
+ *
+ * @return string
+ */
+ protected function getDefaultPattern()
+ {
+ $patternParts = array();
+ if (self::NONE !== $this->datetype) {
+ $patternParts[] = $this->defaultDateFormats[$this->datetype];
+ }
+ if (self::NONE !== $this->timetype) {
+ $patternParts[] = $this->defaultTimeFormats[$this->timetype];
+ }
+ $pattern = implode(' ', $patternParts);
+
+ return $pattern;
+ }
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * Base BadMethodCallException for the Intl component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * Base ExceptionInterface for the Intl component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * InvalidArgumentException for the Intl component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Intl\Exception;
+
+use Symfony\Component\Intl\Exception\NotImplementedException;
+
+/**
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ */
+class MethodArgumentNotImplementedException extends NotImplementedException
+{
+ /**
+ * Constructor
+ *
+ * @param string $methodName The method name that raised the exception
+ * @param string $argName The argument name that is not implemented
+ */
+ public function __construct($methodName, $argName)
+ {
+ $message = sprintf('The %s() method\'s argument $%s behavior is not implemented.', $methodName, $argName);
+ parent::__construct($message);
+ }
+}
--- /dev/null
+<?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\Intl\Exception;
+
+use Symfony\Component\Intl\Exception\NotImplementedException;
+
+/**
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ */
+class MethodArgumentValueNotImplementedException extends NotImplementedException
+{
+ /**
+ * Constructor
+ *
+ * @param string $methodName The method name that raised the exception
+ * @param string $argName The argument name
+ * @param string $argValue The argument value that is not implemented
+ * @param string $additionalMessage An optional additional message to append to the exception message
+ */
+ public function __construct($methodName, $argName, $argValue, $additionalMessage = '')
+ {
+ $message = sprintf(
+ 'The %s() method\'s argument $%s value %s behavior is not implemented.%s',
+ $methodName,
+ $argName,
+ var_export($argValue, true),
+ $additionalMessage !== '' ? ' '.$additionalMessage.'. ' : ''
+ );
+
+ parent::__construct($message);
+ }
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ */
+class MethodNotImplementedException extends NotImplementedException
+{
+ /**
+ * Constructor
+ *
+ * @param string $methodName The name of the method
+ */
+ public function __construct($methodName)
+ {
+ parent::__construct(sprintf('The %s() is not implemented.', $methodName));
+ }
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * Base exception class for not implemented behaviors of the intl extension in the Locale component.
+ *
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ */
+class NotImplementedException extends RuntimeException
+{
+ const INTL_INSTALL_MESSAGE = 'Please install the "intl" extension for full localization capabilities.';
+
+ /**
+ * Constructor
+ *
+ * @param string $message The exception message. A note to install the intl extension is appended to this string
+ */
+ public function __construct($message)
+ {
+ parent::__construct($message.' '.self::INTL_INSTALL_MESSAGE);
+ }
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * Base OutOfBoundsException for the Intl component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Intl\Exception;
+
+/**
+ * RuntimeException for the Intl component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Intl\Globals;
+
+/**
+ * Provides fake static versions of the global functions in the intl extension
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class IntlGlobals
+{
+ /**
+ * Indicates that no error occurred
+ *
+ * @var integer
+ */
+ const U_ZERO_ERROR = 0;
+
+ /**
+ * Indicates that an invalid argument was passed
+ *
+ * @var integer
+ */
+ const U_ILLEGAL_ARGUMENT_ERROR = 1;
+
+ /**
+ * Indicates that the parse() operation failed
+ *
+ * @var integer
+ */
+ const U_PARSE_ERROR = 9;
+
+ /**
+ * All known error codes
+ *
+ * @var array
+ */
+ private static $errorCodes = array(
+ self::U_ZERO_ERROR => 'U_ZERO_ERROR',
+ self::U_ILLEGAL_ARGUMENT_ERROR => 'U_ILLEGAL_ARGUMENT_ERROR',
+ self::U_PARSE_ERROR => 'U_PARSE_ERROR',
+ );
+
+ /**
+ * The error code of the last operation
+ *
+ * @var integer
+ */
+ private static $errorCode = self::U_ZERO_ERROR;
+
+ /**
+ * The error code of the last operation
+ *
+ * @var integer
+ */
+ private static $errorMessage = 'U_ZERO_ERROR';
+
+ /**
+ * Returns whether the error code indicates a failure
+ *
+ * @param integer $errorCode The error code returned by IntlGlobals::getErrorCode()
+ *
+ * @return Boolean
+ */
+ public static function isFailure($errorCode)
+ {
+ return isset(self::$errorCodes[$errorCode])
+ && $errorCode > self::U_ZERO_ERROR;
+ }
+
+ /**
+ * Returns the error code of the last operation
+ *
+ * Returns IntlGlobals::U_ZERO_ERROR if no error occurred.
+ *
+ * @return integer
+ */
+ public static function getErrorCode()
+ {
+ return self::$errorCode;
+ }
+
+ /**
+ * Returns the error message of the last operation
+ *
+ * Returns "U_ZERO_ERROR" if no error occurred.
+ *
+ * @return string
+ */
+ public static function getErrorMessage()
+ {
+ return self::$errorMessage;
+ }
+
+ /**
+ * Returns the symbolic name for a given error code
+ *
+ * @param integer $code The error code returned by IntlGlobals::getErrorCode()
+ *
+ * @return string
+ */
+ public static function getErrorName($code)
+ {
+ if (isset(self::$errorCodes[$code])) {
+ return self::$errorCodes[$code];
+ }
+
+ return '[BOGUS UErrorCode]';
+ }
+
+ /**
+ * Sets the current error
+ *
+ * @param integer $code One of the error constants in this class
+ * @param string $message The ICU class error message
+ *
+ * @throws \InvalidArgumentException If the code is not one of the error constants in this class
+ */
+ public static function setError($code, $message = '')
+ {
+ if (!isset(self::$errorCodes[$code])) {
+ throw new \InvalidArgumentException(sprintf('No such error code: "%s"', $code));
+ }
+
+ self::$errorMessage = $message ? sprintf('%s: %s', $message, self::$errorCodes[$code]) : self::$errorCodes[$code];
+ self::$errorCode = $code;
+ }
+}
--- /dev/null
+<?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\Intl;
+
+use Symfony\Component\Icu\IcuCurrencyBundle;
+use Symfony\Component\Icu\IcuData;
+use Symfony\Component\Icu\IcuLanguageBundle;
+use Symfony\Component\Icu\IcuLocaleBundle;
+use Symfony\Component\Icu\IcuRegionBundle;
+use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader;
+
+/**
+ * Gives access to internationalization data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Intl
+{
+ /**
+ * The number of resource bundles to buffer. Loading the same resource
+ * bundle for n locales takes up n spots in the buffer.
+ */
+ const BUFFER_SIZE = 10;
+
+ /**
+ * @var ResourceBundle\CurrencyBundleInterface
+ */
+ private static $currencyBundle;
+
+ /**
+ * @var ResourceBundle\LanguageBundleInterface
+ */
+ private static $languageBundle;
+
+ /**
+ * @var ResourceBundle\LocaleBundleInterface
+ */
+ private static $localeBundle;
+
+ /**
+ * @var ResourceBundle\RegionBundleInterface
+ */
+ private static $regionBundle;
+
+ /**
+ * @var string|Boolean|null
+ */
+ private static $icuVersion = false;
+
+ /**
+ * @var string
+ */
+ private static $icuDataVersion = false;
+
+ /**
+ * @var ResourceBundle\Reader\StructuredBundleReaderInterface
+ */
+ private static $bundleReader;
+
+ /**
+ * Returns whether the intl extension is installed.
+ *
+ * @return Boolean Returns true if the intl extension is installed, false otherwise.
+ */
+ public static function isExtensionLoaded()
+ {
+ return class_exists('\ResourceBundle');
+ }
+
+ /**
+ * Returns the bundle containing currency information.
+ *
+ * @return ResourceBundle\CurrencyBundleInterface The currency resource bundle.
+ */
+ public static function getCurrencyBundle()
+ {
+ if (null === self::$currencyBundle) {
+ self::$currencyBundle = new IcuCurrencyBundle(self::getBundleReader());
+ }
+
+ return self::$currencyBundle;
+ }
+
+ /**
+ * Returns the bundle containing language information.
+ *
+ * @return ResourceBundle\LanguageBundleInterface The language resource bundle.
+ */
+ public static function getLanguageBundle()
+ {
+ if (null === self::$languageBundle) {
+ self::$languageBundle = new IcuLanguageBundle(self::getBundleReader());
+ }
+
+ return self::$languageBundle;
+ }
+
+ /**
+ * Returns the bundle containing locale information.
+ *
+ * @return ResourceBundle\LocaleBundleInterface The locale resource bundle.
+ */
+ public static function getLocaleBundle()
+ {
+ if (null === self::$localeBundle) {
+ self::$localeBundle = new IcuLocaleBundle(self::getBundleReader());
+ }
+
+ return self::$localeBundle;
+ }
+
+ /**
+ * Returns the bundle containing region information.
+ *
+ * @return ResourceBundle\RegionBundleInterface The region resource bundle.
+ */
+ public static function getRegionBundle()
+ {
+ if (null === self::$regionBundle) {
+ self::$regionBundle = new IcuRegionBundle(self::getBundleReader());
+ }
+
+ return self::$regionBundle;
+ }
+
+ /**
+ * Returns the version of the installed ICU library.
+ *
+ * @return null|string The ICU version or NULL if it could not be determined.
+ */
+ public static function getIcuVersion()
+ {
+ if (false === self::$icuVersion) {
+ if (!self::isExtensionLoaded()) {
+ self::$icuVersion = self::getIcuStubVersion();
+ } elseif (defined('INTL_ICU_VERSION')) {
+ self::$icuVersion = INTL_ICU_VERSION;
+ } else {
+ try {
+ $reflector = new \ReflectionExtension('intl');
+ ob_start();
+ $reflector->info();
+ $output = strip_tags(ob_get_clean());
+ preg_match('/^ICU version (?:=>)?(.*)$/m', $output, $matches);
+
+ self::$icuVersion = trim($matches[1]);
+ } catch (\ReflectionException $e) {
+ self::$icuVersion = null;
+ }
+ }
+ }
+
+ return self::$icuVersion;
+ }
+
+ /**
+ * Returns the version of the installed ICU data.
+ *
+ * @return string The version of the installed ICU data.
+ */
+ public static function getIcuDataVersion()
+ {
+ if (false === self::$icuDataVersion) {
+ self::$icuDataVersion = IcuData::getVersion();
+ }
+
+ return self::$icuDataVersion;
+ }
+
+ /**
+ * Returns the ICU version that the stub classes mimic.
+ *
+ * @return string The ICU version of the stub classes.
+ */
+ public static function getIcuStubVersion()
+ {
+ return '50.1.2';
+ }
+
+ /**
+ * Returns a resource bundle reader for .php resource bundle files.
+ *
+ * @return ResourceBundle\Reader\StructuredBundleReaderInterface The resource reader.
+ */
+ private static function getBundleReader()
+ {
+ if (null === self::$bundleReader) {
+ self::$bundleReader = new StructuredBundleReader(new BufferedBundleReader(
+ IcuData::getBundleReader(),
+ self::BUFFER_SIZE
+ ));
+ }
+
+ return self::$bundleReader;
+ }
+
+ /**
+ * This class must not be instantiated.
+ */
+ private function __construct() {}
+}
--- /dev/null
+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.
--- /dev/null
+<?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\Intl\Locale;
+
+use Symfony\Component\Intl\Exception\MethodNotImplementedException;
+
+/**
+ * Replacement for PHP's native {@link \Locale} class.
+ *
+ * The only method supported in this class is {@link getDefault}. This method
+ * will always return "en". All other methods will throw an exception when used.
+ *
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Locale
+{
+ const DEFAULT_LOCALE = null;
+
+ /* Locale method constants */
+ const ACTUAL_LOCALE = 0;
+ const VALID_LOCALE = 1;
+
+ /* Language tags constants */
+ const LANG_TAG = 'language';
+ const EXTLANG_TAG = 'extlang';
+ const SCRIPT_TAG = 'script';
+ const REGION_TAG = 'region';
+ const VARIANT_TAG = 'variant';
+ const GRANDFATHERED_LANG_TAG = 'grandfathered';
+ const PRIVATE_TAG = 'private';
+
+ /**
+ * Not supported. Returns the best available locale based on HTTP "Accept-Language" header according to RFC 2616
+ *
+ * @param string $header The string containing the "Accept-Language" header value
+ *
+ * @return string The corresponding locale code
+ *
+ * @see http://www.php.net/manual/en/locale.acceptfromhttp.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function acceptFromHttp($header)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns a correctly ordered and delimited locale code
+ *
+ * @param array $subtags A keyed array where the keys identify the particular locale code subtag
+ *
+ * @return string The corresponding locale code
+ *
+ * @see http://www.php.net/manual/en/locale.composelocale.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function composeLocale(array $subtags)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Checks if a language tag filter matches with locale
+ *
+ * @param string $langtag The language tag to check
+ * @param string $locale The language range to check against
+ * @param Boolean $canonicalize
+ *
+ * @return string The corresponding locale code
+ *
+ * @see http://www.php.net/manual/en/locale.filtermatches.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function filterMatches($langtag, $locale, $canonicalize = false)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the variants for the input locale
+ *
+ * @param string $locale The locale to extract the variants from
+ *
+ * @return array The locale variants
+ *
+ * @see http://www.php.net/manual/en/locale.getallvariants.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getAllVariants($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Returns the default locale
+ *
+ * @return string The default locale code. Always returns 'en'
+ *
+ * @see http://www.php.net/manual/en/locale.getdefault.php
+ */
+ public static function getDefault()
+ {
+ return 'en';
+ }
+
+ /**
+ * Not supported. Returns the localized display name for the locale language
+ *
+ * @param string $locale The locale code to return the display language from
+ * @param string $inLocale Optional format locale code to use to display the language name
+ *
+ * @return string The localized language display name
+ *
+ * @see http://www.php.net/manual/en/locale.getdisplaylanguage.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getDisplayLanguage($locale, $inLocale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the localized display name for the locale
+ *
+ * @param string $locale The locale code to return the display locale name from
+ * @param string $inLocale Optional format locale code to use to display the locale name
+ *
+ * @return string The localized locale display name
+ *
+ * @see http://www.php.net/manual/en/locale.getdisplayname.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getDisplayName($locale, $inLocale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the localized display name for the locale region
+ *
+ * @param string $locale The locale code to return the display region from
+ * @param string $inLocale Optional format locale code to use to display the region name
+ *
+ * @return string The localized region display name
+ *
+ * @see http://www.php.net/manual/en/locale.getdisplayregion.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getDisplayRegion($locale, $inLocale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the localized display name for the locale script
+ *
+ * @param string $locale The locale code to return the display script from
+ * @param string $inLocale Optional format locale code to use to display the script name
+ *
+ * @return string The localized script display name
+ *
+ * @see http://www.php.net/manual/en/locale.getdisplayscript.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getDisplayScript($locale, $inLocale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the localized display name for the locale variant
+ *
+ * @param string $locale The locale code to return the display variant from
+ * @param string $inLocale Optional format locale code to use to display the variant name
+ *
+ * @return string The localized variant display name
+ *
+ * @see http://www.php.net/manual/en/locale.getdisplayvariant.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getDisplayVariant($locale, $inLocale = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the keywords for the locale
+ *
+ * @param string $locale The locale code to extract the keywords from
+ *
+ * @return array Associative array with the extracted variants
+ *
+ * @see http://www.php.net/manual/en/locale.getkeywords.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getKeywords($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the primary language for the locale
+ *
+ * @param string $locale The locale code to extract the language code from
+ *
+ * @return string|null The extracted language code or null in case of error
+ *
+ * @see http://www.php.net/manual/en/locale.getprimarylanguage.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getPrimaryLanguage($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the region for the locale
+ *
+ * @param string $locale The locale code to extract the region code from
+ *
+ * @return string|null The extracted region code or null if not present
+ *
+ * @see http://www.php.net/manual/en/locale.getregion.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getRegion($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the script for the locale
+ *
+ * @param string $locale The locale code to extract the script code from
+ *
+ * @return string|null The extracted script code or null if not present
+ *
+ * @see http://www.php.net/manual/en/locale.getscript.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function getScript($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns the closest language tag for the locale
+ *
+ * @param array $langtag A list of the language tags to compare to locale
+ * @param string $locale The locale to use as the language range when matching
+ * @param Boolean $canonicalize If true, the arguments will be converted to canonical form before matching
+ * @param string $default The locale to use if no match is found
+ *
+ * @see http://www.php.net/manual/en/locale.lookup.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function lookup(array $langtag, $locale, $canonicalize = false, $default = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns an associative array of locale identifier subtags
+ *
+ * @param string $locale The locale code to extract the subtag array from
+ *
+ * @return array Associative array with the extracted subtags
+ *
+ * @see http://www.php.net/manual/en/locale.parselocale.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function parseLocale($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Sets the default runtime locale
+ *
+ * @param string $locale The locale code
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/locale.parselocale.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public static function setDefault($locale)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+}
--- /dev/null
+<?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\Intl\NumberFormatter;
+
+use Symfony\Component\Intl\Exception\NotImplementedException;
+use Symfony\Component\Intl\Exception\MethodNotImplementedException;
+use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException;
+use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\Locale\Locale;
+
+/**
+ * Replacement for PHP's native {@link \NumberFormatter} class.
+ *
+ * The only methods currently supported in this class are:
+ *
+ * - {@link __construct}
+ * - {@link create}
+ * - {@link formatCurrency}
+ * - {@link format}
+ * - {@link getAttribute}
+ * - {@link getErrorCode}
+ * - {@link getErrorMessage}
+ * - {@link getLocale}
+ * - {@link parse}
+ * - {@link setAttribute}
+ *
+ * @author Eriksen Costa <eriksen.costa@infranology.com.br>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class NumberFormatter
+{
+ /* Format style constants */
+ const PATTERN_DECIMAL = 0;
+ const DECIMAL = 1;
+ const CURRENCY = 2;
+ const PERCENT = 3;
+ const SCIENTIFIC = 4;
+ const SPELLOUT = 5;
+ const ORDINAL = 6;
+ const DURATION = 7;
+ const PATTERN_RULEBASED = 9;
+ const IGNORE = 0;
+ const DEFAULT_STYLE = 1;
+
+ /* Format type constants */
+ const TYPE_DEFAULT = 0;
+ const TYPE_INT32 = 1;
+ const TYPE_INT64 = 2;
+ const TYPE_DOUBLE = 3;
+ const TYPE_CURRENCY = 4;
+
+ /* Numeric attribute constants */
+ const PARSE_INT_ONLY = 0;
+ const GROUPING_USED = 1;
+ const DECIMAL_ALWAYS_SHOWN = 2;
+ const MAX_INTEGER_DIGITS = 3;
+ const MIN_INTEGER_DIGITS = 4;
+ const INTEGER_DIGITS = 5;
+ const MAX_FRACTION_DIGITS = 6;
+ const MIN_FRACTION_DIGITS = 7;
+ const FRACTION_DIGITS = 8;
+ const MULTIPLIER = 9;
+ const GROUPING_SIZE = 10;
+ const ROUNDING_MODE = 11;
+ const ROUNDING_INCREMENT = 12;
+ const FORMAT_WIDTH = 13;
+ const PADDING_POSITION = 14;
+ const SECONDARY_GROUPING_SIZE = 15;
+ const SIGNIFICANT_DIGITS_USED = 16;
+ const MIN_SIGNIFICANT_DIGITS = 17;
+ const MAX_SIGNIFICANT_DIGITS = 18;
+ const LENIENT_PARSE = 19;
+
+ /* Text attribute constants */
+ const POSITIVE_PREFIX = 0;
+ const POSITIVE_SUFFIX = 1;
+ const NEGATIVE_PREFIX = 2;
+ const NEGATIVE_SUFFIX = 3;
+ const PADDING_CHARACTER = 4;
+ const CURRENCY_CODE = 5;
+ const DEFAULT_RULESET = 6;
+ const PUBLIC_RULESETS = 7;
+
+ /* Format symbol constants */
+ const DECIMAL_SEPARATOR_SYMBOL = 0;
+ const GROUPING_SEPARATOR_SYMBOL = 1;
+ const PATTERN_SEPARATOR_SYMBOL = 2;
+ const PERCENT_SYMBOL = 3;
+ const ZERO_DIGIT_SYMBOL = 4;
+ const DIGIT_SYMBOL = 5;
+ const MINUS_SIGN_SYMBOL = 6;
+ const PLUS_SIGN_SYMBOL = 7;
+ const CURRENCY_SYMBOL = 8;
+ const INTL_CURRENCY_SYMBOL = 9;
+ const MONETARY_SEPARATOR_SYMBOL = 10;
+ const EXPONENTIAL_SYMBOL = 11;
+ const PERMILL_SYMBOL = 12;
+ const PAD_ESCAPE_SYMBOL = 13;
+ const INFINITY_SYMBOL = 14;
+ const NAN_SYMBOL = 15;
+ const SIGNIFICANT_DIGIT_SYMBOL = 16;
+ const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17;
+
+ /* Rounding mode values used by NumberFormatter::setAttribute() with NumberFormatter::ROUNDING_MODE attribute */
+ const ROUND_CEILING = 0;
+ const ROUND_FLOOR = 1;
+ const ROUND_DOWN = 2;
+ const ROUND_UP = 3;
+ const ROUND_HALFEVEN = 4;
+ const ROUND_HALFDOWN = 5;
+ const ROUND_HALFUP = 6;
+
+ /* Pad position values used by NumberFormatter::setAttribute() with NumberFormatter::PADDING_POSITION attribute */
+ const PAD_BEFORE_PREFIX = 0;
+ const PAD_AFTER_PREFIX = 1;
+ const PAD_BEFORE_SUFFIX = 2;
+ const PAD_AFTER_SUFFIX = 3;
+
+ /**
+ * The error code from the last operation
+ *
+ * @var integer
+ */
+ protected $errorCode = IntlGlobals::U_ZERO_ERROR;
+
+ /**
+ * The error message from the last operation
+ *
+ * @var string
+ */
+ protected $errorMessage = 'U_ZERO_ERROR';
+
+ /**
+ * @var int
+ */
+ private $style;
+
+ /**
+ * Default values for the en locale
+ *
+ * @var array
+ */
+ private $attributes = array(
+ self::FRACTION_DIGITS => 0,
+ self::GROUPING_USED => 1,
+ self::ROUNDING_MODE => self::ROUND_HALFEVEN
+ );
+
+ /**
+ * Holds the initialized attributes code
+ *
+ * @var array
+ */
+ private $initializedAttributes = array();
+
+ /**
+ * The supported styles to the constructor $styles argument
+ *
+ * @var array
+ */
+ private static $supportedStyles = array(
+ 'CURRENCY' => self::CURRENCY,
+ 'DECIMAL' => self::DECIMAL
+ );
+
+ /**
+ * Supported attributes to the setAttribute() $attr argument
+ *
+ * @var array
+ */
+ private static $supportedAttributes = array(
+ 'FRACTION_DIGITS' => self::FRACTION_DIGITS,
+ 'GROUPING_USED' => self::GROUPING_USED,
+ 'ROUNDING_MODE' => self::ROUNDING_MODE
+ );
+
+ /**
+ * The available rounding modes for setAttribute() usage with
+ * NumberFormatter::ROUNDING_MODE. NumberFormatter::ROUND_DOWN
+ * and NumberFormatter::ROUND_UP does not have a PHP only equivalent
+ *
+ * @var array
+ */
+ private static $roundingModes = array(
+ 'ROUND_HALFEVEN' => self::ROUND_HALFEVEN,
+ 'ROUND_HALFDOWN' => self::ROUND_HALFDOWN,
+ 'ROUND_HALFUP' => self::ROUND_HALFUP
+ );
+
+ /**
+ * The mapping between NumberFormatter rounding modes to the available
+ * modes in PHP's round() function.
+ *
+ * @see http://www.php.net/manual/en/function.round.php
+ *
+ * @var array
+ */
+ private static $phpRoundingMap = array(
+ self::ROUND_HALFDOWN => \PHP_ROUND_HALF_DOWN,
+ self::ROUND_HALFEVEN => \PHP_ROUND_HALF_EVEN,
+ self::ROUND_HALFUP => \PHP_ROUND_HALF_UP
+ );
+
+ /**
+ * The maximum values of the integer type in 32 bit platforms.
+ *
+ * @var array
+ */
+ private static $int32Range = array(
+ 'positive' => 2147483647,
+ 'negative' => -2147483648
+ );
+
+ /**
+ * The maximum values of the integer type in 64 bit platforms.
+ *
+ * @var array
+ */
+ private static $int64Range = array(
+ 'positive' => 9223372036854775807,
+ 'negative' => -9223372036854775808
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param string $locale The locale code. The only currently supported locale is "en".
+ * @param int $style Style of the formatting, one of the format style constants.
+ * The only supported styles are NumberFormatter::DECIMAL
+ * and NumberFormatter::CURRENCY.
+ * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or
+ * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax
+ * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation
+ *
+ * @see http://www.php.net/manual/en/numberformatter.create.php
+ * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details
+ * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ * @throws MethodArgumentValueNotImplementedException When the $style is not supported
+ * @throws MethodArgumentNotImplementedException When the pattern value is different than null
+ */
+ public function __construct($locale = 'en', $style = null, $pattern = null)
+ {
+ if ('en' != $locale) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported');
+ }
+
+ if (!in_array($style, self::$supportedStyles)) {
+ $message = sprintf('The available styles are: %s.', implode(', ', array_keys(self::$supportedStyles)));
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'style', $style, $message);
+ }
+
+ if (null !== $pattern) {
+ throw new MethodArgumentNotImplementedException(__METHOD__, 'pattern');
+ }
+
+ $this->style = $style;
+ }
+
+ /**
+ * Static constructor.
+ *
+ * @param string $locale The locale code. The only supported locale is "en".
+ * @param int $style Style of the formatting, one of the format style constants.
+ * The only currently supported styles are NumberFormatter::DECIMAL
+ * and NumberFormatter::CURRENCY.
+ * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or
+ * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax
+ * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation
+ *
+ * @return NumberFormatter
+ *
+ * @see http://www.php.net/manual/en/numberformatter.create.php
+ * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details
+ * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details
+ *
+ * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
+ * @throws MethodArgumentValueNotImplementedException When the $style is not supported
+ * @throws MethodArgumentNotImplementedException When the pattern value is different than null
+ */
+ public static function create($locale = 'en', $style = null, $pattern = null)
+ {
+ return new self($locale, $style, $pattern);
+ }
+
+ /**
+ * Format a currency value
+ *
+ * @param float $value The numeric currency value
+ * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use
+ *
+ * @return string The formatted currency value
+ *
+ * @see http://www.php.net/manual/en/numberformatter.formatcurrency.php
+ * @see http://www.iso.org/iso/support/faqs/faqs_widely_used_standards/widely_used_standards_other/currency_codes/currency_codes_list-1.htm
+ */
+ public function formatCurrency($value, $currency)
+ {
+ if ($this->style == self::DECIMAL) {
+ return $this->format($value);
+ }
+
+ $symbol = Intl::getCurrencyBundle()->getCurrencySymbol($currency, 'en');
+ $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency);
+
+ $value = $this->roundCurrency($value, $currency);
+
+ $negative = false;
+ if (0 > $value) {
+ $negative = true;
+ $value *= -1;
+ }
+
+ $value = $this->formatNumber($value, $fractionDigits);
+
+ $ret = $symbol.$value;
+
+ return $negative ? '('.$ret.')' : $ret;
+ }
+
+ /**
+ * Format a number
+ *
+ * @param number $value The value to format
+ * @param int $type Type of the formatting, one of the format type constants.
+ * Only type NumberFormatter::TYPE_DEFAULT is currently supported.
+ *
+ * @return Boolean|string The formatted value or false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.format.php
+ *
+ * @throws NotImplementedException If the method is called with the class $style 'CURRENCY'
+ * @throws MethodArgumentValueNotImplementedException If the $type is different than TYPE_DEFAULT
+ */
+ public function format($value, $type = self::TYPE_DEFAULT)
+ {
+ // The original NumberFormatter does not support this format type
+ if ($type == self::TYPE_CURRENCY) {
+ trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING);
+
+ return false;
+ }
+
+ if ($this->style == self::CURRENCY) {
+ throw new NotImplementedException(sprintf(
+ '%s() method does not support the formatting of currencies (instance with CURRENCY style). %s',
+ __METHOD__, NotImplementedException::INTL_INSTALL_MESSAGE
+ ));
+ }
+
+ // Only the default type is supported.
+ if ($type != self::TYPE_DEFAULT) {
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'type', $type, 'Only TYPE_DEFAULT is supported');
+ }
+
+ $fractionDigits = $this->getAttribute(self::FRACTION_DIGITS);
+
+ $value = $this->round($value, $fractionDigits);
+ $value = $this->formatNumber($value, $fractionDigits);
+
+ // behave like the intl extension
+ $this->resetError();
+
+ return $value;
+ }
+
+ /**
+ * Returns an attribute value
+ *
+ * @param int $attr An attribute specifier, one of the numeric attribute constants
+ *
+ * @return Boolean|int The attribute value on success or false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.getattribute.php
+ */
+ public function getAttribute($attr)
+ {
+ return isset($this->attributes[$attr]) ? $this->attributes[$attr] : null;
+ }
+
+ /**
+ * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value
+ *
+ * @return int The error code from last formatter call
+ *
+ * @see http://www.php.net/manual/en/numberformatter.geterrorcode.php
+ */
+ public function getErrorCode()
+ {
+ return $this->errorCode;
+ }
+
+ /**
+ * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value
+ *
+ * @return string The error message from last formatter call
+ *
+ * @see http://www.php.net/manual/en/numberformatter.geterrormessage.php
+ */
+ public function getErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * Returns the formatter's locale
+ *
+ * The parameter $type is currently ignored.
+ *
+ * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE)
+ *
+ * @return string The locale used to create the formatter. Currently always
+ * returns "en".
+ *
+ * @see http://www.php.net/manual/en/numberformatter.getlocale.php
+ */
+ public function getLocale($type = Locale::ACTUAL_LOCALE)
+ {
+ return 'en';
+ }
+
+ /**
+ * Not supported. Returns the formatter's pattern
+ *
+ * @return Boolean|string The pattern string used by the formatter or false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.getpattern.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getPattern()
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns a formatter symbol value
+ *
+ * @param int $attr A symbol specifier, one of the format symbol constants
+ *
+ * @return Boolean|string The symbol value or false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.getsymbol.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getSymbol($attr)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Returns a formatter text attribute value
+ *
+ * @param int $attr An attribute specifier, one of the text attribute constants
+ *
+ * @return Boolean|string The attribute value or false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.gettextattribute.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function getTextAttribute($attr)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Parse a currency number
+ *
+ * @param string $value The value to parse
+ * @param string $currency Parameter to receive the currency name (reference)
+ * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended
+ *
+ * @return Boolean|string The parsed numeric value of false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.parsecurrency.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function parseCurrency($value, &$currency, &$position = null)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Parse a number
+ *
+ * @param string $value The value to parse
+ * @param int $type Type of the formatting, one of the format type constants.
+ * The only currently supported types are NumberFormatter::TYPE_DOUBLE,
+ * NumberFormatter::TYPE_INT32 and NumberFormatter::TYPE_INT64.
+ * @param int $position Not supported. Offset to begin the parsing on return this value will hold the offset at which the parsing ended
+ *
+ * @return Boolean|string The parsed value of false on error
+ *
+ * @see http://www.php.net/manual/en/numberformatter.parse.php
+ *
+ * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented
+ */
+ public function parse($value, $type = self::TYPE_DOUBLE, &$position = null)
+ {
+ if ($type == self::TYPE_DEFAULT || $type == self::TYPE_CURRENCY) {
+ trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING);
+
+ return false;
+ }
+
+ // We don't calculate the position when parsing the value
+ if (null !== $position) {
+ throw new MethodArgumentNotImplementedException(__METHOD__, 'position');
+ }
+
+ preg_match('/^([^0-9\-]{0,})(.*)/', $value, $matches);
+
+ // Any string before the numeric value causes error in the parsing
+ if (isset($matches[1]) && !empty($matches[1])) {
+ IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Number parsing failed');
+ $this->errorCode = IntlGlobals::getErrorCode();
+ $this->errorMessage = IntlGlobals::getErrorMessage();
+
+ return false;
+ }
+
+ // Remove everything that is not number or dot (.)
+ $value = preg_replace('/[^0-9\.\-]/', '', $value);
+ $value = $this->convertValueDataType($value, $type);
+
+ // behave like the intl extension
+ $this->resetError();
+
+ return $value;
+ }
+
+ /**
+ * Set an attribute
+ *
+ * @param int $attr An attribute specifier, one of the numeric attribute constants.
+ * The only currently supported attributes are NumberFormatter::FRACTION_DIGITS,
+ * NumberFormatter::GROUPING_USED and NumberFormatter::ROUNDING_MODE.
+ * @param int $value The attribute value. The only currently supported rounding modes are
+ * NumberFormatter::ROUND_HALFEVEN, NumberFormatter::ROUND_HALFDOWN and
+ * NumberFormatter::ROUND_HALFUP.
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/numberformatter.setattribute.php
+ *
+ * @throws MethodArgumentValueNotImplementedException When the $attr is not supported
+ * @throws MethodArgumentValueNotImplementedException When the $value is not supported
+ */
+ public function setAttribute($attr, $value)
+ {
+ if (!in_array($attr, self::$supportedAttributes)) {
+ $message = sprintf(
+ 'The available attributes are: %s',
+ implode(', ', array_keys(self::$supportedAttributes))
+ );
+
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message);
+ }
+
+ if (self::$supportedAttributes['ROUNDING_MODE'] == $attr && $this->isInvalidRoundingMode($value)) {
+ $message = sprintf(
+ 'The supported values for ROUNDING_MODE are: %s',
+ implode(', ', array_keys(self::$roundingModes))
+ );
+
+ throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message);
+ }
+
+ if (self::$supportedAttributes['GROUPING_USED'] == $attr) {
+ $value = $this->normalizeGroupingUsedValue($value);
+ }
+
+ if (self::$supportedAttributes['FRACTION_DIGITS'] == $attr) {
+ $value = $this->normalizeFractionDigitsValue($value);
+ }
+
+ $this->attributes[$attr] = $value;
+ $this->initializedAttributes[$attr] = true;
+
+ return true;
+ }
+
+ /**
+ * Not supported. Set the formatter's pattern
+ *
+ * @param string $pattern A pattern string in conformance with the ICU DecimalFormat documentation
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/numberformatter.setpattern.php
+ * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setPattern($pattern)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Set the formatter's symbol
+ *
+ * @param int $attr A symbol specifier, one of the format symbol constants
+ * @param string $value The value for the symbol
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/numberformatter.setsymbol.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setSymbol($attr, $value)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Not supported. Set a text attribute
+ *
+ * @param int $attr An attribute specifier, one of the text attribute constants
+ * @param int $value The attribute value
+ *
+ * @return Boolean true on success or false on failure
+ *
+ * @see http://www.php.net/manual/en/numberformatter.settextattribute.php
+ *
+ * @throws MethodNotImplementedException
+ */
+ public function setTextAttribute($attr, $value)
+ {
+ throw new MethodNotImplementedException(__METHOD__);
+ }
+
+ /**
+ * Set the error to the default U_ZERO_ERROR
+ */
+ protected function resetError()
+ {
+ IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR);
+ $this->errorCode = IntlGlobals::getErrorCode();
+ $this->errorMessage = IntlGlobals::getErrorMessage();
+ }
+
+ /**
+ * Rounds a currency value, applying increment rounding if applicable
+ *
+ * When a currency have a rounding increment, an extra round is made after the first one. The rounding factor is
+ * determined in the ICU data and is explained as of:
+ *
+ * "the rounding increment is given in units of 10^(-fraction_digits)"
+ *
+ * The only actual rounding data as of this writing, is CHF.
+ *
+ * @param float $value The numeric currency value
+ * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use
+ *
+ * @return string The rounded numeric currency value
+ *
+ * @see http://en.wikipedia.org/wiki/Swedish_rounding
+ * @see http://www.docjar.com/html/api/com/ibm/icu/util/Currency.java.html#1007
+ */
+ private function roundCurrency($value, $currency)
+ {
+ $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency);
+ $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement($currency);
+
+ // Round with the formatter rounding mode
+ $value = $this->round($value, $fractionDigits);
+
+ // Swiss rounding
+ if (0 < $roundingIncrement && 0 < $fractionDigits) {
+ $roundingFactor = $roundingIncrement / pow(10, $fractionDigits);
+ $value = round($value / $roundingFactor) * $roundingFactor;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Rounds a value.
+ *
+ * @param integer|float $value The value to round
+ * @param int $precision The number of decimal digits to round to
+ *
+ * @return integer|float The rounded value
+ */
+ private function round($value, $precision)
+ {
+ $precision = $this->getUnitializedPrecision($value, $precision);
+
+ $roundingMode = self::$phpRoundingMap[$this->getAttribute(self::ROUNDING_MODE)];
+ $value = round($value, $precision, $roundingMode);
+
+ return $value;
+ }
+
+ /**
+ * Formats a number.
+ *
+ * @param integer|float $value The numeric value to format
+ * @param int $precision The number of decimal digits to use
+ *
+ * @return string The formatted number
+ */
+ private function formatNumber($value, $precision)
+ {
+ $precision = $this->getUnitializedPrecision($value, $precision);
+
+ return number_format($value, $precision, '.', $this->getAttribute(self::GROUPING_USED) ? ',' : '');
+ }
+
+ /**
+ * Returns the precision value if the DECIMAL style is being used and the FRACTION_DIGITS attribute is unitialized.
+ *
+ * @param integer|float $value The value to get the precision from if the FRACTION_DIGITS attribute is unitialized
+ * @param int $precision The precision value to returns if the FRACTION_DIGITS attribute is initialized
+ *
+ * @return int The precision value
+ */
+ private function getUnitializedPrecision($value, $precision)
+ {
+ if ($this->style == self::CURRENCY) {
+ return $precision;
+ }
+
+ if (!$this->isInitializedAttribute(self::FRACTION_DIGITS)) {
+ preg_match('/.*\.(.*)/', (string) $value, $digits);
+ if (isset($digits[1])) {
+ $precision = strlen($digits[1]);
+ }
+ }
+
+ return $precision;
+ }
+
+ /**
+ * Check if the attribute is initialized (value set by client code).
+ *
+ * @param string $attr The attribute name
+ *
+ * @return Boolean true if the value was set by client, false otherwise
+ */
+ private function isInitializedAttribute($attr)
+ {
+ return isset($this->initializedAttributes[$attr]);
+ }
+
+ /**
+ * Returns the numeric value using the $type to convert to the right data type.
+ *
+ * @param mixed $value The value to be converted
+ * @param int $type The type to convert. Can be TYPE_DOUBLE (float) or TYPE_INT32 (int)
+ *
+ * @return integer|float The converted value
+ */
+ private function convertValueDataType($value, $type)
+ {
+ if ($type == self::TYPE_DOUBLE) {
+ $value = (float) $value;
+ } elseif ($type == self::TYPE_INT32) {
+ $value = $this->getInt32Value($value);
+ } elseif ($type == self::TYPE_INT64) {
+ $value = $this->getInt64Value($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the value data type to int or returns false if the value is out of the integer value range.
+ *
+ * @param mixed $value The value to be converted
+ *
+ * @return int The converted value
+ */
+ private function getInt32Value($value)
+ {
+ if ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) {
+ return false;
+ }
+
+ return (int) $value;
+ }
+
+ /**
+ * Convert the value data type to int or returns false if the value is out of the integer value range.
+ *
+ * @param mixed $value The value to be converted
+ *
+ * @return int|float The converted value
+ *
+ * @see https://bugs.php.net/bug.php?id=59597 Bug #59597
+ */
+ private function getInt64Value($value)
+ {
+ if ($value > self::$int64Range['positive'] || $value < self::$int64Range['negative']) {
+ return false;
+ }
+
+ if (PHP_INT_SIZE !== 8 && ($value > self::$int32Range['positive'] || $value <= self::$int32Range['negative'])) {
+ // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
+ // The negative PHP_INT_MAX was being converted to float
+ if (
+ $value == self::$int32Range['negative'] &&
+ (
+ (version_compare(PHP_VERSION, '5.4.0', '<') && version_compare(PHP_VERSION, '5.3.14', '>=')) ||
+ version_compare(PHP_VERSION, '5.4.4', '>=')
+ )
+ ) {
+ return (int) $value;
+ }
+
+ return (float) $value;
+ }
+
+ if (PHP_INT_SIZE === 8) {
+ // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
+ // A 32 bit integer was being generated instead of a 64 bit integer
+ if (
+ ($value > self::$int32Range['positive'] || $value < self::$int32Range['negative']) &&
+ (
+ (version_compare(PHP_VERSION, '5.3.14', '<')) ||
+ (version_compare(PHP_VERSION, '5.4.0', '>=') && version_compare(PHP_VERSION, '5.4.4', '<'))
+ )
+ ) {
+ $value = (-2147483648 - ($value % -2147483648)) * ($value / abs($value));
+ }
+ }
+
+ return (int) $value;
+ }
+
+ /**
+ * Check if the rounding mode is invalid.
+ *
+ * @param int $value The rounding mode value to check
+ *
+ * @return Boolean true if the rounding mode is invalid, false otherwise
+ */
+ private function isInvalidRoundingMode($value)
+ {
+ if (in_array($value, self::$roundingModes, true)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the normalized value for the GROUPING_USED attribute. Any value that can be converted to int will be
+ * cast to Boolean and then to int again. This way, negative values are converted to 1 and string values to 0.
+ *
+ * @param mixed $value The value to be normalized
+ *
+ * @return int The normalized value for the attribute (0 or 1)
+ */
+ private function normalizeGroupingUsedValue($value)
+ {
+ return (int) (Boolean) (int) $value;
+ }
+
+ /**
+ * Returns the normalized value for the FRACTION_DIGITS attribute. The value is converted to int and if negative,
+ * the returned value will be 0.
+ *
+ * @param mixed $value The value to be normalized
+ *
+ * @return int The normalized value for the attribute
+ */
+ private function normalizeFractionDigitsValue($value)
+ {
+ $value = (int) $value;
+
+ return (0 > $value) ? 0 : $value;
+ }
+}
--- /dev/null
+Intl Component
+=============
+
+A PHP replacement layer for the C intl extension that includes additional data
+from the ICU library.
+
+The replacement layer is limited to the locale "en". If you want to use other
+locales, you should [install the intl extension] [1] instead.
+
+Documentation
+-------------
+
+The documentation for the component can be found [online] [2].
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Intl/
+ $ composer.phar install --dev
+ $ phpunit
+
+[0]: http://www.php.net/manual/en/intl.setup.php
+[1]: http://symfony.com/doc/2.3/components/intl.html
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface;
+
+/**
+ * Base class for {@link ResourceBundleInterface} implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractBundle implements ResourceBundleInterface
+{
+ /**
+ * @var string
+ */
+ private $path;
+
+ /**
+ * @var StructuredBundleReaderInterface
+ */
+ private $reader;
+
+ /**
+ * Creates a bundle at the given path using the given reader for reading
+ * bundle entries.
+ *
+ * @param string $path The path to the bundle.
+ * @param StructuredBundleReaderInterface $reader The reader for reading
+ * the bundle.
+ */
+ public function __construct($path, StructuredBundleReaderInterface $reader)
+ {
+ $this->path = $path;
+ $this->reader = $reader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocales()
+ {
+ return $this->reader->getLocales($this->path);
+ }
+
+ /**
+ * Proxy method for {@link StructuredBundleReaderInterface#read}.
+ */
+ protected function read($locale)
+ {
+ return $this->reader->read($this->path, $locale);
+ }
+
+ /**
+ * Proxy method for {@link StructuredBundleReaderInterface#readEntry}.
+ */
+ protected function readEntry($locale, array $indices, $mergeFallback = false)
+ {
+ return $this->reader->readEntry($this->path, $locale, $indices, $mergeFallback);
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Compiler;
+
+use Symfony\Component\Intl\Exception\RuntimeException;
+
+/**
+ * Compiles .txt resource bundles to binary .res files.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BundleCompiler implements BundleCompilerInterface
+{
+ /**
+ * @var string The path to the "genrb" executable.
+ */
+ private $genrb;
+
+ /**
+ * Creates a new compiler based on the "genrb" executable.
+ *
+ * @param string $genrb Optional. The path to the "genrb" executable.
+ * @param string $envVars Optional. Environment variables to be loaded when
+ * running "genrb".
+ *
+ * @throws RuntimeException If the "genrb" cannot be found.
+ */
+ public function __construct($genrb = 'genrb', $envVars = '')
+ {
+ exec('which ' . $genrb, $output, $status);
+
+ if (0 !== $status) {
+ throw new RuntimeException(sprintf(
+ 'The command "%s" is not installed',
+ $genrb
+ ));
+ }
+
+ $this->genrb = ($envVars ? $envVars . ' ' : '') . $genrb;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function compile($sourcePath, $targetDir)
+ {
+ if (is_dir($sourcePath)) {
+ $sourcePath .= '/*.txt';
+ }
+
+ exec($this->genrb.' --quiet -e UTF-8 -d '.$targetDir.' '.$sourcePath, $output, $status);
+
+ if ($status !== 0) {
+ throw new RuntimeException(sprintf(
+ 'genrb failed with status %d while compiling %s to %s.',
+ $status,
+ $sourcePath,
+ $targetDir
+ ));
+ }
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Compiler;
+
+/**
+ * Compiles a resource bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface BundleCompilerInterface
+{
+ /**
+ * Compiles a resource bundle at the given source to the given target
+ * directory.
+ *
+ * @param string $sourcePath
+ * @param string $targetDir
+ */
+ public function compile($sourcePath, $targetDir);
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Default implementation of {@link CurrencyBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CurrencyBundle extends AbstractBundle implements CurrencyBundleInterface
+{
+ const INDEX_NAME = 0;
+
+ const INDEX_SYMBOL = 1;
+
+ const INDEX_FRACTION_DIGITS = 2;
+
+ const INDEX_ROUNDING_INCREMENT = 3;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrencySymbol($currency, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_SYMBOL));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrencyName($currency, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_NAME));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCurrencyNames($locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($currencies = $this->readEntry($locale, array('Currencies')))) {
+ return array();
+ }
+
+ if ($currencies instanceof \Traversable) {
+ $currencies = iterator_to_array($currencies);
+ }
+
+ $index = static::INDEX_NAME;
+
+ array_walk($currencies, function (&$value) use ($index) {
+ $value = $value[$index];
+ });
+
+ return $currencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFractionDigits($currency)
+ {
+ return $this->readEntry('en', array('Currencies', $currency, static::INDEX_FRACTION_DIGITS));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoundingIncrement($currency)
+ {
+ return $this->readEntry('en', array('Currencies', $currency, static::INDEX_ROUNDING_INCREMENT));
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Gives access to currency-related ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface CurrencyBundleInterface extends ResourceBundleInterface
+{
+ /**
+ * Returns the symbol used for a currency.
+ *
+ * @param string $currency A currency code (e.g. "EUR").
+ * @param string $locale Optional. The locale to return the result in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The currency symbol or NULL if not found.
+ */
+ public function getCurrencySymbol($currency, $locale = null);
+
+ /**
+ * Returns the name of a currency.
+ *
+ * @param string $currency A currency code (e.g. "EUR").
+ * @param string $locale Optional. The locale to return the name in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The name of the currency or NULL if not found.
+ */
+ public function getCurrencyName($currency, $locale = null);
+
+ /**
+ * Returns the names of all known currencies.
+ *
+ * @param string $locale Optional. The locale to return the names in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string[] A list of currency names indexed by currency codes.
+ */
+ public function getCurrencyNames($locale = null);
+
+ /**
+ * Returns the number of digits after the comma of a currency.
+ *
+ * @param string $currency A currency code (e.g. "EUR").
+ *
+ * @return integer|null The number of digits after the comma or NULL if not found.
+ */
+ public function getFractionDigits($currency);
+
+ /**
+ * Returns the rounding increment of a currency.
+ *
+ * The rounding increment indicates to which number a currency is rounded.
+ * For example, 1230 rounded to the nearest 50 is 1250. 1.234 rounded to the
+ * nearest 0.65 is 1.3.
+ *
+ * @param string $currency A currency code (e.g. "EUR").
+ *
+ * @return float|integer|null The rounding increment or NULL if not found.
+ */
+ public function getRoundingIncrement($currency);
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Default implementation of {@link LanguageBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LanguageBundle extends AbstractBundle implements LanguageBundleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getLanguageName($lang, $region = null, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($languages = $this->readEntry($locale, array('Languages')))) {
+ return null;
+ }
+
+ // Some languages are translated together with their region,
+ // i.e. "en_GB" is translated as "British English"
+ if (null !== $region && isset($languages[$lang.'_'.$region])) {
+ return $languages[$lang.'_'.$region];
+ }
+
+ return $languages[$lang];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLanguageNames($locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($languages = $this->readEntry($locale, array('Languages')))) {
+ return array();
+ }
+
+ if ($languages instanceof \Traversable) {
+ $languages = iterator_to_array($languages);
+ }
+
+ return $languages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getScriptName($script, $lang = null, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ $data = $this->read($locale);
+
+ // Some languages are translated together with their script,
+ // e.g. "zh_Hans" is translated as "Simplified Chinese"
+ if (null !== $lang && isset($data['Languages'][$lang.'_'.$script])) {
+ $langName = $data['Languages'][$lang.'_'.$script];
+
+ // If the script is appended in braces, extract it, e.g. "zh_Hans"
+ // is translated as "Chinesisch (vereinfacht)" in locale "de"
+ if (strpos($langName, '(') !== false) {
+ list($langName, $scriptName) = preg_split('/[\s()]/', $langName, null, PREG_SPLIT_NO_EMPTY);
+
+ return $scriptName;
+ }
+ }
+
+ // "af" (Afrikaans) has no "Scripts" block
+ if (!isset($data['Scripts'][$script])) {
+ return null;
+ }
+
+ return $data['Scripts'][$script];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getScriptNames($locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($scripts = $this->readEntry($locale, array('Scripts')))) {
+ return array();
+ }
+
+ if ($scripts instanceof \Traversable) {
+ $scripts = iterator_to_array($scripts);
+ }
+
+ return $scripts;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Gives access to language-related ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface LanguageBundleInterface extends ResourceBundleInterface
+{
+ /**
+ * Returns the name of a language.
+ *
+ * @param string $lang A language code (e.g. "en").
+ * @param string|null $region Optional. A region code (e.g. "US").
+ * @param string $locale Optional. The locale to return the name in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The name of the language or NULL if not found.
+ */
+ public function getLanguageName($lang, $region = null, $locale = null);
+
+ /**
+ * Returns the names of all known languages.
+ *
+ * @param string $locale Optional. The locale to return the names in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string[] A list of language names indexed by language codes.
+ */
+ public function getLanguageNames($locale = null);
+
+ /**
+ * Returns the name of a script.
+ *
+ * @param string $script A script code (e.g. "Hans").
+ * @param string $lang Optional. A language code (e.g. "zh").
+ * @param string $locale Optional. The locale to return the name in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The name of the script or NULL if not found.
+ */
+ public function getScriptName($script, $lang = null, $locale = null);
+
+ /**
+ * Returns the names of all known scripts.
+ *
+ * @param string $locale Optional. The locale to return the names in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string[] A list of script names indexed by script codes.
+ */
+ public function getScriptNames($locale = null);
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Default implementation of {@link LocaleBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LocaleBundle extends AbstractBundle implements LocaleBundleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocaleName($ofLocale, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ return $this->readEntry($locale, array('Locales', $ofLocale));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocaleNames($locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($locales = $this->readEntry($locale, array('Locales')))) {
+ return array();
+ }
+
+ if ($locales instanceof \Traversable) {
+ $locales = iterator_to_array($locales);
+ }
+
+ return $locales;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Gives access to locale-related ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface LocaleBundleInterface extends ResourceBundleInterface
+{
+ /**
+ * Returns the name of a locale.
+ *
+ * @param string $ofLocale The locale to return the name of (e.g. "de_AT").
+ * @param string $locale Optional. The locale to return the name in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The name of the locale or NULL if not found.
+ */
+ public function getLocaleName($ofLocale, $locale = null);
+
+ /**
+ * Returns the names of all known locales.
+ *
+ * @param string $locale Optional. The locale to return the names in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string[] A list of locale names indexed by locale codes.
+ */
+ public function getLocaleNames($locale = null);
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+/**
+ * Base class for {@link BundleReaderInterface} implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractBundleReader implements BundleReaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocales($path)
+ {
+ $extension = '.' . $this->getFileExtension();
+ $locales = glob($path . '/*' . $extension);
+
+ // Remove file extension and sort
+ array_walk($locales, function (&$locale) use ($extension) { $locale = basename($locale, $extension); });
+ sort($locales);
+
+ return $locales;
+ }
+
+ /**
+ * Returns the extension of locale files in this bundle.
+ *
+ * @return string The file extension (without leading dot).
+ */
+ abstract protected function getFileExtension();
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\Exception\RuntimeException;
+use Symfony\Component\Intl\ResourceBundle\Util\ArrayAccessibleResourceBundle;
+
+/**
+ * Reads binary .res resource bundles.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BinaryBundleReader extends AbstractBundleReader implements BundleReaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function read($path, $locale)
+ {
+ // Point for future extension: Modify this class so that it works also
+ // if the \ResourceBundle class is not available.
+ $bundle = new \ResourceBundle($locale, $path);
+
+ if (null === $bundle) {
+ throw new RuntimeException(sprintf(
+ 'Could not load the resource bundle "%s/%s.res".',
+ $path,
+ $locale
+ ));
+ }
+
+ return new ArrayAccessibleResourceBundle($bundle);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getFileExtension()
+ {
+ return 'res';
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\ResourceBundle\Util\RingBuffer;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BufferedBundleReader implements BundleReaderInterface
+{
+ /**
+ * @var BundleReaderInterface
+ */
+ private $reader;
+
+ private $buffer;
+
+ /**
+ * Buffers a given reader.
+ *
+ * @param BundleReaderInterface $reader The reader to buffer.
+ * @param integer $bufferSize The number of entries to store
+ * in the buffer.
+ */
+ public function __construct(BundleReaderInterface $reader, $bufferSize)
+ {
+ $this->reader = $reader;
+ $this->buffer = new RingBuffer($bufferSize);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($path, $locale)
+ {
+ $hash = $path . '//' . $locale;
+
+ if (!isset($this->buffer[$hash])) {
+ $this->buffer[$hash] = $this->reader->read($path, $locale);
+ }
+
+ return $this->buffer[$hash];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocales($path)
+ {
+ return $this->reader->getLocales($path);
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+/**
+ * Reads resource bundle files.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface BundleReaderInterface
+{
+ /**
+ * Reads a resource bundle.
+ *
+ * @param string $path The path to the resource bundle.
+ * @param string $locale The locale to read.
+ *
+ * @return mixed Returns an array or {@link \ArrayAccess} instance for
+ * complex data, a scalar value otherwise.
+ */
+ public function read($path, $locale);
+
+ /**
+ * Reads the available locales of a resource bundle.
+ *
+ * @param string $path The path to the resource bundle.
+ *
+ * @return string[] A list of supported locale codes.
+ */
+ public function getLocales($path);
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\Exception\InvalidArgumentException;
+use Symfony\Component\Intl\Exception\RuntimeException;
+
+/**
+ * Reads .php resource bundles.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PhpBundleReader extends AbstractBundleReader implements BundleReaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function read($path, $locale)
+ {
+ if ('en' !== $locale) {
+ throw new InvalidArgumentException('Only the locale "en" is supported.');
+ }
+
+ $fileName = $path . '/' . $locale . '.php';
+
+ if (!file_exists($fileName)) {
+ throw new RuntimeException(sprintf(
+ 'The resource bundle "%s/%s.php" does not exist.',
+ $path,
+ $locale
+ ));
+ }
+
+ if (!is_file($fileName)) {
+ throw new RuntimeException(sprintf(
+ 'The resource bundle "%s/%s.php" is not a file.',
+ $path,
+ $locale
+ ));
+ }
+
+ return include $fileName;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getFileExtension()
+ {
+ return 'php';
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\ResourceBundle\Util\RecursiveArrayAccess;
+
+/**
+ * A structured reader wrapping an existing resource bundle reader.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see StructuredResourceBundleBundleReaderInterface
+ */
+class StructuredBundleReader implements StructuredBundleReaderInterface
+{
+ /**
+ * @var BundleReaderInterface
+ */
+ private $reader;
+
+ /**
+ * Creates an entry reader based on the given resource bundle reader.
+ *
+ * @param BundleReaderInterface $reader A resource bundle reader to use.
+ */
+ public function __construct(BundleReaderInterface $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($path, $locale)
+ {
+ return $this->reader->read($path, $locale);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLocales($path)
+ {
+ return $this->reader->getLocales($path);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function readEntry($path, $locale, array $indices, $fallback = true)
+ {
+ $data = $this->reader->read($path, $locale);
+
+ $entry = RecursiveArrayAccess::get($data, $indices);
+ $multivalued = is_array($entry) || $entry instanceof \Traversable;
+
+ if (!($fallback && (null === $entry || $multivalued))) {
+ return $entry;
+ }
+
+ if (null !== ($fallbackLocale = $this->getFallbackLocale($locale))) {
+ $parentEntry = $this->readEntry($path, $fallbackLocale, $indices, true);
+
+ if ($entry || $parentEntry) {
+ $multivalued = $multivalued || is_array($parentEntry) || $parentEntry instanceof \Traversable;
+
+ if ($multivalued) {
+ if ($entry instanceof \Traversable) {
+ $entry = iterator_to_array($entry);
+ }
+
+ if ($parentEntry instanceof \Traversable) {
+ $parentEntry = iterator_to_array($parentEntry);
+ }
+
+ $entry = array_merge(
+ $parentEntry ?: array(),
+ $entry ?: array()
+ );
+ } else {
+ $entry = null === $entry ? $parentEntry : $entry;
+ }
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * Returns the fallback locale for a given locale, if any
+ *
+ * @param string $locale The locale to find the fallback for.
+ *
+ * @return string|null The fallback locale, or null if no parent exists
+ */
+ private function getFallbackLocale($locale)
+ {
+ if (false === $pos = strrpos($locale, '_')) {
+ return null;
+ }
+
+ return substr($locale, 0, $pos);
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Reader;
+
+/**
+ * Reads individual entries of a resource file.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface StructuredBundleReaderInterface extends BundleReaderInterface
+{
+ /**
+ * Reads an entry from a resource bundle.
+ *
+ * An entry can be selected from the resource bundle by passing the path
+ * to that entry in the bundle. For example, if the bundle is structured
+ * like this:
+ *
+ * TopLevel
+ * NestedLevel
+ * Entry: Value
+ *
+ * Then the value can be read by calling:
+ *
+ * $reader->readEntry('...', 'en', array('TopLevel', 'NestedLevel', 'Entry'));
+ *
+ * @param string $path The path to the resource bundle.
+ * @param string $locale The locale to read.
+ * @param string[] $indices The indices to read from the bundle.
+ * @param Boolean $fallback Whether to merge the value with the value from
+ * the fallback locale (e.g. "en" for "en_GB").
+ * Only applicable if the result is multivalued
+ * (i.e. array or \ArrayAccess) or cannot be found
+ * in the requested locale.
+ *
+ * @return mixed Returns an array or {@link \ArrayAccess} instance for
+ * complex data, a scalar value for simple data and NULL
+ * if the given path could not be accessed.
+ */
+ public function readEntry($path, $locale, array $indices, $fallback = true);
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Default implementation of {@link RegionBundleInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RegionBundle extends AbstractBundle implements RegionBundleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getCountryName($country, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ return $this->readEntry($locale, array('Countries', $country));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCountryNames($locale = null)
+ {
+ if (null === $locale) {
+ $locale = \Locale::getDefault();
+ }
+
+ if (null === ($countries = $this->readEntry($locale, array('Countries')))) {
+ return array();
+ }
+
+ if ($countries instanceof \Traversable) {
+ $countries = iterator_to_array($countries);
+ }
+
+ return $countries;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Gives access to region-related ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface RegionBundleInterface extends ResourceBundleInterface
+{
+ /**
+ * Returns the name of a country.
+ *
+ * @param string $country A country code (e.g. "US").
+ * @param string $locale Optional. The locale to return the name in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string|null The name of the country or NULL if not found.
+ */
+ public function getCountryName($country, $locale = null);
+
+ /**
+ * Returns the names of all known countries.
+ *
+ * @param string $locale Optional. The locale to return the names in.
+ * Defaults to {@link \Locale::getDefault()}.
+ *
+ * @return string[] A list of country names indexed by country codes.
+ */
+ public function getCountryNames($locale = null);
+}
--- /dev/null
+<?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\Intl\ResourceBundle;
+
+/**
+ * Gives access to ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ResourceBundleInterface
+{
+ /**
+ * Returns the list of locales that this bundle supports.
+ *
+ * @return string[] A list of locale codes.
+ */
+ public function getLocales();
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer;
+
+use Symfony\Component\Intl\Exception\RuntimeException;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\TransformationRuleInterface;
+use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter;
+
+/**
+ * Compiles a number of resource bundles based on predefined compilation rules.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BundleTransformer
+{
+ /**
+ * @var TransformationRuleInterface[]
+ */
+ private $rules = array();
+
+ /**
+ * Adds a new compilation rule.
+ *
+ * @param TransformationRuleInterface $rule The compilation rule.
+ */
+ public function addRule(TransformationRuleInterface $rule)
+ {
+ $this->rules[] = $rule;
+ }
+
+ /**
+ * Runs the compilation with the given compilation context.
+ *
+ * @param CompilationContextInterface $context The context storing information
+ * needed to run the compilation.
+ *
+ * @throws RuntimeException If any of the files to be compiled by the loaded
+ * compilation rules does not exist.
+ */
+ public function compileBundles(CompilationContextInterface $context)
+ {
+ $filesystem = $context->getFilesystem();
+ $compiler = $context->getCompiler();
+
+ $filesystem->remove($context->getBinaryDir());
+ $filesystem->mkdir($context->getBinaryDir());
+
+ foreach ($this->rules as $rule) {
+ $filesystem->mkdir($context->getBinaryDir() . '/' . $rule->getBundleName());
+
+ $resources = (array) $rule->beforeCompile($context);
+
+ foreach ($resources as $resource) {
+ if (!file_exists($resource)) {
+ throw new RuntimeException(sprintf(
+ 'The file "%s" to be compiled by %s does not exist.',
+ $resource,
+ get_class($rule)
+ ));
+ }
+
+ $compiler->compile($resource, $context->getBinaryDir() . '/' . $rule->getBundleName());
+ }
+
+ $rule->afterCompile($context);
+ }
+ }
+
+ public function createStubs(StubbingContextInterface $context)
+ {
+ $filesystem = $context->getFilesystem();
+ $phpWriter = new PhpBundleWriter();
+
+ $filesystem->remove($context->getStubDir());
+ $filesystem->mkdir($context->getStubDir());
+
+ foreach ($this->rules as $rule) {
+ $filesystem->mkdir($context->getStubDir() . '/' . $rule->getBundleName());
+
+ $data = $rule->beforeCreateStub($context);
+
+ $phpWriter->write($context->getStubDir() . '/' . $rule->getBundleName(), 'en', $data);
+
+ $rule->afterCreateStub($context);
+ }
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer;
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface;
+
+/**
+ * Default implementation of {@link CompilationContextInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CompilationContext implements CompilationContextInterface
+{
+ /**
+ * @var string
+ */
+ private $sourceDir;
+
+ /**
+ * @var string
+ */
+ private $binaryDir;
+
+ /**
+ * @var FileSystem
+ */
+ private $filesystem;
+
+ /**
+ * @var BundleCompilerInterface
+ */
+ private $compiler;
+
+ /**
+ * @var string
+ */
+ private $icuVersion;
+
+ public function __construct($sourceDir, $binaryDir, Filesystem $filesystem, BundleCompilerInterface $compiler, $icuVersion)
+ {
+ $this->sourceDir = $sourceDir;
+ $this->binaryDir = $binaryDir;
+ $this->filesystem = $filesystem;
+ $this->compiler = $compiler;
+ $this->icuVersion = $icuVersion;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSourceDir()
+ {
+ return $this->sourceDir;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBinaryDir()
+ {
+ return $this->binaryDir;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilesystem()
+ {
+ return $this->filesystem;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCompiler()
+ {
+ return $this->compiler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIcuVersion()
+ {
+ return $this->icuVersion;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer;
+
+/**
+ * Stores contextual information for resource bundle compilation.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface CompilationContextInterface
+{
+ /**
+ * Returns the directory where the source versions of the resource bundles
+ * are stored.
+ *
+ * @return string An absolute path to a directory.
+ */
+ public function getSourceDir();
+
+ /**
+ * Returns the directory where the binary resource bundles are stored.
+ *
+ * @return string An absolute path to a directory.
+ */
+ public function getBinaryDir();
+
+ /**
+ * Returns a tool for manipulating the filesystem.
+ *
+ * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator.
+ */
+ public function getFilesystem();
+
+ /**
+ * Returns a resource bundle compiler.
+ *
+ * @return \Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface The loaded resource bundle compiler.
+ */
+ public function getCompiler();
+
+ /**
+ * Returns the ICU version of the bundles being converted.
+ *
+ * @return string The ICU version string.
+ */
+ public function getIcuVersion();
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer\Rule;
+
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\CurrencyBundle;
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface;
+use Symfony\Component\Intl\Util\IcuVersion;
+
+/**
+ * The rule for compiling the currency bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CurrencyBundleTransformationRule implements TransformationRuleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundleName()
+ {
+ return 'curr';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCompile(CompilationContextInterface $context)
+ {
+ // The currency data is contained in the locales and misc bundles
+ // in ICU <= 4.2
+ if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) {
+ return array(
+ $context->getSourceDir() . '/misc/supplementalData.txt',
+ $context->getSourceDir() . '/locales'
+ );
+ }
+
+ return $context->getSourceDir() . '/curr';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCompile(CompilationContextInterface $context)
+ {
+ // \ResourceBundle does not like locale names with uppercase chars, so rename
+ // the resource file
+ // See: http://bugs.php.net/bug.php?id=54025
+ $fileName = $context->getBinaryDir() . '/curr/supplementalData.res';
+ $fileNameLower = $context->getBinaryDir() . '/curr/supplementaldata.res';
+
+ $context->getFilesystem()->rename($fileName, $fileNameLower);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCreateStub(StubbingContextInterface $context)
+ {
+ $currencies = array();
+ $currencyBundle = Intl::getCurrencyBundle();
+
+ foreach ($currencyBundle->getCurrencyNames('en') as $code => $name) {
+ $currencies[$code] = array(
+ CurrencyBundle::INDEX_NAME => $name,
+ CurrencyBundle::INDEX_SYMBOL => $currencyBundle->getCurrencySymbol($code, 'en'),
+ CurrencyBundle::INDEX_FRACTION_DIGITS => $currencyBundle->getFractionDigits($code),
+ CurrencyBundle::INDEX_ROUNDING_INCREMENT => $currencyBundle->getRoundingIncrement($code),
+ );
+ }
+
+ return array(
+ 'Currencies' => $currencies,
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCreateStub(StubbingContextInterface $context)
+ {
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer\Rule;
+
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface;
+use Symfony\Component\Intl\Util\IcuVersion;
+
+/**
+ * The rule for compiling the language bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LanguageBundleTransformationRule implements TransformationRuleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundleName()
+ {
+ return 'lang';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCompile(CompilationContextInterface $context)
+ {
+ // The language data is contained in the locales bundle in ICU <= 4.2
+ if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) {
+ return $context->getSourceDir() . '/locales';
+ }
+
+ return $context->getSourceDir() . '/lang';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCompile(CompilationContextInterface $context)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCreateStub(StubbingContextInterface $context)
+ {
+ return array(
+ 'Languages' => Intl::getLanguageBundle()->getLanguageNames('en'),
+ 'Scripts' => Intl::getLanguageBundle()->getScriptNames('en'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCreateStub(StubbingContextInterface $context)
+ {
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer\Rule;
+
+use Symfony\Component\Intl\Exception\RuntimeException;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter;
+
+/**
+ * The rule for compiling the locale bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LocaleBundleTransformationRule implements TransformationRuleInterface
+{
+ /**
+ * @var \Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface
+ */
+ private $languageBundle;
+
+ /**
+ * @var \Symfony\Component\Intl\ResourceBundle\RegionBundleInterface
+ */
+ private $regionBundle;
+
+ public function __construct()
+ {
+ $this->languageBundle = Intl::getLanguageBundle();
+ $this->regionBundle = Intl::getRegionBundle();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundleName()
+ {
+ return 'locales';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCompile(CompilationContextInterface $context)
+ {
+ $tempDir = sys_get_temp_dir() . '/icu-data-locales';
+
+ $context->getFilesystem()->remove($tempDir);
+ $context->getFilesystem()->mkdir($tempDir);
+
+ $this->generateTextFiles($tempDir, $this->scanLocales($context));
+
+ return $tempDir;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCompile(CompilationContextInterface $context)
+ {
+ $context->getFilesystem()->remove(sys_get_temp_dir() . '/icu-data-locales');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCreateStub(StubbingContextInterface $context)
+ {
+ return array(
+ 'Locales' => Intl::getLocaleBundle()->getLocaleNames('en'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCreateStub(StubbingContextInterface $context)
+ {
+ }
+
+ private function scanLocales(CompilationContextInterface $context)
+ {
+ $tempDir = sys_get_temp_dir() . '/icu-data-locales-source';
+
+ $context->getFilesystem()->remove($tempDir);
+ $context->getFilesystem()->mkdir($tempDir);
+
+ // Temporarily generate the resource bundles
+ $context->getCompiler()->compile($context->getSourceDir() . '/locales', $tempDir);
+
+ // Discover the list of supported locales, which are the names of the resource
+ // bundles in the "locales" directory
+ $locales = glob($tempDir . '/*.res');
+
+ // Remove file extension and sort
+ array_walk($locales, function (&$locale) { $locale = basename($locale, '.res'); });
+ sort($locales);
+
+ // Delete unneeded locales
+ foreach ($locales as $key => $locale) {
+ // Delete all aliases from the list
+ // i.e., "az_AZ" is an alias for "az_Latn_AZ"
+ $content = file_get_contents($context->getSourceDir() . '/locales/' . $locale . '.txt');
+
+ // The key "%%ALIAS" is not accessible through the \ResourceBundle class,
+ // so look in the original .txt file instead
+ if (strpos($content, '%%ALIAS') !== false) {
+ unset($locales[$key]);
+ }
+
+ // Delete locales that have no content (i.e. only "Version" key)
+ $bundle = new \ResourceBundle($locale, $tempDir);
+
+ if (null === $bundle) {
+ throw new RuntimeException('The resource bundle for locale ' . $locale . ' could not be loaded from directory ' . $tempDir);
+ }
+
+ // There seems to be no other way for identifying all keys in this specific
+ // resource bundle
+ if (array_keys(iterator_to_array($bundle)) === array('Version')) {
+ unset($locales[$key]);
+ }
+ }
+
+ $context->getFilesystem()->remove($tempDir);
+
+ return $locales;
+ }
+
+ private function generateTextFiles($targetDirectory, array $locales)
+ {
+ $displayLocales = array_unique(array_merge(
+ $this->languageBundle->getLocales(),
+ $this->regionBundle->getLocales()
+ ));
+
+ $txtWriter = new TextBundleWriter();
+
+ // Generate a list of locale names in the language of each display locale
+ // Each locale name has the form: "Language (Script, Region, Variant1, ...)
+ // Script, Region and Variants are optional. If none of them is available,
+ // the braces are not printed.
+ foreach ($displayLocales as $displayLocale) {
+ // Don't include ICU's root resource bundle
+ if ('root' === $displayLocale) {
+ continue;
+ }
+
+ $names = array();
+
+ foreach ($locales as $locale) {
+ // Don't include ICU's root resource bundle
+ if ($locale === 'root') {
+ continue;
+ }
+
+ if (null !== ($name = $this->generateLocaleName($locale, $displayLocale))) {
+ $names[$locale] = $name;
+ }
+ }
+
+ // If no names could be generated for the current locale, skip it
+ if (0 === count($names)) {
+ continue;
+ }
+
+ $txtWriter->write($targetDirectory, $displayLocale, array('Locales' => $names));
+ }
+ }
+
+ private function generateLocaleName($locale, $displayLocale)
+ {
+ $name = null;
+
+ $lang = \Locale::getPrimaryLanguage($locale);
+ $script = \Locale::getScript($locale);
+ $region = \Locale::getRegion($locale);
+ $variants = \Locale::getAllVariants($locale);
+
+ // Currently the only available variant is POSIX, which we don't want
+ // to include in the list
+ if (count($variants) > 0) {
+ return null;
+ }
+
+ // Some languages are translated together with their region,
+ // i.e. "en_GB" is translated as "British English"
+ // we don't include these languages though because they mess up
+ // the name sorting
+ // $name = $this->langBundle->getLanguageName($displayLocale, $lang, $region);
+
+ // Some languages are simply not translated
+ // Example: "az" (Azerbaijani) has no translation in "af" (Afrikaans)
+ if (null === ($name = $this->languageBundle->getLanguageName($lang, null, $displayLocale))) {
+ return null;
+ }
+
+ // "as" (Assamese) has no "Variants" block
+ //if (!$langBundle->get('Variants')) {
+ // continue;
+ //}
+
+ $extras = array();
+
+ // Discover the name of the script part of the locale
+ // i.e. in zh_Hans_MO, "Hans" is the script
+ if ($script) {
+ // Some scripts are not translated into every language
+ if (null === ($scriptName = $this->languageBundle->getScriptName($script, $lang, $displayLocale))) {
+ return null;
+ }
+
+ $extras[] = $scriptName;
+ }
+
+ // Discover the name of the region part of the locale
+ // i.e. in de_AT, "AT" is the region
+ if ($region) {
+ // Some regions are not translated into every language
+ if (null === ($regionName = $this->regionBundle->getCountryName($region, $displayLocale))) {
+ return null;
+ }
+
+ $extras[] = $regionName;
+ }
+
+ if (count($extras) > 0) {
+ // Remove any existing extras
+ // For example, in German, zh_Hans is "Chinesisch (vereinfacht)".
+ // The latter is the script part which is already included in the
+ // extras and will be appended again with the other extras.
+ if (preg_match('/^(.+)\s+\([^\)]+\)$/', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ $name .= ' ('.implode(', ', $extras).')';
+ }
+
+ return $name;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer\Rule;
+
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface;
+use Symfony\Component\Intl\Util\IcuVersion;
+
+/**
+ * The rule for compiling the region bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RegionBundleTransformationRule implements TransformationRuleInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundleName()
+ {
+ return 'region';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCompile(CompilationContextInterface $context)
+ {
+ // The region data is contained in the locales bundle in ICU <= 4.2
+ if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) {
+ return $context->getSourceDir() . '/locales';
+ }
+
+ return $context->getSourceDir() . '/region';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCompile(CompilationContextInterface $context)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function beforeCreateStub(StubbingContextInterface $context)
+ {
+ return array(
+ 'Countries' => Intl::getRegionBundle()->getCountryNames('en'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function afterCreateStub(StubbingContextInterface $context)
+ {
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer\Rule;
+
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface;
+
+/**
+ * Contains instruction for compiling a resource bundle.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface TransformationRuleInterface
+{
+ /**
+ * Returns the name of the compiled resource bundle.
+ *
+ * @return string The name of the bundle.
+ */
+ public function getBundleName();
+
+ /**
+ * Runs instructions to be executed before compiling the sources of the
+ * resource bundle.
+ *
+ * @param CompilationContextInterface $context The contextual information of
+ * the compilation.
+ *
+ * @return string[] The source directories/files of the bundle.
+ */
+ public function beforeCompile(CompilationContextInterface $context);
+
+ /**
+ * Runs instructions to be executed after compiling the sources of the
+ * resource bundle.
+ *
+ * @param CompilationContextInterface $context The contextual information of
+ * the compilation.
+ */
+ public function afterCompile(CompilationContextInterface $context);
+
+ /**
+ * Runs instructions to be executed before creating the stub version of the
+ * resource bundle.
+ *
+ * @param StubbingContextInterface $context The contextual information of
+ * the compilation.
+ *
+ * @return mixed The data to include in the stub version.
+ */
+ public function beforeCreateStub(StubbingContextInterface $context);
+
+ /**
+ * Runs instructions to be executed after creating the stub version of the
+ * resource bundle.
+ *
+ * @param StubbingContextInterface $context The contextual information of
+ * the compilation.
+ */
+ public function afterCreateStub(StubbingContextInterface $context);
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer;
+
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class StubbingContext implements StubbingContextInterface
+{
+ /**
+ * @var string
+ */
+ private $binaryDir;
+
+ /**
+ * @var string
+ */
+ private $stubDir;
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ /**
+ * @var string
+ */
+ private $icuVersion;
+
+ public function __construct($binaryDir, $stubDir, Filesystem $filesystem, $icuVersion)
+ {
+ $this->binaryDir = $binaryDir;
+ $this->stubDir = $stubDir;
+ $this->filesystem = $filesystem;
+ $this->icuVersion = $icuVersion;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBinaryDir()
+ {
+ return $this->binaryDir;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStubDir()
+ {
+ return $this->stubDir;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilesystem()
+ {
+ return $this->filesystem;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIcuVersion()
+ {
+ return $this->icuVersion;
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Transformer;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface StubbingContextInterface
+{
+ /**
+ * Returns the directory where the binary resource bundles are stored.
+ *
+ * @return string An absolute path to a directory.
+ */
+ public function getBinaryDir();
+
+ /**
+ * Returns the directory where the stub resource bundles are stored.
+ *
+ * @return string An absolute path to a directory.
+ */
+ public function getStubDir();
+
+ /**
+ * Returns a tool for manipulating the filesystem.
+ *
+ * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator.
+ */
+ public function getFilesystem();
+
+ /**
+ * Returns the ICU version of the bundles being converted.
+ *
+ * @return string The ICU version string.
+ */
+ public function getIcuVersion();
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Util;
+
+use Symfony\Component\Intl\Exception\BadMethodCallException;
+
+/**
+ * Work-around for a bug in PHP's \ResourceBundle implementation.
+ *
+ * More information can be found on https://bugs.php.net/bug.php?id=64356.
+ * This class can be removed once that bug is fixed.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class ArrayAccessibleResourceBundle implements \ArrayAccess, \IteratorAggregate, \Countable
+{
+ private $bundleImpl;
+
+ public function __construct(\ResourceBundle $bundleImpl)
+ {
+ $this->bundleImpl = $bundleImpl;
+ }
+
+ public function get($offset, $fallback = null)
+ {
+ $value = $this->bundleImpl->get($offset, $fallback);
+
+ return $value instanceof \ResourceBundle ? new static($value) : $value;
+ }
+
+ public function offsetExists($offset)
+ {
+ return null !== $this->bundleImpl[$offset];
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->get($offset);
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ throw new BadMethodCallException('Resource bundles cannot be modified.');
+ }
+
+ public function offsetUnset($offset)
+ {
+ throw new BadMethodCallException('Resource bundles cannot be modified.');
+ }
+
+ public function getIterator()
+ {
+ return $this->bundleImpl;
+ }
+
+ public function count()
+ {
+ return $this->bundleImpl->count();
+ }
+
+ public function getErrorCode()
+ {
+ return $this->bundleImpl->getErrorCode();
+ }
+
+ public function getErrorMessage()
+ {
+ return $this->bundleImpl->getErrorMessage();
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Util;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RecursiveArrayAccess
+{
+ public static function get($array, array $indices)
+ {
+ foreach ($indices as $index) {
+ if (!$array instanceof \ArrayAccess && !is_array($array)) {
+ return null;
+ }
+
+ $array = $array[$index];
+ }
+
+ return $array;
+ }
+
+ private function __construct() {}
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Util;
+
+use Symfony\Component\Intl\Exception\OutOfBoundsException;
+
+/**
+ * Implements a ring buffer.
+ *
+ * A ring buffer is an array-like structure with a fixed size. If the buffer
+ * is full, the next written element overwrites the first bucket in the buffer,
+ * then the second and so on.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RingBuffer implements \ArrayAccess
+{
+ private $values = array();
+
+ private $indices = array();
+
+ private $cursor = 0;
+
+ private $size;
+
+ public function __construct($size)
+ {
+ $this->size = $size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetExists($key)
+ {
+ return isset($this->indices[$key]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetGet($key)
+ {
+ if (!isset($this->indices[$key])) {
+ throw new OutOfBoundsException(sprintf(
+ 'The index "%s" does not exist.',
+ $key
+ ));
+ }
+
+ return $this->values[$this->indices[$key]];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetSet($key, $value)
+ {
+ if (false !== ($keyToRemove = array_search($this->cursor, $this->indices))) {
+ unset($this->indices[$keyToRemove]);
+ }
+
+ $this->values[$this->cursor] = $value;
+ $this->indices[$key] = $this->cursor;
+
+ $this->cursor = ($this->cursor + 1) % $this->size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function offsetUnset($key)
+ {
+ if (isset($this->indices[$key])) {
+ $this->values[$this->indices[$key]] = null;
+ unset($this->indices[$key]);
+ }
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Writer;
+
+/**
+ * Writes resource bundle files.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface BundleWriterInterface
+{
+ /**
+ * Writes data to a resource bundle.
+ *
+ * @param string $path The path to the resource bundle.
+ * @param string $locale The locale to (over-)write.
+ * @param mixed $data The data to write.
+ */
+ public function write($path, $locale, $data);
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Writer;
+
+/**
+ * Writes .php resource bundles.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PhpBundleWriter implements BundleWriterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write($path, $locale, $data)
+ {
+ $template = <<<TEMPLATE
+<?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.
+ */
+
+return %s;
+
+TEMPLATE;
+
+ $data = var_export($data, true);
+ $data = preg_replace('/array \(/', 'array(', $data);
+ $data = preg_replace('/\n {1,10}array\(/', 'array(', $data);
+ $data = preg_replace('/ /', ' ', $data);
+ $data = sprintf($template, $data);
+
+ file_put_contents($path.'/'.$locale.'.php', $data);
+ }
+}
--- /dev/null
+<?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\Intl\ResourceBundle\Writer;
+
+/**
+ * Writes .txt resource bundles.
+ *
+ * The resulting files can be converted to binary .res files using the
+ * {@link \Symfony\Component\Intl\ResourceBundle\Transformer\BundleCompiler}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+class TextBundleWriter implements BundleWriterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function write($path, $locale, $data)
+ {
+ $file = fopen($path.'/'.$locale.'.txt', 'w');
+
+ $this->writeResourceBundle($file, $locale, $data);
+
+ fclose($file);
+ }
+
+ /**
+ * Writes a "resourceBundle" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param string $bundleName The name of the bundle.
+ * @param mixed $value The value of the node.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeResourceBundle($file, $bundleName, $value)
+ {
+ fwrite($file, $bundleName);
+
+ $this->writeTable($file, $value, 0);
+
+ fwrite($file, "\n");
+ }
+
+ /**
+ * Writes a "resource" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param mixed $value The value of the node.
+ * @param integer $indentation The number of levels to indent.
+ * @param Boolean $requireBraces Whether to require braces to be printed
+ * around the value.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeResource($file, $value, $indentation, $requireBraces = true)
+ {
+ if (is_int($value)) {
+ $this->writeInteger($file, $value);
+
+ return;
+ }
+
+ if (is_array($value)) {
+ if (count($value) === count(array_filter($value, 'is_int'))) {
+ $this->writeIntVector($file, $value, $indentation);
+
+ return;
+ }
+
+ $keys = array_keys($value);
+
+ if (count($keys) === count(array_filter($keys, 'is_int'))) {
+ $this->writeArray($file, $value, $indentation);
+
+ return;
+ }
+
+ $this->writeTable($file, $value, $indentation);
+
+ return;
+ }
+
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+
+ $this->writeString($file, (string) $value, $requireBraces);
+ }
+
+ /**
+ * Writes an "integer" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param integer $value The value of the node.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeInteger($file, $value)
+ {
+ fprintf($file, ':int{%d}', $value);
+ }
+
+ /**
+ * Writes an "intvector" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param array $value The value of the node.
+ * @param integer $indentation The number of levels to indent.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeIntVector($file, array $value, $indentation)
+ {
+ fwrite($file, ":intvector{\n");
+
+ foreach ($value as $int) {
+ fprintf($file, "%s%d,\n", str_repeat(' ', $indentation + 1), $int);
+ }
+
+ fprintf($file, "%s}", str_repeat(' ', $indentation));
+ }
+
+ /**
+ * Writes a "string" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param string $value The value of the node.
+ * @param Boolean $requireBraces Whether to require braces to be printed
+ * around the value.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeString($file, $value, $requireBraces = true)
+ {
+ if ($requireBraces) {
+ fprintf($file, '{"%s"}', $value);
+
+ return;
+ }
+
+ fprintf($file, '"%s"', $value);
+ }
+
+ /**
+ * Writes an "array" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param array $value The value of the node.
+ * @param integer $indentation The number of levels to indent.
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+ private function writeArray($file, array $value, $indentation)
+ {
+ fwrite($file, "{\n");
+
+ foreach ($value as $entry) {
+ fwrite($file, str_repeat(' ', $indentation + 1));
+
+ $this->writeResource($file, $entry, $indentation + 1, false);
+
+ fwrite($file, ",\n");
+ }
+
+ fprintf($file, '%s}', str_repeat(' ', $indentation));
+ }
+
+ /**
+ * Writes a "table" node.
+ *
+ * @param resource $file The file handle to write to.
+ * @param array $value The value of the node.
+ * @param integer $indentation The number of levels to indent.
+ */
+ private function writeTable($file, array $value, $indentation)
+ {
+ fwrite($file, "{\n");
+
+ foreach ($value as $key => $entry) {
+ fwrite($file, str_repeat(' ', $indentation + 1));
+ fwrite($file, $key);
+
+ $this->writeResource($file, $entry, $indentation + 1);
+
+ fwrite($file, "\n");
+ }
+
+ fprintf($file, '%s}', str_repeat(' ', $indentation));
+ }
+}
--- /dev/null
+<?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.
+ */
+
+$autoload = __DIR__ . '/../../vendor/autoload.php';
+
+if (!file_exists($autoload)) {
+ bailout('You should run "composer install --dev" in the component before running this script.');
+}
+
+require_once realpath($autoload);
--- /dev/null
+<?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.
+ */
+
+define('LINE_WIDTH', 75);
+
+define('LINE', str_repeat('-', LINE_WIDTH) . "\n");
+
+function bailout($message)
+{
+ echo wordwrap($message, LINE_WIDTH) . " Aborting.\n";
+
+ exit(1);
+}
+
+function strip_minor_versions($version)
+{
+ preg_match('/^(?P<version>[0-9]\.[0-9]|[0-9]{2,})/', $version, $matches);
+
+ return $matches['version'];
+}
+
+function centered($text)
+{
+ $padding = (int) ((LINE_WIDTH - strlen($text))/2);
+
+ return str_repeat(' ', $padding) . $text;
+}
+
+function cd($dir)
+{
+ if (false === chdir($dir)) {
+ bailout("Could not switch to directory $dir.");
+ }
+}
+
+function run($command)
+{
+ exec($command, $output, $status);
+
+ if (0 !== $status) {
+ $output = implode("\n", $output);
+ echo "Error while running:\n " . getcwd() . '$ ' . $command . "\nOutput:\n" . LINE . "$output\n" . LINE;
+
+ bailout("\"$command\" failed.");
+ }
+}
+
+function get_icu_version_from_genrb($genrb)
+{
+ exec($genrb . ' --version 2>&1', $output, $status);
+
+ if (0 !== $status) {
+ bailout($genrb . ' failed.');
+ }
+
+ if (!preg_match('/ICU version ([\d\.]+)/', implode('', $output), $matches)) {
+ return null;
+ }
+
+ return $matches[1];
+}
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Icu\IcuData;
+use Symfony\Component\Intl\Intl;
+
+require_once __DIR__ . '/common.php';
+require_once __DIR__ . '/autoload.php';
+
+if (1 !== $GLOBALS['argc']) {
+ bailout(<<<MESSAGE
+Usage: php copy-stubs-to-component.php
+
+Copies stub files created with create-stubs.php to the Icu component.
+
+For running this script, the intl extension must be loaded and all vendors
+must have been installed through composer:
+
+ composer install --dev
+
+MESSAGE
+ );
+}
+
+echo LINE;
+echo centered("ICU Resource Bundle Stub Update") . "\n";
+echo LINE;
+
+if (!class_exists('\Symfony\Component\Icu\IcuData')) {
+ bailout('You must run "composer update --dev" before running this script.');
+}
+
+$stubBranch = '1.0.x';
+
+if (!IcuData::isStubbed()) {
+ bailout("Please switch to the Icu component branch $stubBranch.");
+}
+
+$filesystem = new Filesystem();
+
+$sourceDir = sys_get_temp_dir() . '/icu-stubs';
+$targetDir = IcuData::getResourceDirectory();
+
+if (!$filesystem->exists($sourceDir)) {
+ bailout("The directory $sourceDir does not exist. Please run create-stubs.php first.");
+}
+
+$filesystem->remove($targetDir);
+
+echo "Copying files from $sourceDir to $targetDir...\n";
+
+$filesystem->mirror($sourceDir, $targetDir);
+
+echo "Done.\n";
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Icu\IcuData;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Transformer\BundleTransformer;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\CurrencyBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\LanguageBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\LocaleBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\RegionBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext;
+
+require_once __DIR__ . '/common.php';
+require_once __DIR__ . '/autoload.php';
+
+if (1 !== $GLOBALS['argc']) {
+ bailout(<<<MESSAGE
+Usage: php create-stubs.php
+
+Creates resource bundle stubs from the resource bundles in the Icu component.
+
+For running this script, the intl extension must be loaded and all vendors
+must have been installed through composer:
+
+ composer install --dev
+
+MESSAGE
+ );
+}
+
+echo LINE;
+echo centered("ICU Resource Bundle Stub Creation") . "\n";
+echo LINE;
+
+if (!Intl::isExtensionLoaded()) {
+ bailout('The intl extension for PHP is not installed.');
+}
+
+if (!class_exists('\Symfony\Component\Icu\IcuData')) {
+ bailout('You must run "composer update --dev" before running this script.');
+}
+
+$stubBranch = '1.0.x';
+
+if (IcuData::isStubbed()) {
+ bailout("Please switch to a branch of the Icu component that contains .res files (anything but $stubBranch).");
+}
+
+$shortIcuVersionInPhp = strip_minor_versions(Intl::getIcuVersion());
+$shortIcuVersionInIntlComponent = strip_minor_versions(Intl::getIcuStubVersion());
+$shortIcuVersionInIcuComponent = strip_minor_versions(IcuData::getVersion());
+
+if ($shortIcuVersionInPhp !== $shortIcuVersionInIcuComponent) {
+ bailout("The ICU version of the component ($shortIcuVersionInIcuComponent) does not match the ICU version in the intl extension ($shortIcuVersionInPhp).");
+}
+
+if ($shortIcuVersionInIntlComponent !== $shortIcuVersionInIcuComponent) {
+ bailout("The ICU version of the component ($shortIcuVersionInIcuComponent) does not match the ICU version of the stub classes in the Intl component ($shortIcuVersionInIntlComponent).");
+}
+
+echo wordwrap("Make sure that you don't have any ICU development files " .
+ "installed. If the build fails, try to run:\n", LINE_WIDTH);
+
+echo "\n sudo apt-get remove libicu-dev\n\n";
+
+$icuVersionInIcuComponent = IcuData::getVersion();
+
+echo "Compiling stubs for ICU version $icuVersionInIcuComponent.\n";
+
+echo "Preparing stub creation...\n";
+
+$targetDir = sys_get_temp_dir() . '/icu-stubs';
+
+$context = new StubbingContext(
+ IcuData::getResourceDirectory(),
+ $targetDir,
+ new Filesystem(),
+ $icuVersionInIcuComponent
+);
+
+$transformer = new BundleTransformer();
+$transformer->addRule(new LanguageBundleTransformationRule());
+$transformer->addRule(new RegionBundleTransformationRule());
+$transformer->addRule(new CurrencyBundleTransformationRule());
+$transformer->addRule(new LocaleBundleTransformationRule());
+
+echo "Starting stub creation...\n";
+
+$transformer->createStubs($context);
+
+echo "Wrote stubs to $targetDir.\n";
+
+$versionFile = $context->getStubDir() . '/version.txt';
+
+file_put_contents($versionFile, "$icuVersionInIcuComponent\n");
+
+echo "Wrote $versionFile.\n";
+
+echo "Done.\n";
+
+echo wordwrap("Please change the Icu component to branch $stubBranch now and run:\n", LINE_WIDTH);
+
+echo "\n php copy-stubs-to-component.php\n";
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Intl\Intl;
+
+require_once __DIR__ . '/common.php';
+require_once __DIR__ . '/autoload.php';
+
+echo "ICU version: ";
+echo Intl::getIcuVersion() . "\n";
--- /dev/null
+; ICU data source URLs
+; We use always the latest release of a major version.
+4.0 = http://source.icu-project.org/repos/icu/icu/tags/release-4-0-1/source
+4.2 = http://source.icu-project.org/repos/icu/icu/tags/release-4-2-1/source
+4.4 = http://source.icu-project.org/repos/icu/icu/tags/release-4-4-2/source
+4.6 = http://source.icu-project.org/repos/icu/icu/tags/release-4-6-1/source
+4.8 = http://source.icu-project.org/repos/icu/icu/tags/release-4-8-1-1/source
+49 = http://source.icu-project.org/repos/icu/icu/tags/release-49-1-2/source
+50 = http://source.icu-project.org/repos/icu/icu/tags/release-50-1-2/source
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Intl\Intl;
+
+require_once __DIR__ . '/common.php';
+require_once __DIR__ . '/autoload.php';
+
+if (1 !== $GLOBALS['argc']) {
+ bailout(<<<MESSAGE
+Usage: php test-compat.php
+
+Tests the compatibility of the current ICU version (bundled in ext/intl) with
+different versions of symfony/icu.
+
+For running this script, the intl extension must be loaded and all vendors
+must have been installed through composer:
+
+ composer install --dev
+
+MESSAGE
+ );
+}
+
+echo LINE;
+echo centered("ICU Compatibility Test") . "\n";
+echo LINE;
+
+echo "Your ICU version: " . Intl::getIcuVersion() . "\n";
+
+echo "Compatibility with symfony/icu:\n";
+
+$branches = array(
+ '1.1.x',
+ '1.2.x',
+);
+
+cd(__DIR__ . '/../../vendor/symfony/icu/Symfony/Component/Icu');
+
+foreach ($branches as $branch) {
+ run('git checkout ' . $branch . ' 2>&1');
+
+ exec('php ' . __DIR__ . '/util/test-compat-helper.php > /dev/null 2> /dev/null', $output, $status);
+
+ echo "$branch: " . (0 === $status ? "YES" : "NO") . "\n";
+}
+
+echo "Done.\n";
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Icu\IcuData;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler;
+use Symfony\Component\Intl\ResourceBundle\Transformer\BundleTransformer;
+use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\CurrencyBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\LanguageBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\LocaleBundleTransformationRule;
+use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\RegionBundleTransformationRule;
+use Symfony\Component\Intl\Util\SvnRepository;
+use Symfony\Component\Filesystem\Filesystem;
+
+require_once __DIR__ . '/common.php';
+require_once __DIR__ . '/autoload.php';
+
+if ($GLOBALS['argc'] > 3 || 2 === $GLOBALS['argc'] && '-h' === $GLOBALS['argv'][1]) {
+ bailout(<<<MESSAGE
+Usage: php update-icu-component.php <path/to/icu/source> <path/to/icu/build>
+
+Updates the ICU data for Symfony2 to the latest version of the ICU version
+included in the intl extension. For example, if your intl extension includes
+ICU 4.8, the script will download the latest data available for ICU 4.8.
+
+If you downloaded the SVN repository before, you can pass the path to the
+repository source in the first optional argument.
+
+If you also built the repository before, you can pass the directory where that
+build is stored in the second parameter. The build directory needs to contain
+the subdirectories bin/ and lib/.
+
+For running this script, the intl extension must be loaded and all vendors
+must have been installed through composer:
+
+ composer install --dev
+
+MESSAGE
+ );
+}
+
+echo LINE;
+echo centered("ICU Resource Bundle Compilation") . "\n";
+echo LINE;
+
+if (!Intl::isExtensionLoaded()) {
+ bailout('The intl extension for PHP is not installed.');
+}
+
+if (!class_exists('\Symfony\Component\Icu\IcuData')) {
+ bailout('You must run "composer update --dev" before running this script.');
+}
+
+$filesystem = new Filesystem();
+
+$icuVersionInPhp = Intl::getIcuVersion();
+
+echo "Found intl extension with ICU version $icuVersionInPhp.\n";
+
+$shortIcuVersion = strip_minor_versions($icuVersionInPhp);
+$urls = parse_ini_file(__DIR__ . '/icu.ini');
+
+if (!isset($urls[$shortIcuVersion])) {
+ bailout('The version ' . $shortIcuVersion . ' is not available in the icu.ini file.');
+}
+
+echo "icu.ini parsed. Available versions:\n";
+
+foreach ($urls as $urlVersion => $url) {
+ echo " $urlVersion\n";
+}
+
+if ($GLOBALS['argc'] >= 2) {
+ $sourceDir = $GLOBALS['argv'][1];
+ $svn = new SvnRepository($sourceDir);
+
+ echo "Using existing SVN repository at {$sourceDir}.\n";
+} else {
+ echo "Starting SVN checkout for version $shortIcuVersion. This may take a while...\n";
+
+ $sourceDir = sys_get_temp_dir() . '/icu-data/' . $shortIcuVersion . '/source';
+ $svn = SvnRepository::download($urls[$shortIcuVersion], $sourceDir);
+
+ echo "SVN checkout to {$sourceDir} complete.\n";
+}
+
+if ($GLOBALS['argc'] >= 3) {
+ $buildDir = $GLOBALS['argv'][2];
+} else {
+ // Always build genrb so that we can determine the ICU version of the
+ // download by running genrb --version
+ echo "Building genrb.\n";
+
+ cd($sourceDir);
+
+ echo "Running configure...\n";
+
+ $buildDir = sys_get_temp_dir() . '/icu-data/' . $shortIcuVersion . '/build';
+
+ $filesystem->remove($buildDir);
+ $filesystem->mkdir($buildDir);
+
+ run('./configure --prefix=' . $buildDir . ' 2>&1');
+
+ echo "Running make...\n";
+
+ // If the directory "lib" does not exist in the download, create it or we
+ // will run into problems when building libicuuc.so.
+ $filesystem->mkdir($sourceDir . '/lib');
+
+ // If the directory "bin" does not exist in the download, create it or we
+ // will run into problems when building genrb.
+ $filesystem->mkdir($sourceDir . '/bin');
+
+ echo "[1/5] libicudata.so...";
+
+ cd($sourceDir . '/stubdata');
+ run('make 2>&1 && make install 2>&1');
+
+ echo " ok.\n";
+
+ echo "[2/5] libicuuc.so...";
+
+ cd($sourceDir . '/common');
+ run('make 2>&1 && make install 2>&1');
+
+ echo " ok.\n";
+
+ echo "[3/5] libicui18n.so...";
+
+ cd($sourceDir . '/i18n');
+ run('make 2>&1 && make install 2>&1');
+
+ echo " ok.\n";
+
+ echo "[4/5] libicutu.so...";
+
+ cd($sourceDir . '/tools/toolutil');
+ run('make 2>&1 && make install 2>&1');
+
+ echo " ok.\n";
+
+ echo "[5/5] genrb...";
+
+ cd($sourceDir . '/tools/genrb');
+ run('make 2>&1 && make install 2>&1');
+
+ echo " ok.\n";
+}
+
+$genrb = $buildDir . '/bin/genrb';
+$genrbEnv = 'LD_LIBRARY_PATH=' . $buildDir . '/lib ';
+
+echo "Using $genrb.\n";
+
+$icuVersionInDownload = get_icu_version_from_genrb($genrbEnv . ' ' . $genrb);
+
+echo "Preparing resource bundle compilation (version $icuVersionInDownload)...\n";
+
+$context = new CompilationContext(
+ $sourceDir . '/data',
+ IcuData::getResourceDirectory(),
+ $filesystem,
+ new BundleCompiler($genrb, $genrbEnv),
+ $icuVersionInDownload
+);
+
+$transformer = new BundleTransformer();
+$transformer->addRule(new LanguageBundleTransformationRule());
+$transformer->addRule(new RegionBundleTransformationRule());
+$transformer->addRule(new CurrencyBundleTransformationRule());
+$transformer->addRule(new LocaleBundleTransformationRule());
+
+echo "Starting resource bundle compilation. This may take a while...\n";
+
+$transformer->compileBundles($context);
+
+echo "Resource bundle compilation complete.\n";
+
+$svnInfo = <<<SVN_INFO
+SVN information
+===============
+
+URL: {$svn->getUrl()}
+Revision: {$svn->getLastCommit()->getRevision()}
+Author: {$svn->getLastCommit()->getAuthor()}
+Date: {$svn->getLastCommit()->getDate()}
+
+SVN_INFO;
+
+$svnInfoFile = $context->getBinaryDir() . '/svn-info.txt';
+
+file_put_contents($svnInfoFile, $svnInfo);
+
+echo "Wrote $svnInfoFile.\n";
+
+$versionFile = $context->getBinaryDir() . '/version.txt';
+
+file_put_contents($versionFile, "$icuVersionInDownload\n");
+
+echo "Wrote $versionFile.\n";
+
+echo "Done.\n";
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Icu\IcuData;
+use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
+
+require_once __DIR__ . '/../common.php';
+require_once __DIR__ . '/../autoload.php';
+
+$reader = new BinaryBundleReader();
+
+$reader->read(IcuData::getResourceDirectory() . '/curr', 'en');
+$reader->read(IcuData::getResourceDirectory() . '/lang', 'en');
+$reader->read(IcuData::getResourceDirectory() . '/locales', 'en');
+$reader->read(IcuData::getResourceDirectory() . '/region', 'en');
--- /dev/null
+<?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.
+ */
+
+/**
+ * Stub implementation for the Collator class of the intl extension
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see \Symfony\Component\Intl\Collator\StubCollator
+ */
+class Collator extends \Symfony\Component\Intl\Collator\Collator
+{
+}
--- /dev/null
+<?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.
+ */
+
+/**
+ * Stub implementation for the IntlDateFormatter class of the intl extension
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see \Symfony\Component\Intl\DateFormatter\IntlDateFormatter
+ */
+class IntlDateFormatter extends \Symfony\Component\Intl\DateFormatter\IntlDateFormatter
+{
+}
--- /dev/null
+<?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.
+ */
+
+/**
+ * Stub implementation for the Locale class of the intl extension
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see \Symfony\Component\Intl\Locale\Locale
+ */
+class Locale extends \Symfony\Component\Intl\Locale\Locale
+{
+}
--- /dev/null
+<?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.
+ */
+
+/**
+ * Stub implementation for the NumberFormatter class of the intl extension
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see \Symfony\Component\Intl\NumberFormatter\NumberFormatter
+ */
+class NumberFormatter extends \Symfony\Component\Intl\NumberFormatter\NumberFormatter
+{
+}
--- /dev/null
+<?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.
+ */
+
+use Symfony\Component\Intl\Globals\IntlGlobals;
+
+if (!function_exists('intl_is_failure')) {
+
+ /**
+ * Stub implementation for the {@link intl_is_failure()} function of the intl
+ * extension.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @param integer $errorCode The error code returned by intl_get_error_code().
+ *
+ * @return Boolean Whether the error code indicates an error.
+ *
+ * @see \Symfony\Component\Intl\Globals\StubIntlGlobals::isFailure
+ */
+ function intl_is_failure($errorCode)
+ {
+ return IntlGlobals::isFailure($errorCode);
+ }
+
+ /**
+ * Stub implementation for the {@link intl_get_error_code()} function of the
+ * intl extension.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @return Boolean The error code of the last intl function call or
+ * IntlGlobals::U_ZERO_ERROR if no error occurred.
+ *
+ * @see \Symfony\Component\Intl\Globals\StubIntlGlobals::getErrorCode
+ */
+ function intl_get_error_code()
+ {
+ return IntlGlobals::getErrorCode();
+ }
+
+ /**
+ * Stub implementation for the {@link intl_get_error_code()} function of the
+ * intl extension.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @return Boolean The error message of the last intl function call or
+ * "U_ZERO_ERROR" if no error occurred.
+ *
+ * @see \Symfony\Component\Intl\Globals\StubIntlGlobals::getErrorMessage
+ */
+ function intl_get_error_message()
+ {
+ return IntlGlobals::getErrorMessage();
+ }
+
+ /**
+ * Stub implementation for the {@link intl_error_name()} function of the intl
+ * extension.
+ *
+ * @param integer $errorCode The error code.
+ *
+ * @return string The name of the error code constant.
+ *
+ * @see \Symfony\Component\Intl\Globals\StubIntlGlobals::getErrorName
+ */
+ function intl_error_name($errorCode)
+ {
+ return IntlGlobals::getErrorName($errorCode);
+ }
+
+}
--- /dev/null
+<?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\Intl\Tests\Collator;
+
+use Symfony\Component\Intl\Collator\Collator;
+use Symfony\Component\Intl\Locale;
+
+/**
+ * Test case for Collator implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractCollatorTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider asortProvider
+ */
+ public function testAsort($array, $sortFlag, $expected)
+ {
+ $collator = $this->getCollator('en');
+ $collator->asort($array, $sortFlag);
+ $this->assertSame($expected, $array);
+ }
+
+ public function asortProvider()
+ {
+ return array(
+ /* array, sortFlag, expected */
+ array(
+ array('a', 'b', 'c'),
+ Collator::SORT_REGULAR,
+ array('a', 'b', 'c'),
+ ),
+ array(
+ array('c', 'b', 'a'),
+ Collator::SORT_REGULAR,
+ array(2 => 'a', 1 => 'b', 0 => 'c'),
+ ),
+ array(
+ array('b', 'c', 'a'),
+ Collator::SORT_REGULAR,
+ array(2 => 'a', 0 => 'b', 1 => 'c'),
+ ),
+ );
+ }
+
+ /**
+ * @param string $locale
+ *
+ * @return \Collator
+ */
+ abstract protected function getCollator($locale);
+}
--- /dev/null
+<?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\Intl\Tests\Collator;
+
+use Symfony\Component\Intl\Collator\Collator;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+
+class CollatorTest extends AbstractCollatorTest
+{
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testConstructorWithUnsupportedLocale()
+ {
+ new Collator('pt_BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testCompare()
+ {
+ $collator = $this->getCollator('en');
+ $collator->compare('a', 'b');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetAttribute()
+ {
+ $collator = $this->getCollator('en');
+ $collator->getAttribute(Collator::NUMERIC_COLLATION);
+ }
+
+ public function testGetErrorCode()
+ {
+ $collator = $this->getCollator('en');
+ $this->assertEquals(IntlGlobals::U_ZERO_ERROR, $collator->getErrorCode());
+ }
+
+ public function testGetErrorMessage()
+ {
+ $collator = $this->getCollator('en');
+ $this->assertEquals('U_ZERO_ERROR', $collator->getErrorMessage());
+ }
+
+ public function testGetLocale()
+ {
+ $collator = $this->getCollator('en');
+ $this->assertEquals('en', $collator->getLocale());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetSortKey()
+ {
+ $collator = $this->getCollator('en');
+ $collator->getSortKey('Hello');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetStrength()
+ {
+ $collator = $this->getCollator('en');
+ $collator->getStrength();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetAttribute()
+ {
+ $collator = $this->getCollator('en');
+ $collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetStrength()
+ {
+ $collator = $this->getCollator('en');
+ $collator->setStrength(Collator::PRIMARY);
+ }
+
+ public function testStaticCreate()
+ {
+ $collator = Collator::create('en');
+ $this->assertInstanceOf('\Symfony\Component\Intl\Collator\Collator', $collator);
+ }
+
+ protected function getCollator($locale)
+ {
+ return new Collator($locale);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Collator\Verification;
+
+use Symfony\Component\Intl\Locale;
+use Symfony\Component\Intl\Tests\Collator\AbstractCollatorTest;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Verifies that {@link AbstractCollatorTest} matches the behavior of the
+ * {@link \Collator} class in a specific version of ICU.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CollatorTest extends AbstractCollatorTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ protected function getCollator($locale)
+ {
+ return new \Collator($locale);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\DateFormatter;
+
+use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\Intl;
+
+/**
+ * Test case for IntlDateFormatter implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractIntlDateFormatterTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * When a time zone is not specified, it uses the system default however it returns null in the getter method
+ * @covers Symfony\Component\Intl\DateFormatter\IntlDateFormatter::getTimeZoneId
+ * @see StubIntlDateFormatterTest::testDefaultTimeZoneIntl()
+ */
+ public function testConstructorDefaultTimeZone()
+ {
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
+
+ // In PHP 5.5 default timezone depends on `date_default_timezone_get()` method
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $this->assertEquals(date_default_timezone_get(), $formatter->getTimeZoneId());
+ } else {
+ $this->assertNull($formatter->getTimeZoneId());
+ }
+ }
+
+ /**
+ * @dataProvider formatProvider
+ */
+ public function testFormat($pattern, $timestamp, $expected)
+ {
+ $errorCode = IntlGlobals::U_ZERO_ERROR;
+ $errorMessage = 'U_ZERO_ERROR';
+
+ $formatter = $this->getDefaultDateFormatter($pattern);
+ $this->assertSame($expected, $formatter->format($timestamp));
+ $this->assertIsIntlSuccess($formatter, $errorMessage, $errorCode);
+ }
+
+ public function formatProvider()
+ {
+ $formatData = array(
+ /* general */
+ array('y-M-d', 0, '1970-1-1'),
+ array("EEE, MMM d, ''yy", 0, "Thu, Jan 1, '70"),
+ array('h:mm a', 0, '12:00 AM'),
+ array('yyyyy.MMMM.dd hh:mm aaa', 0, '01970.January.01 12:00 AM'),
+
+ /* escaping */
+ array("'M'", 0, 'M'),
+ array("'yy'", 0, 'yy'),
+ array("'''yy'", 0, "'yy"),
+ array("''y", 0, "'1970"),
+ array("''yy", 0, "'70"),
+ array("H 'o'' clock'", 0, "0 o' clock"),
+
+ /* month */
+ array('M', 0, '1'),
+ array('MM', 0, '01'),
+ array('MMM', 0, 'Jan'),
+ array('MMMM', 0, 'January'),
+ array('MMMMM', 0, 'J'),
+ array('MMMMMM', 0, '000001'),
+
+ array('L', 0, '1'),
+ array('LL', 0, '01'),
+ array('LLL', 0, 'Jan'),
+ array('LLLL', 0, 'January'),
+ array('LLLLL', 0, 'J'),
+ array('LLLLLL', 0, '000001'),
+
+ /* year */
+ array('y', 0, '1970'),
+ array('yy', 0, '70'),
+ array('yyy', 0, '1970'),
+ array('yyyy', 0, '1970'),
+ array('yyyyy', 0, '01970'),
+ array('yyyyyy', 0, '001970'),
+
+ /* day */
+ array('d', 0, '1'),
+ array('dd', 0, '01'),
+ array('ddd', 0, '001'),
+
+ /* quarter */
+ array('Q', 0, '1'),
+ array('QQ', 0, '01'),
+ array('QQQ', 0, 'Q1'),
+ array('QQQQ', 0, '1st quarter'),
+ array('QQQQQ', 0, '1st quarter'),
+
+ array('q', 0, '1'),
+ array('qq', 0, '01'),
+ array('qqq', 0, 'Q1'),
+ array('qqqq', 0, '1st quarter'),
+ array('qqqqq', 0, '1st quarter'),
+
+ // 4 months
+ array('Q', 7776000, '2'),
+ array('QQ', 7776000, '02'),
+ array('QQQ', 7776000, 'Q2'),
+ array('QQQQ', 7776000, '2nd quarter'),
+
+ // 7 months
+ array('QQQQ', 15638400, '3rd quarter'),
+
+ // 10 months
+ array('QQQQ', 23587200, '4th quarter'),
+
+ /* 12-hour (1-12) */
+ array('h', 0, '12'),
+ array('hh', 0, '12'),
+ array('hhh', 0, '012'),
+
+ array('h', 1, '12'),
+ array('h', 3600, '1'),
+ array('h', 43200, '12'), // 12 hours
+
+ /* day of year */
+ array('D', 0, '1'),
+ array('D', 86400, '2'), // 1 day
+ array('D', 31536000, '1'), // 1 year
+ array('D', 31622400, '2'), // 1 year + 1 day
+
+ /* day of week */
+ array('E', 0, 'Thu'),
+ array('EE', 0, 'Thu'),
+ array('EEE', 0, 'Thu'),
+ array('EEEE', 0, 'Thursday'),
+ array('EEEEE', 0, 'T'),
+ array('EEEEEE', 0, 'Thu'),
+
+ array('E', 1296540000, 'Tue'), // 2011-02-01
+ array('E', 1296950400, 'Sun'), // 2011-02-06
+
+ /* am/pm marker */
+ array('a', 0, 'AM'),
+ array('aa', 0, 'AM'),
+ array('aaa', 0, 'AM'),
+ array('aaaa', 0, 'AM'),
+
+ // 12 hours
+ array('a', 43200, 'PM'),
+ array('aa', 43200, 'PM'),
+ array('aaa', 43200, 'PM'),
+ array('aaaa', 43200, 'PM'),
+
+ /* 24-hour (0-23) */
+ array('H', 0, '0'),
+ array('HH', 0, '00'),
+ array('HHH', 0, '000'),
+
+ array('H', 1, '0'),
+ array('H', 3600, '1'),
+ array('H', 43200, '12'),
+ array('H', 46800, '13'),
+
+ /* 24-hour (1-24) */
+ array('k', 0, '24'),
+ array('kk', 0, '24'),
+ array('kkk', 0, '024'),
+
+ array('k', 1, '24'),
+ array('k', 3600, '1'),
+ array('k', 43200, '12'),
+ array('k', 46800, '13'),
+
+ /* 12-hour (0-11) */
+ array('K', 0, '0'),
+ array('KK', 0, '00'),
+ array('KKK', 0, '000'),
+
+ array('K', 1, '0'),
+ array('K', 3600, '1'),
+ array('K', 43200, '0'), // 12 hours
+
+ /* minute */
+ array('m', 0, '0'),
+ array('mm', 0, '00'),
+ array('mmm', 0, '000'),
+
+ array('m', 1, '0'),
+ array('m', 60, '1'),
+ array('m', 120, '2'),
+ array('m', 180, '3'),
+ array('m', 3600, '0'),
+ array('m', 3660, '1'),
+ array('m', 43200, '0'), // 12 hours
+
+ /* second */
+ array('s', 0, '0'),
+ array('ss', 0, '00'),
+ array('sss', 0, '000'),
+
+ array('s', 1, '1'),
+ array('s', 2, '2'),
+ array('s', 5, '5'),
+ array('s', 30, '30'),
+ array('s', 59, '59'),
+ array('s', 60, '0'),
+ array('s', 120, '0'),
+ array('s', 180, '0'),
+ array('s', 3600, '0'),
+ array('s', 3601, '1'),
+ array('s', 3630, '30'),
+ array('s', 43200, '0'), // 12 hours
+
+ // general
+ array("yyyy.MM.dd 'at' HH:mm:ss zzz", 0, '1970.01.01 at 00:00:00 GMT'),
+ array('K:mm a, z', 0, '0:00 AM, GMT'),
+
+ // timezone
+ array('z', 0, 'GMT'),
+ array('zz', 0, 'GMT'),
+ array('zzz', 0, 'GMT'),
+ array('zzzz', 0, 'GMT'),
+ array('zzzzz', 0, 'GMT'),
+ );
+
+ // As of PHP 5.3.4, IntlDateFormatter::format() accepts DateTime instances
+ if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
+ $dateTime = new \DateTime('@0');
+
+ /* general, DateTime */
+ $formatData[] = array('y-M-d', $dateTime, '1970-1-1');
+ $formatData[] = array("EEE, MMM d, ''yy", $dateTime, "Thu, Jan 1, '70");
+ $formatData[] = array('h:mm a', $dateTime, '12:00 AM');
+ $formatData[] = array('yyyyy.MMMM.dd hh:mm aaa', $dateTime, '01970.January.01 12:00 AM');
+
+ $formatData[] = array("yyyy.MM.dd 'at' HH:mm:ss zzz", $dateTime, '1970.01.01 at 00:00:00 GMT');
+ $formatData[] = array('K:mm a, z', $dateTime, '0:00 AM, GMT');
+ }
+
+ return $formatData;
+ }
+
+ /**
+ * @dataProvider formatErrorProvider
+ */
+ public function testFormatIllegalArgumentError($pattern, $timestamp, $errorMessage)
+ {
+ $errorCode = IntlGlobals::U_ILLEGAL_ARGUMENT_ERROR;
+
+ $formatter = $this->getDefaultDateFormatter($pattern);
+ $this->assertFalse($formatter->format($timestamp));
+ $this->assertIsIntlFailure($formatter, $errorMessage, $errorCode);
+ }
+
+ public function formatErrorProvider()
+ {
+ // With PHP 5.5 IntlDateFormatter accepts empty values ('0')
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ return array(
+ array('y-M-d', 'foobar', 'datefmt_format: string \'foobar\' is not numeric, which would be required for it to be a valid date: U_ILLEGAL_ARGUMENT_ERROR')
+ );
+ }
+
+ $message = 'datefmt_format: takes either an array or an integer timestamp value : U_ILLEGAL_ARGUMENT_ERROR';
+
+ if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
+ $message = 'datefmt_format: takes either an array or an integer timestamp value or a DateTime object: U_ILLEGAL_ARGUMENT_ERROR';
+ }
+
+ return array(
+ array('y-M-d', '0', $message),
+ array('y-M-d', 'foobar', $message),
+ );
+ }
+
+ /**
+ * @dataProvider formatWithTimezoneProvider
+ */
+ public function testFormatWithTimezone($timestamp, $timezone, $expected)
+ {
+ $pattern = 'yyyy-MM-dd HH:mm:ss';
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, $timezone, IntlDateFormatter::GREGORIAN, $pattern);
+ $this->assertSame($expected, $formatter->format($timestamp));
+ }
+
+ public function formatWithTimezoneProvider()
+ {
+ $data = array(
+ array(0, 'UTC', '1970-01-01 00:00:00'),
+ array(0, 'GMT', '1970-01-01 00:00:00'),
+ array(0, 'GMT-03:00', '1969-12-31 21:00:00'),
+ array(0, 'GMT+03:00', '1970-01-01 03:00:00'),
+ array(0, 'Europe/Zurich', '1970-01-01 01:00:00'),
+ array(0, 'Europe/Paris', '1970-01-01 01:00:00'),
+ array(0, 'Africa/Cairo', '1970-01-01 02:00:00'),
+ array(0, 'Africa/Casablanca', '1970-01-01 00:00:00'),
+ array(0, 'Africa/Djibouti', '1970-01-01 03:00:00'),
+ array(0, 'Africa/Johannesburg', '1970-01-01 02:00:00'),
+ array(0, 'America/Antigua', '1969-12-31 20:00:00'),
+ array(0, 'America/Toronto', '1969-12-31 19:00:00'),
+ array(0, 'America/Vancouver', '1969-12-31 16:00:00'),
+ array(0, 'Asia/Aqtau', '1970-01-01 05:00:00'),
+ array(0, 'Asia/Bangkok', '1970-01-01 07:00:00'),
+ array(0, 'Asia/Dubai', '1970-01-01 04:00:00'),
+ array(0, 'Australia/Brisbane', '1970-01-01 10:00:00'),
+ array(0, 'Australia/Eucla', '1970-01-01 08:45:00'),
+ array(0, 'Australia/Melbourne', '1970-01-01 10:00:00'),
+ array(0, 'Europe/Berlin', '1970-01-01 01:00:00'),
+ array(0, 'Europe/Dublin', '1970-01-01 01:00:00'),
+ array(0, 'Europe/Warsaw', '1970-01-01 01:00:00'),
+ array(0, 'Pacific/Fiji', '1970-01-01 12:00:00'),
+ );
+
+ // As of PHP 5.5, intl ext no longer fallbacks invalid time zones to UTC
+ if (!version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ // When time zone not exists, uses UTC by default
+ $data[] = array(0, 'Foo/Bar', '1970-01-01 00:00:00');
+ $data[] = array(0, 'UTC+04:30', '1970-01-01 00:00:00');
+ $data[] = array(0, 'UTC+04:AA', '1970-01-01 00:00:00');
+ }
+
+ return $data;
+ }
+
+ public function testFormatWithGmtTimezone()
+ {
+ $formatter = $this->getDefaultDateFormatter('zzzz');
+
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone('GMT+03:00');
+ } else {
+ $formatter->setTimeZoneId('GMT+03:00');
+ }
+
+ $this->assertEquals('GMT+03:00', $formatter->format(0));
+ }
+
+ public function testFormatWithGmtTimeZoneAndMinutesOffset()
+ {
+ $formatter = $this->getDefaultDateFormatter('zzzz');
+
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone('GMT+00:30');
+ } else {
+ $formatter->setTimeZoneId('GMT+00:30');
+ }
+
+ $this->assertEquals('GMT+00:30', $formatter->format(0));
+ }
+
+ public function testFormatWithNonStandardTimezone()
+ {
+ $formatter = $this->getDefaultDateFormatter('zzzz');
+
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone('Pacific/Fiji');
+ } else {
+ $formatter->setTimeZoneId('Pacific/Fiji');
+ }
+
+ $this->assertEquals('Fiji Standard Time', $formatter->format(0));
+ }
+
+ public function testFormatWithConstructorTimezone()
+ {
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC');
+ $formatter->setPattern('yyyy-MM-dd HH:mm:ss');
+
+ $this->assertEquals(
+ $this->getDateTime(0)->format('Y-m-d H:i:s'),
+ $formatter->format(0)
+ );
+ }
+
+ public function testFormatWithTimezoneFromEnvironmentVariable()
+ {
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $this->markTestSkipped('IntlDateFormatter in PHP 5.5 no longer depends on TZ environment.');
+ }
+
+ $tz = getenv('TZ');
+ putenv('TZ=Europe/London');
+
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
+ $formatter->setPattern('yyyy-MM-dd HH:mm:ss');
+
+ $this->assertEquals(
+ $this->getDateTime(0)->format('Y-m-d H:i:s'),
+ $formatter->format(0)
+ );
+
+ $this->assertEquals('Europe/London', getenv('TZ'));
+
+ // Restores TZ.
+ putenv('TZ='.$tz);
+ }
+
+ public function testFormatWithTimezoneFromPhp()
+ {
+ if (!version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $this->markTestSkipped('Only in PHP 5.5 IntlDateFormatter depends on default timezone (`date_default_timezone_get()`).');
+ }
+
+ $tz = date_default_timezone_get();
+ date_default_timezone_set('Europe/London');
+
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
+ $formatter->setPattern('yyyy-MM-dd HH:mm:ss');
+
+ $this->assertEquals(
+ $this->getDateTime(0)->format('Y-m-d H:i:s'),
+ $formatter->format(0)
+ );
+
+ $this->assertEquals('Europe/London', date_default_timezone_get());
+
+ // Restores TZ.
+ date_default_timezone_set($tz);
+ }
+
+ /**
+ * @dataProvider dateAndTimeTypeProvider
+ */
+ public function testDateAndTimeType($timestamp, $datetype, $timetype, $expected)
+ {
+ $formatter = $this->getDateFormatter('en', $datetype, $timetype, 'UTC');
+ $this->assertSame($expected, $formatter->format($timestamp));
+ }
+
+ public function dateAndTimeTypeProvider()
+ {
+ return array(
+ array(0, IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'Thursday, January 1, 1970'),
+ array(0, IntlDateFormatter::LONG, IntlDateFormatter::NONE, 'January 1, 1970'),
+ array(0, IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE, 'Jan 1, 1970'),
+ array(0, IntlDateFormatter::SHORT, IntlDateFormatter::NONE, '1/1/70'),
+ array(0, IntlDateFormatter::NONE, IntlDateFormatter::FULL, '12:00:00 AM GMT'),
+ array(0, IntlDateFormatter::NONE, IntlDateFormatter::LONG, '12:00:00 AM GMT'),
+ array(0, IntlDateFormatter::NONE, IntlDateFormatter::MEDIUM, '12:00:00 AM'),
+ array(0, IntlDateFormatter::NONE, IntlDateFormatter::SHORT, '12:00 AM'),
+ );
+ }
+
+ public function testGetCalendar()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $this->assertEquals(IntlDateFormatter::GREGORIAN, $formatter->getCalendar());
+ }
+
+ public function testGetDateType()
+ {
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::FULL, IntlDateFormatter::NONE);
+ $this->assertEquals(IntlDateFormatter::FULL, $formatter->getDateType());
+ }
+
+ public function testGetLocale()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $this->assertEquals('en', $formatter->getLocale());
+ }
+
+ public function testGetPattern()
+ {
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'UTC', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd');
+ $this->assertEquals('yyyy-MM-dd', $formatter->getPattern());
+ }
+
+ public function testGetTimeType()
+ {
+ $formatter = $this->getDateFormatter('en', IntlDateFormatter::NONE, IntlDateFormatter::FULL);
+ $this->assertEquals(IntlDateFormatter::FULL, $formatter->getTimeType());
+ }
+
+ /**
+ * @dataProvider parseProvider
+ */
+ public function testParse($pattern, $value, $expected)
+ {
+ $errorCode = IntlGlobals::U_ZERO_ERROR;
+ $errorMessage = 'U_ZERO_ERROR';
+
+ $formatter = $this->getDefaultDateFormatter($pattern);
+ $this->assertSame($expected, $formatter->parse($value));
+ $this->assertIsIntlSuccess($formatter, $errorMessage, $errorCode);
+ }
+
+ public function parseProvider()
+ {
+ return array_merge(
+ $this->parseYearProvider(),
+ $this->parseQuarterProvider(),
+ $this->parseMonthProvider(),
+ $this->parseStandaloneMonthProvider(),
+ $this->parseDayProvider(),
+ $this->parseDayOfWeekProvider(),
+ $this->parseDayOfYearProvider(),
+ $this->parseHour12ClockOneBasedProvider(),
+ $this->parseHour12ClockZeroBasedProvider(),
+ $this->parseHour24ClockOneBasedProvider(),
+ $this->parseHour24ClockZeroBasedProvider(),
+ $this->parseMinuteProvider(),
+ $this->parseSecondProvider(),
+ $this->parseTimezoneProvider(),
+ $this->parseAmPmProvider(),
+ $this->parseStandaloneAmPmProvider(),
+ $this->parseRegexMetaCharsProvider(),
+ $this->parseQuoteCharsProvider(),
+ $this->parseDashSlashProvider()
+ );
+ }
+
+ public function parseYearProvider()
+ {
+ return array(
+ array('y-M-d', '1970-1-1', 0),
+ array('yy-M-d', '70-1-1', 0),
+ );
+ }
+
+ public function parseQuarterProvider()
+ {
+ return array(
+ array('Q', '1', 0),
+ array('QQ', '01', 0),
+ array('QQQ', 'Q1', 0),
+ array('QQQQ', '1st quarter', 0),
+ array('QQQQQ', '1st quarter', 0),
+
+ array('Q', '2', 7776000),
+ array('QQ', '02', 7776000),
+ array('QQQ', 'Q2', 7776000),
+ array('QQQQ', '2nd quarter', 7776000),
+ array('QQQQQ', '2nd quarter', 7776000),
+
+ array('q', '1', 0),
+ array('qq', '01', 0),
+ array('qqq', 'Q1', 0),
+ array('qqqq', '1st quarter', 0),
+ array('qqqqq', '1st quarter', 0),
+ );
+ }
+
+ public function parseMonthProvider()
+ {
+ return array(
+ array('y-M-d', '1970-1-1', 0),
+ array('y-MMM-d', '1970-Jan-1', 0),
+ array('y-MMMM-d', '1970-January-1', 0),
+ );
+ }
+
+ public function parseStandaloneMonthProvider()
+ {
+ return array(
+ array('y-L-d', '1970-1-1', 0),
+ array('y-LLL-d', '1970-Jan-1', 0),
+ array('y-LLLL-d', '1970-January-1', 0),
+ );
+ }
+
+ public function parseDayProvider()
+ {
+ return array(
+ array('y-M-d', '1970-1-1', 0),
+ array('y-M-dd', '1970-1-01', 0),
+ array('y-M-ddd', '1970-1-001', 0),
+ );
+ }
+
+ public function parseDayOfWeekProvider()
+ {
+ return array(
+ array('E', 'Thu', 0),
+ array('EE', 'Thu', 0),
+ array('EEE', 'Thu', 0),
+ array('EEEE', 'Thursday', 0),
+ array('EEEEE', 'T', 432000),
+ array('EEEEEE', 'Thu', 0),
+ );
+ }
+
+ public function parseDayOfYearProvider()
+ {
+ return array(
+ array('D', '1', 0),
+ array('D', '2', 86400),
+ );
+ }
+
+ public function parseHour12ClockOneBasedProvider()
+ {
+ return array(
+ // 12 hours (1-12)
+ array('y-M-d h', '1970-1-1 1', 3600),
+ array('y-M-d h', '1970-1-1 10', 36000),
+ array('y-M-d hh', '1970-1-1 11', 39600),
+ array('y-M-d hh', '1970-1-1 12', 0),
+ array('y-M-d hh a', '1970-1-1 0 AM', 0),
+ array('y-M-d hh a', '1970-1-1 1 AM', 3600),
+ array('y-M-d hh a', '1970-1-1 10 AM', 36000),
+ array('y-M-d hh a', '1970-1-1 11 AM', 39600),
+ array('y-M-d hh a', '1970-1-1 12 AM', 0),
+ array('y-M-d hh a', '1970-1-1 23 AM', 82800),
+ array('y-M-d hh a', '1970-1-1 24 AM', 86400),
+ array('y-M-d hh a', '1970-1-1 0 PM', 43200),
+ array('y-M-d hh a', '1970-1-1 1 PM', 46800),
+ array('y-M-d hh a', '1970-1-1 10 PM', 79200),
+ array('y-M-d hh a', '1970-1-1 11 PM', 82800),
+ array('y-M-d hh a', '1970-1-1 12 PM', 43200),
+ array('y-M-d hh a', '1970-1-1 23 PM', 126000),
+ array('y-M-d hh a', '1970-1-1 24 PM', 129600),
+ );
+ }
+
+ public function parseHour12ClockZeroBasedProvider()
+ {
+ return array(
+ // 12 hours (0-11)
+ array('y-M-d K', '1970-1-1 1', 3600),
+ array('y-M-d K', '1970-1-1 10', 36000),
+ array('y-M-d KK', '1970-1-1 11', 39600),
+ array('y-M-d KK', '1970-1-1 12', 43200),
+ array('y-M-d KK a', '1970-1-1 0 AM', 0),
+ array('y-M-d KK a', '1970-1-1 1 AM', 3600),
+ array('y-M-d KK a', '1970-1-1 10 AM', 36000),
+ array('y-M-d KK a', '1970-1-1 11 AM', 39600),
+ array('y-M-d KK a', '1970-1-1 12 AM', 43200),
+ array('y-M-d KK a', '1970-1-1 23 AM', 82800),
+ array('y-M-d KK a', '1970-1-1 24 AM', 86400),
+ array('y-M-d KK a', '1970-1-1 0 PM', 43200),
+ array('y-M-d KK a', '1970-1-1 1 PM', 46800),
+ array('y-M-d KK a', '1970-1-1 10 PM', 79200),
+ array('y-M-d KK a', '1970-1-1 11 PM', 82800),
+ array('y-M-d KK a', '1970-1-1 12 PM', 86400),
+ array('y-M-d KK a', '1970-1-1 23 PM', 126000),
+ array('y-M-d KK a', '1970-1-1 24 PM', 129600),
+ );
+ }
+
+ public function parseHour24ClockOneBasedProvider()
+ {
+ return array(
+ // 24 hours (1-24)
+ array('y-M-d k', '1970-1-1 1', 3600),
+ array('y-M-d k', '1970-1-1 10', 36000),
+ array('y-M-d kk', '1970-1-1 11', 39600),
+ array('y-M-d kk', '1970-1-1 12', 43200),
+ array('y-M-d kk', '1970-1-1 23', 82800),
+ array('y-M-d kk', '1970-1-1 24', 0),
+ array('y-M-d kk a', '1970-1-1 0 AM', 0),
+ array('y-M-d kk a', '1970-1-1 1 AM', 0),
+ array('y-M-d kk a', '1970-1-1 10 AM', 0),
+ array('y-M-d kk a', '1970-1-1 11 AM', 0),
+ array('y-M-d kk a', '1970-1-1 12 AM', 0),
+ array('y-M-d kk a', '1970-1-1 23 AM', 0),
+ array('y-M-d kk a', '1970-1-1 24 AM', 0),
+ array('y-M-d kk a', '1970-1-1 0 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 1 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 10 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 11 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 12 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 23 PM', 43200),
+ array('y-M-d kk a', '1970-1-1 24 PM', 43200),
+ );
+ }
+
+ public function parseHour24ClockZeroBasedProvider()
+ {
+ return array(
+ // 24 hours (0-23)
+ array('y-M-d H', '1970-1-1 0', 0),
+ array('y-M-d H', '1970-1-1 1', 3600),
+ array('y-M-d H', '1970-1-1 10', 36000),
+ array('y-M-d HH', '1970-1-1 11', 39600),
+ array('y-M-d HH', '1970-1-1 12', 43200),
+ array('y-M-d HH', '1970-1-1 23', 82800),
+ array('y-M-d HH a', '1970-1-1 0 AM', 0),
+ array('y-M-d HH a', '1970-1-1 1 AM', 0),
+ array('y-M-d HH a', '1970-1-1 10 AM', 0),
+ array('y-M-d HH a', '1970-1-1 11 AM', 0),
+ array('y-M-d HH a', '1970-1-1 12 AM', 0),
+ array('y-M-d HH a', '1970-1-1 23 AM', 0),
+ array('y-M-d HH a', '1970-1-1 24 AM', 0),
+ array('y-M-d HH a', '1970-1-1 0 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 1 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 10 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 11 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 12 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 23 PM', 43200),
+ array('y-M-d HH a', '1970-1-1 24 PM', 43200),
+ );
+ }
+
+ public function parseMinuteProvider()
+ {
+ return array(
+ array('y-M-d HH:m', '1970-1-1 0:1', 60),
+ array('y-M-d HH:mm', '1970-1-1 0:10', 600),
+ );
+ }
+
+ public function parseSecondProvider()
+ {
+ return array(
+ array('y-M-d HH:mm:s', '1970-1-1 00:01:1', 61),
+ array('y-M-d HH:mm:ss', '1970-1-1 00:01:10', 70),
+ );
+ }
+
+ public function parseTimezoneProvider()
+ {
+ return array(
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-03:00', 10800),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-04:00', 14400),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-00:00', 0),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+03:00', -10800),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+04:00', -14400),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT-0300', 10800),
+ array('y-M-d HH:mm:ss zzzz', '1970-1-1 00:00:00 GMT+0300', -10800),
+
+ // a previous timezone parsing should not change the timezone for the next parsing
+ array('y-M-d HH:mm:ss', '1970-1-1 00:00:00', 0),
+ );
+ }
+
+ public function parseAmPmProvider()
+ {
+ return array(
+ // AM/PM (already covered by hours tests)
+ array('y-M-d HH:mm:ss a', '1970-1-1 00:00:00 AM', 0),
+ array('y-M-d HH:mm:ss a', '1970-1-1 00:00:00 PM', 43200),
+ );
+ }
+
+ public function parseStandaloneAmPmProvider()
+ {
+ return array(
+ array('a', 'AM', 0),
+ array('a', 'PM', 43200),
+ );
+ }
+
+ public function parseRegexMetaCharsProvider()
+ {
+ return array(
+ // regexp meta chars in the pattern string
+ array('y[M-d', '1970[1-1', 0),
+ array('y[M/d', '1970[1/1', 0),
+ );
+ }
+
+ public function parseQuoteCharsProvider()
+ {
+ return array(
+ array("'M'", 'M', 0),
+ array("'yy'", 'yy', 0),
+ array("'''yy'", "'yy", 0),
+ array("''y", "'1970", 0),
+ array("H 'o'' clock'", "0 o' clock", 0),
+ );
+ }
+
+ public function parseDashSlashProvider()
+ {
+ return array(
+ array('y-M-d', '1970/1/1', 0),
+ array('yy-M-d', '70/1/1', 0),
+ array('y/M/d', '1970-1-1', 0),
+ array('yy/M/d', '70-1-1', 0),
+ );
+ }
+
+ /**
+ * @dataProvider parseErrorProvider
+ */
+ public function testParseError($pattern, $value)
+ {
+ $errorCode = IntlGlobals::U_PARSE_ERROR;
+ $errorMessage = 'Date parsing failed: U_PARSE_ERROR';
+
+ $formatter = $this->getDefaultDateFormatter($pattern);
+ $this->assertFalse($formatter->parse($value));
+ $this->assertIsIntlFailure($formatter, $errorMessage, $errorCode);
+ }
+
+ public function parseErrorProvider()
+ {
+ return array(
+ // 1 char month
+ array('y-MMMMM-d', '1970-J-1'),
+ array('y-MMMMM-d', '1970-S-1'),
+
+ // standalone 1 char month
+ array('y-LLLLL-d', '1970-J-1'),
+ array('y-LLLLL-d', '1970-S-1'),
+ );
+ }
+
+ /*
+ * https://github.com/symfony/symfony/issues/4242
+ */
+ public function testParseAfterError()
+ {
+ $this->testParseError('y-MMMMM-d', '1970-J-1');
+ $this->testParse('y-M-d', '1970-1-1', 0);
+ }
+
+ public function testParseWithNullPositionValue()
+ {
+ $position = null;
+ $formatter = $this->getDefaultDateFormatter('y');
+ $this->assertSame(0, $formatter->parse('1970', $position));
+ $this->assertNull($position);
+ }
+
+ public function testSetPattern()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $formatter->setPattern('yyyy-MM-dd');
+ $this->assertEquals('yyyy-MM-dd', $formatter->getPattern());
+ }
+
+ /**
+ * @covers Symfony\Component\Intl\DateFormatter\IntlDateFormatter::getTimeZoneId
+ * @dataProvider setTimeZoneIdProvider
+ */
+ public function testSetTimeZoneId($timeZoneId, $expectedTimeZoneId)
+ {
+ $formatter = $this->getDefaultDateFormatter();
+
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $formatter->setTimeZone($timeZoneId);
+ } else {
+ $formatter->setTimeZoneId($timeZoneId);
+ }
+
+ $this->assertEquals($expectedTimeZoneId, $formatter->getTimeZoneId());
+ }
+
+ public function setTimeZoneIdProvider()
+ {
+ return array(
+ array('UTC', 'UTC'),
+ array('GMT', 'GMT'),
+ array('GMT-03:00', 'GMT-03:00'),
+ array('Europe/Zurich', 'Europe/Zurich'),
+ array('GMT-0300', 'GMT-0300'),
+ array('Foo/Bar', 'Foo/Bar'),
+ array('GMT+00:AA', 'GMT+00:AA'),
+ array('GMT+00AA', 'GMT+00AA'),
+ );
+ }
+
+ protected function getDefaultDateFormatter($pattern = null)
+ {
+ return $this->getDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN, $pattern);
+ }
+
+ protected function getDateTime($timestamp = null)
+ {
+ if (version_compare(PHP_VERSION, '5.5.0-dev', '>=')) {
+ $timeZone = date_default_timezone_get();
+ } else {
+ $timeZone = getenv('TZ') ?: 'UTC';
+ }
+
+ $dateTime = new \DateTime();
+ $dateTime->setTimestamp(null === $timestamp ? time() : $timestamp);
+ $dateTime->setTimeZone(new \DateTimeZone($timeZone));
+
+ return $dateTime;
+ }
+
+ protected function assertIsIntlFailure($formatter, $errorMessage, $errorCode)
+ {
+ $this->assertSame($errorMessage, $this->getIntlErrorMessage());
+ $this->assertSame($errorCode, $this->getIntlErrorCode());
+ $this->assertTrue($this->isIntlFailure($this->getIntlErrorCode()));
+ $this->assertSame($errorMessage, $formatter->getErrorMessage());
+ $this->assertSame($errorCode, $formatter->getErrorCode());
+ $this->assertTrue($this->isIntlFailure($formatter->getErrorCode()));
+ }
+
+ protected function assertIsIntlSuccess($formatter, $errorMessage, $errorCode)
+ {
+ /* @var IntlDateFormatter $formatter */
+ $this->assertSame($errorMessage, $this->getIntlErrorMessage());
+ $this->assertSame($errorCode, $this->getIntlErrorCode());
+ $this->assertFalse($this->isIntlFailure($this->getIntlErrorCode()));
+ $this->assertSame($errorMessage, $formatter->getErrorMessage());
+ $this->assertSame($errorCode, $formatter->getErrorCode());
+ $this->assertFalse($this->isIntlFailure($formatter->getErrorCode()));
+ }
+
+ /**
+ * @param $locale
+ * @param $datetype
+ * @param $timetype
+ * @param null $timezone
+ * @param int $calendar
+ * @param null $pattern
+ *
+ * @return mixed
+ */
+ abstract protected function getDateFormatter($locale, $datetype, $timetype, $timezone = null, $calendar = IntlDateFormatter::GREGORIAN, $pattern = null);
+
+ /**
+ * @return string
+ */
+ abstract protected function getIntlErrorMessage();
+
+ /**
+ * @return integer
+ */
+ abstract protected function getIntlErrorCode();
+
+ /**
+ * @param integer $errorCode
+ *
+ * @return Boolean
+ */
+ abstract protected function isIntlFailure($errorCode);
+}
--- /dev/null
+<?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\Intl\Tests\DateFormatter;
+
+use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
+use Symfony\Component\Intl\Globals\IntlGlobals;
+
+class IntlDateFormatterTest extends AbstractIntlDateFormatterTest
+{
+ public function testConstructor()
+ {
+ $formatter = new IntlDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN, 'y-M-d');
+ $this->assertEquals('y-M-d', $formatter->getPattern());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testConstructorWithUnsupportedLocale()
+ {
+ new IntlDateFormatter('pt_BR', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
+ }
+
+ public function testStaticCreate()
+ {
+ $formatter = IntlDateFormatter::create('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT);
+ $this->assertInstanceOf('\Symfony\Component\Intl\DateFormatter\IntlDateFormatter', $formatter);
+ }
+
+ public function testFormatWithUnsupportedTimestampArgument()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+
+ $localtime = array(
+ 'tm_sec' => 59,
+ 'tm_min' => 3,
+ 'tm_hour' => 15,
+ 'tm_mday' => 15,
+ 'tm_mon' => 3,
+ 'tm_year' => 112,
+ 'tm_wday' => 0,
+ 'tm_yday' => 105,
+ 'tm_isdst' => 0
+ );
+
+ try {
+ $formatter->format($localtime);
+ } catch (\Exception $e) {
+ $this->assertInstanceOf('Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException', $e);
+
+ if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
+ $this->assertStringEndsWith('Only integer unix timestamps and DateTime objects are supported. Please install the "intl" extension for full localization capabilities.', $e->getMessage());
+ } else {
+ $this->assertStringEndsWith('Only integer unix timestamps are supported. Please install the "intl" extension for full localization capabilities.', $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatWithUnimplementedChars()
+ {
+ $pattern = 'Y';
+ $formatter = new IntlDateFormatter('en', IntlDateFormatter::MEDIUM, IntlDateFormatter::SHORT, 'UTC', IntlDateFormatter::GREGORIAN, $pattern);
+ $formatter->format(0);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatWithNonIntegerTimestamp()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $formatter->format(array());
+ }
+
+ public function testGetErrorCode()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $this->assertEquals(IntlGlobals::getErrorCode(), $formatter->getErrorCode());
+ }
+
+ public function testGetErrorMessage()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $this->assertEquals(IntlGlobals::getErrorMessage(), $formatter->getErrorMessage());
+ }
+
+ public function testIsLenient()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $this->assertFalse($formatter->isLenient());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testLocaltime()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $formatter->localtime('Wednesday, December 31, 1969 4:00:00 PM PT');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException
+ */
+ public function testParseWithNotNullPositionValue()
+ {
+ $position = 0;
+ $formatter = $this->getDefaultDateFormatter('y');
+ $this->assertSame(0, $formatter->parse('1970', $position));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetCalendar()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $formatter->setCalendar(IntlDateFormatter::GREGORIAN);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testSetLenient()
+ {
+ $formatter = $this->getDefaultDateFormatter();
+ $formatter->setLenient(true);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatWithGmtTimeZoneAndMinutesOffset()
+ {
+ parent::testFormatWithGmtTimeZoneAndMinutesOffset();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatWithNonStandardTimezone()
+ {
+ parent::testFormatWithNonStandardTimezone();
+ }
+
+ public function parseStandaloneAmPmProvider()
+ {
+ return $this->notImplemented(parent::parseStandaloneAmPmProvider());
+ }
+
+ public function parseDayOfWeekProvider()
+ {
+ return $this->notImplemented(parent::parseDayOfWeekProvider());
+ }
+
+ public function parseDayOfYearProvider()
+ {
+ return $this->notImplemented(parent::parseDayOfYearProvider());
+ }
+
+ public function parseQuarterProvider()
+ {
+ return $this->notImplemented(parent::parseQuarterProvider());
+ }
+
+ protected function getDateFormatter($locale, $datetype, $timetype, $timezone = null, $calendar = IntlDateFormatter::GREGORIAN, $pattern = null)
+ {
+ return new IntlDateFormatter($locale, $datetype, $timetype, $timezone, $calendar, $pattern);
+ }
+
+ protected function getIntlErrorMessage()
+ {
+ return IntlGlobals::getErrorMessage();
+ }
+
+ protected function getIntlErrorCode()
+ {
+ return IntlGlobals::getErrorCode();
+ }
+
+ protected function isIntlFailure($errorCode)
+ {
+ return IntlGlobals::isFailure($errorCode);
+ }
+
+ /**
+ * Just to document the differences between the stub and the intl
+ * implementations. The intl can parse any of the tested formats alone. The
+ * stub does not implement them as it would be needed to add more
+ * abstraction, passing more context to the transformers objects. Any of the
+ * formats are ignored alone or with date/time data (years, months, days,
+ * hours, minutes and seconds).
+ *
+ * Also in intl, format like 'ss E' for '10 2' (2nd day of year
+ * + 10 seconds) are added, then we have 86,400 seconds (24h * 60min * 60s)
+ * + 10 seconds
+ *
+ * @param array $dataSets
+ *
+ * @return array
+ */
+ private function notImplemented(array $dataSets)
+ {
+ return array_map(function ($row) {
+ return array($row[0], $row[1], 0);
+ }, $dataSets);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\DateFormatter\Verification;
+
+use Symfony\Component\Intl\DateFormatter\IntlDateFormatter;
+use Symfony\Component\Intl\Tests\DateFormatter\AbstractIntlDateFormatterTest;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Verifies that {@link AbstractIntlDateFormatterTest} matches the behavior of
+ * the {@link \IntlDateFormatter} class in a specific version of ICU.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IntlDateFormatterTest extends AbstractIntlDateFormatterTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ /**
+ * It seems IntlDateFormatter caches the timezone id when not explicitly set via constructor or by the
+ * setTimeZoneId() method. Since testFormatWithDefaultTimezoneIntl() runs using the default environment
+ * time zone, this test would use it too if not running in a separated process.
+ *
+ * @runInSeparateProcess
+ */
+ public function testFormatWithTimezoneFromEnvironmentVariable()
+ {
+ parent::testFormatWithTimezoneFromEnvironmentVariable();
+ }
+
+ protected function getDateFormatter($locale, $datetype, $timetype, $timezone = null, $calendar = IntlDateFormatter::GREGORIAN, $pattern = null)
+ {
+ return new \IntlDateFormatter($locale, $datetype, $timetype, $timezone, $calendar, $pattern);
+ }
+
+ protected function getIntlErrorMessage()
+ {
+ return intl_get_error_message();
+ }
+
+ protected function getIntlErrorCode()
+ {
+ return intl_get_error_code();
+ }
+
+ protected function isIntlFailure($errorCode)
+ {
+ return intl_is_failure($errorCode);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Globals;
+
+/**
+ * Test case for intl function implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractIntlGlobalsTest extends \PHPUnit_Framework_TestCase
+{
+ public function errorNameProvider()
+ {
+ return array (
+ array(-129, '[BOGUS UErrorCode]'),
+ array(0, 'U_ZERO_ERROR'),
+ array(1, 'U_ILLEGAL_ARGUMENT_ERROR'),
+ array(9, 'U_PARSE_ERROR'),
+ array(129, '[BOGUS UErrorCode]'),
+ );
+ }
+
+ /**
+ * @dataProvider errorNameProvider
+ */
+ public function testGetErrorName($errorCode, $errorName)
+ {
+ $this->assertSame($errorName, $this->getIntlErrorName($errorCode));
+ }
+
+ abstract protected function getIntlErrorName($errorCode);
+}
--- /dev/null
+<?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\Intl\Tests\Globals;
+
+use Symfony\Component\Intl\Globals\IntlGlobals;
+
+class IntlGlobalsTest extends AbstractIntlGlobalsTest
+{
+ protected function getIntlErrorName($errorCode)
+ {
+ return IntlGlobals::getErrorName($errorCode);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Globals\Verification;
+
+use Symfony\Component\Intl\Tests\Globals\AbstractIntlGlobalsTest;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Verifies that {@link AbstractIntlGlobalsTest} matches the behavior of the
+ * intl functions with a specific version of ICU.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IntlGlobalsTest extends AbstractIntlGlobalsTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ protected function getIntlErrorName($errorCode)
+ {
+ return intl_error_name($errorCode);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Locale;
+
+/**
+ * Test case for Locale implementations.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+abstract class AbstractLocaleTest extends \PHPUnit_Framework_TestCase
+{
+ public function testSetDefault()
+ {
+ $this->call('setDefault', 'en_GB');
+
+ $this->assertSame('en_GB', $this->call('getDefault'));
+ }
+
+ abstract protected function call($methodName);
+}
--- /dev/null
+<?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\Intl\Tests\Locale;
+
+class LocaleTest extends AbstractLocaleTest
+{
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testAcceptFromHttp()
+ {
+ $this->call('acceptFromHttp', 'pt-br,en-us;q=0.7,en;q=0.5');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testComposeLocale()
+ {
+ $subtags = array(
+ 'language' => 'pt',
+ 'script' => 'Latn',
+ 'region' => 'BR'
+ );
+ $this->call('composeLocale', $subtags);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testFilterMatches()
+ {
+ $this->call('filterMatches', 'pt-BR', 'pt-BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetAllVariants()
+ {
+ $this->call('getAllVariants', 'pt_BR_Latn');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetDisplayLanguage()
+ {
+ $this->call('getDisplayLanguage', 'pt-Latn-BR', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetDisplayName()
+ {
+ $this->call('getDisplayName', 'pt-Latn-BR', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetDisplayRegion()
+ {
+ $this->call('getDisplayRegion', 'pt-Latn-BR', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetDisplayScript()
+ {
+ $this->call('getDisplayScript', 'pt-Latn-BR', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetDisplayVariant()
+ {
+ $this->call('getDisplayVariant', 'pt-Latn-BR', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetKeywords()
+ {
+ $this->call('getKeywords', 'pt-BR@currency=BRL');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetPrimaryLanguage()
+ {
+ $this->call('getPrimaryLanguage', 'pt-Latn-BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetRegion()
+ {
+ $this->call('getRegion', 'pt-Latn-BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetScript()
+ {
+ $this->call('getScript', 'pt-Latn-BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testLookup()
+ {
+ $langtag = array(
+ 'pt-Latn-BR',
+ 'pt-BR'
+ );
+ $this->call('lookup', $langtag, 'pt-BR-x-priv1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testParseLocale()
+ {
+ $this->call('parseLocale', 'pt-Latn-BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetDefault()
+ {
+ $this->call('setDefault', 'pt_BR');
+ }
+
+ protected function call($methodName)
+ {
+ $args = array_slice(func_get_args(), 1);
+
+ return call_user_func_array(array('Symfony\Component\Intl\Locale\Locale', $methodName), $args);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Locale\Verification;
+
+use Symfony\Component\Intl\Tests\Locale\AbstractLocaleTest;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Verifies that {@link AbstractLocaleTest} matches the behavior of the
+ * {@link Locale} class with a specific version of ICU.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LocaleTest extends AbstractLocaleTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ protected function call($methodName)
+ {
+ $args = array_slice(func_get_args(), 1);
+
+ return call_user_func_array(array('Locale', $methodName), $args);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\NumberFormatter;
+
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\Intl;
+use Symfony\Component\Intl\Locale;
+use Symfony\Component\Intl\NumberFormatter\NumberFormatter;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Note that there are some values written like -2147483647 - 1. This is the lower 32bit int max and is a known
+ * behavior of PHP.
+ */
+abstract class AbstractNumberFormatterTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider formatCurrencyWithDecimalStyleProvider
+ */
+ public function testFormatCurrencyWithDecimalStyle($value, $currency, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $this->assertEquals($expected, $formatter->formatCurrency($value, $currency));
+ }
+
+ public function formatCurrencyWithDecimalStyleProvider()
+ {
+ return array(
+ array(100, 'ALL', '100'),
+ array(100, 'BRL', '100.00'),
+ array(100, 'CRC', '100'),
+ array(100, 'JPY', '100'),
+ array(100, 'CHF', '100'),
+ array(-100, 'ALL', '-100'),
+ array(-100, 'BRL', '-100'),
+ array(-100, 'CRC', '-100'),
+ array(-100, 'JPY', '-100'),
+ array(-100, 'CHF', '-100'),
+ array(1000.12, 'ALL', '1,000.12'),
+ array(1000.12, 'BRL', '1,000.12'),
+ array(1000.12, 'CRC', '1,000.12'),
+ array(1000.12, 'JPY', '1,000.12'),
+ array(1000.12, 'CHF', '1,000.12')
+ );
+ }
+
+ /**
+ * @dataProvider formatCurrencyWithCurrencyStyleProvider
+ */
+ public function testFormatCurrencyWithCurrencyStyle($value, $currency, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+ $this->assertEquals($expected, $formatter->formatCurrency($value, $currency));
+ }
+
+ public function formatCurrencyWithCurrencyStyleProvider()
+ {
+ return array(
+ array(100, 'ALL', 'ALL100'),
+ array(-100, 'ALL', '(ALL100)'),
+ array(1000.12, 'ALL', 'ALL1,000'),
+
+ array(100, 'JPY', '¥100'),
+ array(-100, 'JPY', '(¥100)'),
+ array(1000.12, 'JPY', '¥1,000'),
+
+ array(100, 'EUR', '€100.00'),
+ array(-100, 'EUR', '(€100.00)'),
+ array(1000.12, 'EUR', '€1,000.12'),
+ );
+ }
+
+ /**
+ * @dataProvider formatCurrencyWithCurrencyStyleCostaRicanColonsRoundingProvider
+ */
+ public function testFormatCurrencyWithCurrencyStyleCostaRicanColonsRounding($value, $currency, $symbol, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+ $this->assertEquals(sprintf($expected, $symbol), $formatter->formatCurrency($value, $currency));
+ }
+
+ public function formatCurrencyWithCurrencyStyleCostaRicanColonsRoundingProvider()
+ {
+ return array(
+ array(100, 'CRC', 'CRC', '%s100'),
+ array(-100, 'CRC', 'CRC', '(%s100)'),
+ array(1000.12, 'CRC', 'CRC', '%s1,000'),
+ );
+ }
+
+ /**
+ * @dataProvider formatCurrencyWithCurrencyStyleBrazilianRealRoundingProvider
+ */
+ public function testFormatCurrencyWithCurrencyStyleBrazilianRealRounding($value, $currency, $symbol, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+ $this->assertEquals(sprintf($expected, $symbol), $formatter->formatCurrency($value, $currency));
+ }
+
+ public function formatCurrencyWithCurrencyStyleBrazilianRealRoundingProvider()
+ {
+ return array(
+ array(100, 'BRL', 'R', '%s$100.00'),
+ array(-100, 'BRL', 'R', '(%s$100.00)'),
+ array(1000.12, 'BRL', 'R', '%s$1,000.12'),
+
+ // Rounding checks
+ array(1000.121, 'BRL', 'R', '%s$1,000.12'),
+ array(1000.123, 'BRL', 'R', '%s$1,000.12'),
+ array(1000.125, 'BRL', 'R', '%s$1,000.12'),
+ array(1000.127, 'BRL', 'R', '%s$1,000.13'),
+ array(1000.129, 'BRL', 'R', '%s$1,000.13'),
+ array(11.50999, 'BRL', 'R', '%s$11.51'),
+ array(11.9999464, 'BRL', 'R', '%s$12.00')
+ );
+ }
+
+ /**
+ * @dataProvider formatCurrencyWithCurrencyStyleSwissRoundingProvider
+ */
+ public function testFormatCurrencyWithCurrencyStyleSwissRounding($value, $currency, $symbol, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+ $this->assertEquals(sprintf($expected, $symbol), $formatter->formatCurrency($value, $currency));
+ }
+
+ public function formatCurrencyWithCurrencyStyleSwissRoundingProvider()
+ {
+ return array(
+ array(100, 'CHF', 'CHF', '%s100.00'),
+ array(-100, 'CHF', 'CHF', '(%s100.00)'),
+ array(1000.12, 'CHF', 'CHF', '%s1,000.10'),
+ array('1000.12', 'CHF', 'CHF', '%s1,000.10'),
+
+ // Rounding checks
+ array(1000.121, 'CHF', 'CHF', '%s1,000.10'),
+ array(1000.123, 'CHF', 'CHF', '%s1,000.10'),
+ array(1000.125, 'CHF', 'CHF', '%s1,000.10'),
+ array(1000.127, 'CHF', 'CHF', '%s1,000.15'),
+ array(1000.129, 'CHF', 'CHF', '%s1,000.15'),
+
+ array(1200000.00, 'CHF', 'CHF', '%s1,200,000.00'),
+ array(1200000.1, 'CHF', 'CHF', '%s1,200,000.10'),
+ array(1200000.10, 'CHF', 'CHF', '%s1,200,000.10'),
+ array(1200000.101, 'CHF', 'CHF', '%s1,200,000.10')
+ );
+ }
+
+ public function testFormat()
+ {
+ $errorCode = IntlGlobals::U_ZERO_ERROR;
+ $errorMessage = 'U_ZERO_ERROR';
+
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $this->assertSame('9.555', $formatter->format(9.555));
+
+ $this->assertSame($errorMessage, $this->getIntlErrorMessage());
+ $this->assertSame($errorCode, $this->getIntlErrorCode());
+ $this->assertFalse($this->isIntlFailure($this->getIntlErrorCode()));
+ $this->assertSame($errorMessage, $formatter->getErrorMessage());
+ $this->assertSame($errorCode, $formatter->getErrorCode());
+ $this->assertFalse($this->isIntlFailure($formatter->getErrorCode()));
+ }
+
+ public function testFormatWithCurrencyStyle()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+ $this->assertEquals('¤1.00', $formatter->format(1));
+ }
+
+ /**
+ * @dataProvider formatTypeInt32Provider
+ */
+ public function testFormatTypeInt32($formatter, $value, $expected, $message = '')
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_INT32);
+ $this->assertEquals($expected, $formattedValue, $message);
+ }
+
+ public function formatTypeInt32Provider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ $message = '->format() TYPE_INT32 formats inconsistently an integer if out of the 32 bit range.';
+
+ return array(
+ array($formatter, 1, '1'),
+ array($formatter, 1.1, '1'),
+ array($formatter, 2147483648, '-2,147,483,648', $message),
+ array($formatter, -2147483649, '2,147,483,647', $message),
+ );
+ }
+
+ /**
+ * @dataProvider formatTypeInt32WithCurrencyStyleProvider
+ */
+ public function testFormatTypeInt32WithCurrencyStyle($formatter, $value, $expected, $message = '')
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_INT32);
+ $this->assertEquals($expected, $formattedValue, $message);
+ }
+
+ public function formatTypeInt32WithCurrencyStyleProvider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+
+ $message = '->format() TYPE_INT32 formats inconsistently an integer if out of the 32 bit range.';
+
+ return array(
+ array($formatter, 1, '¤1.00'),
+ array($formatter, 1.1, '¤1.00'),
+ array($formatter, 2147483648, '(¤2,147,483,648.00)', $message),
+ array($formatter, -2147483649, '¤2,147,483,647.00', $message)
+ );
+ }
+
+ /**
+ * The parse() method works differently with integer out of the 32 bit range. format() works fine.
+ * @dataProvider formatTypeInt64Provider
+ */
+ public function testFormatTypeInt64($formatter, $value, $expected)
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_INT64);
+ $this->assertEquals($expected, $formattedValue);
+ }
+
+ public function formatTypeInt64Provider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ return array(
+ array($formatter, 1, '1'),
+ array($formatter, 1.1, '1'),
+ array($formatter, 2147483648, '2,147,483,648'),
+ array($formatter, -2147483649, '-2,147,483,649'),
+ );
+ }
+
+ /**
+ * @dataProvider formatTypeInt64WithCurrencyStyleProvider
+ */
+ public function testFormatTypeInt64WithCurrencyStyle($formatter, $value, $expected)
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_INT64);
+ $this->assertEquals($expected, $formattedValue);
+ }
+
+ public function formatTypeInt64WithCurrencyStyleProvider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+
+ return array(
+ array($formatter, 1, '¤1.00'),
+ array($formatter, 1.1, '¤1.00'),
+ array($formatter, 2147483648, '¤2,147,483,648.00'),
+ array($formatter, -2147483649, '(¤2,147,483,649.00)')
+ );
+ }
+
+ /**
+ * @dataProvider formatTypeDoubleProvider
+ */
+ public function testFormatTypeDouble($formatter, $value, $expected)
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_DOUBLE);
+ $this->assertEquals($expected, $formattedValue);
+ }
+
+ public function formatTypeDoubleProvider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ return array(
+ array($formatter, 1, '1'),
+ array($formatter, 1.1, '1.1'),
+ );
+ }
+
+ /**
+ * @dataProvider formatTypeDoubleWithCurrencyStyleProvider
+ */
+ public function testFormatTypeDoubleWithCurrencyStyle($formatter, $value, $expected)
+ {
+ $formattedValue = $formatter->format($value, NumberFormatter::TYPE_DOUBLE);
+ $this->assertEquals($expected, $formattedValue);
+ }
+
+ public function formatTypeDoubleWithCurrencyStyleProvider()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+
+ return array(
+ array($formatter, 1, '¤1.00'),
+ array($formatter, 1.1, '¤1.10'),
+ );
+ }
+
+ /**
+ * @dataProvider formatTypeCurrencyProvider
+ * @expectedException \PHPUnit_Framework_Error_Warning
+ */
+ public function testFormatTypeCurrency($formatter, $value)
+ {
+ $formatter->format($value, NumberFormatter::TYPE_CURRENCY);
+ }
+
+ /**
+ * @dataProvider formatTypeCurrencyProvider
+ */
+ public function testFormatTypeCurrencyReturn($formatter, $value)
+ {
+ $this->assertFalse(@$formatter->format($value, NumberFormatter::TYPE_CURRENCY));
+ }
+
+ public function formatTypeCurrencyProvider()
+ {
+ $df = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $cf = $this->getNumberFormatter('en', NumberFormatter::CURRENCY);
+
+ return array(
+ array($df, 1),
+ array($cf, 1),
+ );
+ }
+
+ /**
+ * @dataProvider formatFractionDigitsProvider
+ */
+ public function testFormatFractionDigits($value, $expected, $fractionDigits = null, $expectedFractionDigits = 1)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ if (null !== $fractionDigits) {
+ $attributeRet = $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $fractionDigits);
+ }
+
+ $formattedValue = $formatter->format($value);
+ $this->assertSame($expected, $formattedValue);
+ $this->assertSame($expectedFractionDigits, $formatter->getAttribute(NumberFormatter::FRACTION_DIGITS));
+
+ if (isset($attributeRet)) {
+ $this->assertTrue($attributeRet);
+ }
+ }
+
+ public function formatFractionDigitsProvider()
+ {
+ return array(
+ array(1.123, '1.123', null, 0),
+ array(1.123, '1', 0, 0),
+ array(1.123, '1.1', 1, 1),
+ array(1.123, '1.12', 2, 2),
+ array(1.123, '1', -1, 0),
+ array(1.123, '1', 'abc', 0)
+ );
+ }
+
+ /**
+ * @dataProvider formatGroupingUsedProvider
+ */
+ public function testFormatGroupingUsed($value, $expected, $groupingUsed = null, $expectedGroupingUsed = 1)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ if (null !== $groupingUsed) {
+ $attributeRet = $formatter->setAttribute(NumberFormatter::GROUPING_USED, $groupingUsed);
+ }
+
+ $formattedValue = $formatter->format($value);
+ $this->assertSame($expected, $formattedValue);
+ $this->assertSame($expectedGroupingUsed, $formatter->getAttribute(NumberFormatter::GROUPING_USED));
+
+ if (isset($attributeRet)) {
+ $this->assertTrue($attributeRet);
+ }
+ }
+
+ public function formatGroupingUsedProvider()
+ {
+ return array(
+ array(1000, '1,000', null, 1),
+ array(1000, '1000', 0, 0),
+ array(1000, '1,000', 1, 1),
+ array(1000, '1,000', 2, 1),
+ array(1000, '1000', 'abc', 0),
+ array(1000, '1,000', -1, 1),
+ );
+ }
+
+ /**
+ * @dataProvider formatRoundingModeRoundHalfUpProvider
+ */
+ public function testFormatRoundingModeHalfUp($value, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
+
+ $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_HALFUP);
+ $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_HALFUP rounding mode.');
+ }
+
+ public function formatRoundingModeRoundHalfUpProvider()
+ {
+ // The commented value is differently rounded by intl's NumberFormatter in 32 and 64 bit architectures
+ return array(
+ array(1.121, '1.12'),
+ array(1.123, '1.12'),
+ // array(1.125, '1.13'),
+ array(1.127, '1.13'),
+ array(1.129, '1.13'),
+ );
+ }
+
+ /**
+ * @dataProvider formatRoundingModeRoundHalfDownProvider
+ */
+ public function testFormatRoundingModeHalfDown($value, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
+
+ $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_HALFDOWN);
+ $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_HALFDOWN rounding mode.');
+ }
+
+ public function formatRoundingModeRoundHalfDownProvider()
+ {
+ return array(
+ array(1.121, '1.12'),
+ array(1.123, '1.12'),
+ array(1.125, '1.12'),
+ array(1.127, '1.13'),
+ array(1.129, '1.13'),
+ );
+ }
+
+ /**
+ * @dataProvider formatRoundingModeRoundHalfEvenProvider
+ */
+ public function testFormatRoundingModeHalfEven($value, $expected)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
+
+ $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_HALFEVEN);
+ $this->assertSame($expected, $formatter->format($value), '->format() with ROUND_HALFEVEN rounding mode.');
+ }
+
+ public function formatRoundingModeRoundHalfEvenProvider()
+ {
+ return array(
+ array(1.121, '1.12'),
+ array(1.123, '1.12'),
+ array(1.125, '1.12'),
+ array(1.127, '1.13'),
+ array(1.129, '1.13'),
+ );
+ }
+
+ public function testGetLocale()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $this->assertEquals('en', $formatter->getLocale());
+ }
+
+ /**
+ * @dataProvider parseProvider
+ */
+ public function testParse($value, $expected, $message = '')
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $parsedValue = $formatter->parse($value, NumberFormatter::TYPE_DOUBLE);
+ $this->assertSame($expected, $parsedValue, $message);
+
+ if ($expected === false) {
+ $errorCode = IntlGlobals::U_PARSE_ERROR;
+ $errorMessage = 'Number parsing failed: U_PARSE_ERROR';
+ } else {
+ $errorCode = IntlGlobals::U_ZERO_ERROR;
+ $errorMessage = 'U_ZERO_ERROR';
+ }
+
+ $this->assertSame($errorMessage, $this->getIntlErrorMessage());
+ $this->assertSame($errorCode, $this->getIntlErrorCode());
+ $this->assertSame($errorCode !== 0, $this->isIntlFailure($this->getIntlErrorCode()));
+ $this->assertSame($errorMessage, $formatter->getErrorMessage());
+ $this->assertSame($errorCode, $formatter->getErrorCode());
+ $this->assertSame($errorCode !== 0, $this->isIntlFailure($formatter->getErrorCode()));
+ }
+
+ public function parseProvider()
+ {
+ return array(
+ array('prefix1', false, '->parse() does not parse a number with a string prefix.'),
+ array('1suffix', (float) 1, '->parse() parses a number with a string suffix.'),
+ );
+ }
+
+ /**
+ * @expectedException \PHPUnit_Framework_Error_Warning
+ */
+ public function testParseTypeDefault()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->parse('1', NumberFormatter::TYPE_DEFAULT);
+ }
+
+ /**
+ * @dataProvider parseTypeInt32Provider
+ */
+ public function testParseTypeInt32($value, $expected, $message = '')
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $parsedValue = $formatter->parse($value, NumberFormatter::TYPE_INT32);
+ $this->assertSame($expected, $parsedValue);
+ }
+
+ public function parseTypeInt32Provider()
+ {
+ return array(
+ array('1', 1),
+ array('1.1', 1),
+ array('2,147,483,647', 2147483647),
+ array('-2,147,483,648', -2147483647 - 1),
+ array('2,147,483,648', false, '->parse() TYPE_INT32 returns false when the number is greater than the integer positive range.'),
+ array('-2,147,483,649', false, '->parse() TYPE_INT32 returns false when the number is greater than the integer negative range.')
+ );
+ }
+
+ public function testParseTypeInt64With32BitIntegerInPhp32Bit()
+ {
+ IntlTestHelper::require32Bit($this);
+
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ $parsedValue = $formatter->parse('2,147,483,647', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('integer', $parsedValue);
+ $this->assertEquals(2147483647, $parsedValue);
+
+ $parsedValue = $formatter->parse('-2,147,483,648', NumberFormatter::TYPE_INT64);
+
+ // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
+ // The negative PHP_INT_MAX was being converted to float
+ if (
+ (version_compare(PHP_VERSION, '5.4.0', '<') && version_compare(PHP_VERSION, '5.3.14', '>=')) ||
+ version_compare(PHP_VERSION, '5.4.4', '>=')
+ ) {
+ $this->assertInternalType('int', $parsedValue);
+ } else {
+ $this->assertInternalType('float', $parsedValue);
+ }
+
+ $this->assertEquals(-2147483648, $parsedValue);
+ }
+
+ public function testParseTypeInt64With32BitIntegerInPhp64Bit()
+ {
+ IntlTestHelper::require64Bit($this);
+
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ $parsedValue = $formatter->parse('2,147,483,647', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('integer', $parsedValue);
+ $this->assertEquals(2147483647, $parsedValue);
+
+ $parsedValue = $formatter->parse('-2,147,483,648', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('integer', $parsedValue);
+ $this->assertEquals(-2147483647 - 1, $parsedValue);
+ }
+
+ /**
+ * If PHP is compiled in 32bit mode, the returned value for a 64bit integer are float numbers.
+ */
+ public function testParseTypeInt64With64BitIntegerInPhp32Bit()
+ {
+ IntlTestHelper::require32Bit($this);
+
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ // int 64 using only 32 bit range strangeness
+ $parsedValue = $formatter->parse('2,147,483,648', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('float', $parsedValue);
+ $this->assertEquals(2147483648, $parsedValue, '->parse() TYPE_INT64 does not use true 64 bit integers, using only the 32 bit range.');
+
+ $parsedValue = $formatter->parse('-2,147,483,649', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('float', $parsedValue);
+ $this->assertEquals(-2147483649, $parsedValue, '->parse() TYPE_INT64 does not use true 64 bit integers, using only the 32 bit range.');
+ }
+
+ /**
+ * If PHP is compiled in 64bit mode, the returned value for a 64bit integer are 32bit integer numbers.
+ */
+ public function testParseTypeInt64With64BitIntegerInPhp64Bit()
+ {
+ IntlTestHelper::require64Bit($this);
+
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+
+ $parsedValue = $formatter->parse('2,147,483,648', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('integer', $parsedValue);
+
+ // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
+ // A 32 bit integer was being generated instead of a 64 bit integer
+ if (
+ (version_compare(PHP_VERSION, '5.3.14', '<')) ||
+ (version_compare(PHP_VERSION, '5.4.0', '>=') && version_compare(PHP_VERSION, '5.4.4', '<'))
+ ) {
+ $this->assertEquals(-2147483648, $parsedValue, '->parse() TYPE_INT64 does not use true 64 bit integers, using only the 32 bit range (PHP < 5.3.14 and PHP < 5.4.4).');
+ } else {
+ $this->assertEquals(2147483648, $parsedValue, '->parse() TYPE_INT64 uses true 64 bit integers (PHP >= 5.3.14 and PHP >= 5.4.4).');
+ }
+
+ $parsedValue = $formatter->parse('-2,147,483,649', NumberFormatter::TYPE_INT64);
+ $this->assertInternalType('integer', $parsedValue);
+
+ // Bug #59597 was fixed on PHP 5.3.14 and 5.4.4
+ // A 32 bit integer was being generated instead of a 64 bit integer
+ if (
+ (version_compare(PHP_VERSION, '5.3.14', '<')) ||
+ (version_compare(PHP_VERSION, '5.4.0', '>=') && version_compare(PHP_VERSION, '5.4.4', '<'))
+ ) {
+ $this->assertEquals(2147483647, $parsedValue, '->parse() TYPE_INT64 does not use true 64 bit integers, using only the 32 bit range (PHP < 5.3.14 and PHP < 5.4.4).');
+ } else {
+ $this->assertEquals(-2147483649, $parsedValue, '->parse() TYPE_INT64 uses true 64 bit integers (PHP >= 5.3.14 and PHP >= 5.4.4).');
+ }
+ }
+
+ /**
+ * @dataProvider parseTypeDoubleProvider
+ */
+ public function testParseTypeDouble($value, $expectedValue)
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $parsedValue = $formatter->parse($value, NumberFormatter::TYPE_DOUBLE);
+ $this->assertSame($expectedValue, $parsedValue);
+ }
+
+ public function parseTypeDoubleProvider()
+ {
+ return array(
+ array('1', (float) 1),
+ array('1.1', 1.1),
+ array('9,223,372,036,854,775,808', 9223372036854775808),
+ array('-9,223,372,036,854,775,809', -9223372036854775809),
+ );
+ }
+
+ /**
+ * @expectedException \PHPUnit_Framework_Error_Warning
+ */
+ public function testParseTypeCurrency()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->parse('1', NumberFormatter::TYPE_CURRENCY);
+ }
+
+ public function testParseWithNullPositionValue()
+ {
+ $position = null;
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->parse('123', NumberFormatter::TYPE_INT32, $position);
+ $this->assertNull($position);
+ }
+
+ public function testParseWithNotNullPositionValue()
+ {
+ $position = 1;
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->parse('123', NumberFormatter::TYPE_DOUBLE, $position);
+ $this->assertEquals(3, $position);
+ }
+
+ /**
+ * @param string $locale
+ * @param null $style
+ * @param null $pattern
+ *
+ * @return \NumberFormatter
+ */
+ abstract protected function getNumberFormatter($locale = 'en', $style = null, $pattern = null);
+
+ /**
+ * @return string
+ */
+ abstract protected function getIntlErrorMessage();
+
+ /**
+ * @return integer
+ */
+ abstract protected function getIntlErrorCode();
+
+ /**
+ * @param integer $errorCode
+ *
+ * @return Boolean
+ */
+ abstract protected function isIntlFailure($errorCode);
+}
--- /dev/null
+<?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\Intl\Tests\NumberFormatter;
+
+use Symfony\Component\Intl\Globals\IntlGlobals;
+use Symfony\Component\Intl\NumberFormatter\NumberFormatter;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Note that there are some values written like -2147483647 - 1. This is the lower 32bit int max and is a known
+ * behavior of PHP.
+ */
+class NumberFormatterTest extends AbstractNumberFormatterTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireIntl($this);
+
+ parent::setUp();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testConstructorWithUnsupportedLocale()
+ {
+ new NumberFormatter('pt_BR');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testConstructorWithUnsupportedStyle()
+ {
+ new NumberFormatter('en', NumberFormatter::PATTERN_DECIMAL);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException
+ */
+ public function testConstructorWithPatternDifferentThanNull()
+ {
+ new NumberFormatter('en', NumberFormatter::DECIMAL, '');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testSetAttributeWithUnsupportedAttribute()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setAttribute(NumberFormatter::LENIENT_PARSE, null);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testSetAttributeInvalidRoundingMode()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, null);
+ }
+
+ public function testCreate()
+ {
+ $this->assertInstanceOf(
+ '\Symfony\Component\Intl\NumberFormatter\NumberFormatter',
+ NumberFormatter::create('en', NumberFormatter::DECIMAL)
+ );
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ */
+ public function testFormatWithCurrencyStyle()
+ {
+ parent::testFormatWithCurrencyStyle();
+ }
+
+ /**
+ * @dataProvider formatTypeInt32Provider
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testFormatTypeInt32($formatter, $value, $expected, $message = '')
+ {
+ parent::testFormatTypeInt32($formatter, $value, $expected, $message);
+ }
+
+ /**
+ * @dataProvider formatTypeInt32WithCurrencyStyleProvider
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatTypeInt32WithCurrencyStyle($formatter, $value, $expected, $message = '')
+ {
+ parent::testFormatTypeInt32WithCurrencyStyle($formatter, $value, $expected, $message);
+ }
+
+ /**
+ * @dataProvider formatTypeInt64Provider
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testFormatTypeInt64($formatter, $value, $expected)
+ {
+ parent::testFormatTypeInt64($formatter, $value, $expected);
+ }
+
+ /**
+ * @dataProvider formatTypeInt64WithCurrencyStyleProvider
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatTypeInt64WithCurrencyStyle($formatter, $value, $expected)
+ {
+ parent::testFormatTypeInt64WithCurrencyStyle($formatter, $value, $expected);
+ }
+
+ /**
+ * @dataProvider formatTypeDoubleProvider
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException
+ */
+ public function testFormatTypeDouble($formatter, $value, $expected)
+ {
+ parent::testFormatTypeDouble($formatter, $value, $expected);
+ }
+
+ /**
+ * @dataProvider formatTypeDoubleWithCurrencyStyleProvider
+ * @expectedException \Symfony\Component\Intl\Exception\NotImplementedException
+ */
+ public function testFormatTypeDoubleWithCurrencyStyle($formatter, $value, $expected)
+ {
+ parent::testFormatTypeDoubleWithCurrencyStyle($formatter, $value, $expected);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetPattern()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->getPattern();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetSymbol()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->getSymbol(null);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testGetTextAttribute()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->getTextAttribute(null);
+ }
+
+ public function testGetErrorCode()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $this->assertEquals(IntlGlobals::U_ZERO_ERROR, $formatter->getErrorCode());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testParseCurrency()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->parseCurrency(null, $currency);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException
+ */
+ public function testParseWithNotNullPositionValue()
+ {
+ parent::testParseWithNotNullPositionValue();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetPattern()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setPattern(null);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetSymbol()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setSymbol(null, null);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException
+ */
+ public function testSetTextAttribute()
+ {
+ $formatter = $this->getNumberFormatter('en', NumberFormatter::DECIMAL);
+ $formatter->setTextAttribute(null, null);
+ }
+
+ protected function getNumberFormatter($locale = 'en', $style = null, $pattern = null)
+ {
+ return new NumberFormatter($locale, $style, $pattern);
+ }
+
+ protected function getIntlErrorMessage()
+ {
+ return IntlGlobals::getErrorMessage();
+ }
+
+ protected function getIntlErrorCode()
+ {
+ return IntlGlobals::getErrorCode();
+ }
+
+ protected function isIntlFailure($errorCode)
+ {
+ return IntlGlobals::isFailure($errorCode);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\NumberFormatter\Verification;
+
+use Symfony\Component\Intl\Tests\NumberFormatter\AbstractNumberFormatterTest;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * Note that there are some values written like -2147483647 - 1. This is the lower 32bit int max and is a known
+ * behavior of PHP.
+ */
+class NumberFormatterTest extends AbstractNumberFormatterTest
+{
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ parent::setUp();
+ }
+
+ public function testCreate()
+ {
+ $this->assertInstanceOf('\NumberFormatter', \NumberFormatter::create('en', \NumberFormatter::DECIMAL));
+ }
+
+ protected function getNumberFormatter($locale = 'en', $style = null, $pattern = null)
+ {
+ return new \NumberFormatter($locale, $style, $pattern);
+ }
+
+ protected function getIntlErrorMessage()
+ {
+ return intl_get_error_message();
+ }
+
+ protected function getIntlErrorCode()
+ {
+ return intl_get_error_code();
+ }
+
+ protected function isIntlFailure($errorCode)
+ {
+ return intl_is_failure($errorCode);
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class AbstractBundleTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/base/dirName';
+
+ /**
+ * @var \Symfony\Component\Intl\ResourceBundle\AbstractBundle
+ */
+ private $bundle;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->bundle = $this->getMockForAbstractClass(
+ 'Symfony\Component\Intl\ResourceBundle\AbstractBundle',
+ array(self::RES_DIR, $this->reader)
+ );
+
+ $this->bundle->expects($this->any())
+ ->method('getDirectoryName')
+ ->will($this->returnValue('dirName'));
+ }
+
+ public function testGetLocales()
+ {
+ $locales = array('de', 'en', 'fr');
+
+ $this->reader->expects($this->once())
+ ->method('getLocales')
+ ->with(self::RES_DIR)
+ ->will($this->returnValue($locales));
+
+ $this->assertSame($locales, $this->bundle->getLocales());
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle;
+
+use Symfony\Component\Intl\ResourceBundle\CurrencyBundle;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class CurrencyBundleTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/base/curr';
+
+ /**
+ * @var CurrencyBundle
+ */
+ private $bundle;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->bundle = new CurrencyBundle(self::RES_DIR, $this->reader);
+ }
+
+ public function testGetCurrencySymbol()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 1))
+ ->will($this->returnValue('€'));
+
+ $this->assertSame('€', $this->bundle->getCurrencySymbol('EUR', 'en'));
+ }
+
+ public function testGetCurrencyName()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 0))
+ ->will($this->returnValue('Euro'));
+
+ $this->assertSame('Euro', $this->bundle->getCurrencyName('EUR', 'en'));
+ }
+
+ public function testGetCurrencyNames()
+ {
+ $sortedCurrencies = array(
+ 'USD' => array(0 => 'Dollar'),
+ 'EUR' => array(0 => 'Euro'),
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Currencies'))
+ ->will($this->returnValue($sortedCurrencies));
+
+ $sortedNames = array(
+ 'USD' => 'Dollar',
+ 'EUR' => 'Euro',
+ );
+
+ $this->assertSame($sortedNames, $this->bundle->getCurrencyNames('en'));
+ }
+
+ public function testGetFractionDigits()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 2))
+ ->will($this->returnValue(123));
+
+ $this->assertSame(123, $this->bundle->getFractionDigits('EUR'));
+ }
+
+ public function testGetRoundingIncrement()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 3))
+ ->will($this->returnValue(123));
+
+ $this->assertSame(123, $this->bundle->getRoundingIncrement('EUR'));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle;
+
+use Symfony\Component\Intl\ResourceBundle\LanguageBundle;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LanguageBundleTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/base/lang';
+
+ /**
+ * @var LanguageBundle
+ */
+ private $bundle;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->bundle = new LanguageBundle(self::RES_DIR, $this->reader);
+ }
+
+ public function testGetLanguageName()
+ {
+ $languages = array(
+ 'de' => 'German',
+ 'en' => 'English',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Languages'))
+ ->will($this->returnValue($languages));
+
+ $this->assertSame('German', $this->bundle->getLanguageName('de', null, 'en'));
+ }
+
+ public function testGetLanguageNameWithRegion()
+ {
+ $languages = array(
+ 'de' => 'German',
+ 'en' => 'English',
+ 'en_GB' => 'British English',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Languages'))
+ ->will($this->returnValue($languages));
+
+ $this->assertSame('British English', $this->bundle->getLanguageName('en', 'GB', 'en'));
+ }
+
+ public function testGetLanguageNameWithUntranslatedRegion()
+ {
+ $languages = array(
+ 'de' => 'German',
+ 'en' => 'English',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Languages'))
+ ->will($this->returnValue($languages));
+
+ $this->assertSame('English', $this->bundle->getLanguageName('en', 'US', 'en'));
+ }
+
+ public function testGetLanguageNames()
+ {
+ $sortedLanguages = array(
+ 'en' => 'English',
+ 'de' => 'German',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Languages'))
+ ->will($this->returnValue($sortedLanguages));
+
+ $this->assertSame($sortedLanguages, $this->bundle->getLanguageNames('en'));
+ }
+
+ public function testGetScriptName()
+ {
+ $data = array(
+ 'Languages' => array(
+ 'de' => 'German',
+ 'en' => 'English',
+ ),
+ 'Scripts' => array(
+ 'Latn' => 'latin',
+ 'Cyrl' => 'cyrillique',
+ ),
+ );
+
+ $this->reader->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertSame('latin', $this->bundle->getScriptName('Latn', null, 'en'));
+ }
+
+ public function testGetScriptNameIncludedInLanguage()
+ {
+ $data = array(
+ 'Languages' => array(
+ 'de' => 'German',
+ 'en' => 'English',
+ 'zh_Hans' => 'Simplified Chinese',
+ ),
+ 'Scripts' => array(
+ 'Latn' => 'latin',
+ 'Cyrl' => 'cyrillique',
+ ),
+ );
+
+ $this->reader->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ // Null because the script is included in the language anyway
+ $this->assertNull($this->bundle->getScriptName('Hans', 'zh', 'en'));
+ }
+
+ public function testGetScriptNameIncludedInLanguageInBraces()
+ {
+ $data = array(
+ 'Languages' => array(
+ 'de' => 'German',
+ 'en' => 'English',
+ 'zh_Hans' => 'Chinese (simplified)',
+ ),
+ 'Scripts' => array(
+ 'Latn' => 'latin',
+ 'Cyrl' => 'cyrillique',
+ ),
+ );
+
+ $this->reader->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertSame('simplified', $this->bundle->getScriptName('Hans', 'zh', 'en'));
+ }
+
+ public function testGetScriptNameNoScriptsBlock()
+ {
+ $data = array(
+ 'Languages' => array(
+ 'de' => 'German',
+ 'en' => 'English',
+ ),
+ );
+
+ $this->reader->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertNull($this->bundle->getScriptName('Latn', null, 'en'));
+ }
+
+ public function testGetScriptNames()
+ {
+ $sortedScripts = array(
+ 'Cyrl' => 'cyrillique',
+ 'Latn' => 'latin',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Scripts'))
+ ->will($this->returnValue($sortedScripts));
+
+ $this->assertSame($sortedScripts, $this->bundle->getScriptNames('en'));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle;
+
+use Symfony\Component\Intl\ResourceBundle\LocaleBundle;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class LocaleBundleTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/base/locales';
+
+ /**
+ * @var LocaleBundle
+ */
+ private $bundle;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->bundle = new LocaleBundle(self::RES_DIR, $this->reader);
+ }
+
+ public function testGetLocaleName()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Locales', 'de_AT'))
+ ->will($this->returnValue('German (Austria)'));
+
+ $this->assertSame('German (Austria)', $this->bundle->getLocaleName('de_AT', 'en'));
+ }
+
+ public function testGetLocaleNames()
+ {
+ $sortedLocales = array(
+ 'en_IE' => 'English (Ireland)',
+ 'en_GB' => 'English (United Kingdom)',
+ 'en_US' => 'English (United States)',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Locales'))
+ ->will($this->returnValue($sortedLocales));
+
+ $this->assertSame($sortedLocales, $this->bundle->getLocaleNames('en'));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Reader;
+
+use Symfony\Component\Filesystem\Filesystem;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class AbstractBundleReaderTest extends \PHPUnit_Framework_TestCase
+{
+ private $directory;
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->directory = sys_get_temp_dir() . '/AbstractBundleReaderTest/' . rand(1000, 9999);
+ $this->filesystem = new Filesystem();
+ $this->reader = $this->getMockForAbstractClass('Symfony\Component\Intl\ResourceBundle\Reader\AbstractBundleReader');
+
+ $this->filesystem->mkdir($this->directory);
+ }
+
+ protected function tearDown()
+ {
+ $this->filesystem->remove($this->directory);
+ }
+
+ public function testGetLocales()
+ {
+ $this->filesystem->touch($this->directory . '/en.foo');
+ $this->filesystem->touch($this->directory . '/de.foo');
+ $this->filesystem->touch($this->directory . '/fr.foo');
+ $this->filesystem->touch($this->directory . '/bo.txt');
+ $this->filesystem->touch($this->directory . '/gu.bin');
+ $this->filesystem->touch($this->directory . '/s.lol');
+
+ $this->reader->expects($this->any())
+ ->method('getFileExtension')
+ ->will($this->returnValue('foo'));
+
+ $sortedLocales = array('de', 'en', 'fr');
+
+ $this->assertSame($sortedLocales, $this->reader->getLocales($this->directory));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader;
+use Symfony\Component\Intl\Util\IntlTestHelper;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class BinaryBundleReaderTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var BinaryBundleReader
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ IntlTestHelper::requireFullIntl($this);
+
+ $this->reader = new BinaryBundleReader();
+ }
+
+ public function testReadReturnsArrayAccess()
+ {
+ $data = $this->reader->read(__DIR__ . '/Fixtures', 'en');
+
+ $this->assertInstanceOf('\ArrayAccess', $data);
+ $this->assertSame('Bar', $data['Foo']);
+ $this->assertFalse(isset($data['ExistsNot']));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\RuntimeException
+ */
+ public function testReadFailsIfNonExistingLocale()
+ {
+ $this->reader->read(__DIR__ . '/Fixtures', 'foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\RuntimeException
+ */
+ public function testReadFailsIfNonExistingDirectory()
+ {
+ $this->reader->read(__DIR__ . '/foo', 'en');
+ }
+}
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Foo' => 'Bar',
+);
--- /dev/null
+en{
+ Foo{"Bar"}
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\ResourceBundle\Reader\PhpBundleReader;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PhpBundleReaderTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var PhpBundleReader
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = new PhpBundleReader();
+ }
+
+ public function testReadReturnsArray()
+ {
+ $data = $this->reader->read(__DIR__ . '/Fixtures', 'en');
+
+ $this->assertTrue(is_array($data));
+ $this->assertSame('Bar', $data['Foo']);
+ $this->assertFalse(isset($data['ExistsNot']));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException
+ */
+ public function testReadFailsIfLocaleOtherThanEn()
+ {
+ $this->reader->read(__DIR__ . '/Fixtures', 'foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\RuntimeException
+ */
+ public function testReadFailsIfNonExistingDirectory()
+ {
+ $this->reader->read(__DIR__ . '/foo', 'en');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\RuntimeException
+ */
+ public function testReadFailsIfNotAFile()
+ {
+ $this->reader->read(__DIR__ . '/Fixtures/NotAFile', 'en');
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Reader;
+
+use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class StructuredBundleReaderTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/res/dir';
+
+ /**
+ * @var StructuredBundleReader
+ */
+ private $reader;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $readerImpl;
+
+ protected function setUp()
+ {
+ $this->readerImpl = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->reader = new StructuredBundleReader($this->readerImpl);
+ }
+
+ public function testGetLocales()
+ {
+ $locales = array('en', 'de', 'fr');
+
+ $this->readerImpl->expects($this->once())
+ ->method('getLocales')
+ ->with(self::RES_DIR)
+ ->will($this->returnValue($locales));
+
+ $this->assertSame($locales, $this->reader->getLocales(self::RES_DIR));
+ }
+
+ public function testRead()
+ {
+ $data = array('foo', 'bar');
+
+ $this->readerImpl->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertSame($data, $this->reader->read(self::RES_DIR, 'en'));
+ }
+
+ public function testReadEntryNoParams()
+ {
+ $data = array('foo', 'bar');
+
+ $this->readerImpl->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertSame($data, $this->reader->readEntry(self::RES_DIR, 'en', array()));
+ }
+
+ public function testReadEntryWithParam()
+ {
+ $data = array('Foo' => array('Bar' => 'Baz'));
+
+ $this->readerImpl->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertSame('Baz', $this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar')));
+ }
+
+ public function testReadEntryWithUnresolvablePath()
+ {
+ $data = array('Foo' => 'Baz');
+
+ $this->readerImpl->expects($this->once())
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($data));
+
+ $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar')));
+ }
+
+ public function readMergedEntryProvider()
+ {
+ return array(
+ array('foo', null, 'foo'),
+ array(null, 'foo', 'foo'),
+ array(array('foo', 'bar'), null, array('foo', 'bar')),
+ array(array('foo', 'bar'), array(), array('foo', 'bar')),
+ array(null, array('baz'), array('baz')),
+ array(array(), array('baz'), array('baz')),
+ array(array('foo', 'bar'), array('baz'), array('baz', 'foo', 'bar')),
+ );
+ }
+
+ /**
+ * @dataProvider readMergedEntryProvider
+ */
+ public function testReadMergedEntryNoParams($childData, $parentData, $result)
+ {
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue($childData));
+
+ if (null === $childData || is_array($childData)) {
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue($parentData));
+ }
+
+ $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array(), true));
+ }
+
+ /**
+ * @dataProvider readMergedEntryProvider
+ */
+ public function testReadMergedEntryWithParams($childData, $parentData, $result)
+ {
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue(array('Foo' => array('Bar' => $childData))));
+
+ if (null === $childData || is_array($childData)) {
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue(array('Foo' => array('Bar' => $parentData))));
+ }
+
+ $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true));
+ }
+
+ public function testReadMergedEntryWithUnresolvablePath()
+ {
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue(array('Foo' => 'Baz')));
+
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue(array('Foo' => 'Bar')));
+
+ $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true));
+ }
+
+ public function testReadMergedEntryWithUnresolvablePathInParent()
+ {
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue(array('Foo' => array('Bar' => array('three')))));
+
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue(array('Foo' => 'Bar')));
+
+ $result = array('three');
+
+ $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true));
+ }
+
+ public function testReadMergedEntryWithUnresolvablePathInChild()
+ {
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue(array('Foo' => 'Baz')));
+
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue(array('Foo' => array('Bar' => array('one', 'two')))));
+
+ $result = array('one', 'two');
+
+ $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true));
+ }
+
+ /**
+ * @dataProvider readMergedEntryProvider
+ */
+ public function testReadMergedEntryWithTraversables($childData, $parentData, $result)
+ {
+ $parentData = is_array($parentData) ? new \ArrayObject($parentData) : $parentData;
+ $childData = is_array($childData) ? new \ArrayObject($childData) : $childData;
+
+ $this->readerImpl->expects($this->at(0))
+ ->method('read')
+ ->with(self::RES_DIR, 'en_GB')
+ ->will($this->returnValue(array('Foo' => array('Bar' => $childData))));
+
+ if (null === $childData || $childData instanceof \ArrayObject) {
+ $this->readerImpl->expects($this->at(1))
+ ->method('read')
+ ->with(self::RES_DIR, 'en')
+ ->will($this->returnValue(array('Foo' => array('Bar' => $parentData))));
+ }
+
+ $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle;
+
+use Symfony\Component\Intl\ResourceBundle\RegionBundle;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RegionBundleTest extends \PHPUnit_Framework_TestCase
+{
+ const RES_DIR = '/base/region';
+
+ /**
+ * @var RegionBundle
+ */
+ private $bundle;
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $reader;
+
+ protected function setUp()
+ {
+ $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface');
+ $this->bundle = new RegionBundle(self::RES_DIR, $this->reader);
+ }
+
+ public function testGetCountryName()
+ {
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Countries', 'AT'))
+ ->will($this->returnValue('Austria'));
+
+ $this->assertSame('Austria', $this->bundle->getCountryName('AT', 'en'));
+ }
+
+ public function testGetCountryNames()
+ {
+ $sortedCountries = array(
+ 'AT' => 'Austria',
+ 'DE' => 'Germany',
+ );
+
+ $this->reader->expects($this->once())
+ ->method('readEntry')
+ ->with(self::RES_DIR, 'en', array('Countries'))
+ ->will($this->returnValue($sortedCountries));
+
+ $this->assertSame($sortedCountries, $this->bundle->getCountryNames('en'));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Util;
+
+use Symfony\Component\Intl\ResourceBundle\Util\RingBuffer;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RingBufferTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var RingBuffer
+ */
+ private $buffer;
+
+ protected function setUp()
+ {
+ $this->buffer = new RingBuffer(2);
+ }
+
+ public function testWriteWithinBuffer()
+ {
+ $this->buffer[0] = 'foo';
+ $this->buffer['bar'] = 'baz';
+
+ $this->assertTrue(isset($this->buffer[0]));
+ $this->assertTrue(isset($this->buffer['bar']));
+ $this->assertSame('foo', $this->buffer[0]);
+ $this->assertSame('baz', $this->buffer['bar']);
+ }
+
+ public function testWritePastBuffer()
+ {
+ $this->buffer[0] = 'foo';
+ $this->buffer['bar'] = 'baz';
+ $this->buffer[2] = 'bam';
+
+ $this->assertTrue(isset($this->buffer['bar']));
+ $this->assertTrue(isset($this->buffer[2]));
+ $this->assertSame('baz', $this->buffer['bar']);
+ $this->assertSame('bam', $this->buffer[2]);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\OutOfBoundsException
+ */
+ public function testReadNonExistingFails()
+ {
+ $this->buffer['foo'];
+ }
+
+ public function testQueryNonExisting()
+ {
+ $this->assertFalse(isset($this->buffer['foo']));
+ }
+
+ public function testUnsetNonExistingSucceeds()
+ {
+ unset($this->buffer['foo']);
+
+ $this->assertFalse(isset($this->buffer['foo']));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Intl\Exception\OutOfBoundsException
+ */
+ public function testReadOverwrittenFails()
+ {
+ $this->buffer[0] = 'foo';
+ $this->buffer['bar'] = 'baz';
+ $this->buffer[2] = 'bam';
+
+ $this->buffer[0];
+ }
+
+ public function testQueryOverwritten()
+ {
+ $this->assertFalse(isset($this->buffer[0]));
+ }
+
+ public function testUnsetOverwrittenSucceeds()
+ {
+ $this->buffer[0] = 'foo';
+ $this->buffer['bar'] = 'baz';
+ $this->buffer[2] = 'bam';
+
+ unset($this->buffer[0]);
+
+ $this->assertFalse(isset($this->buffer[0]));
+ }
+}
--- /dev/null
+<?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.
+ */
+
+return array(
+ 'Entry1' => array(
+ 'Array' => array(
+ 0 => 'foo',
+ 1 => 'bar',
+ ),
+ 'Integer' => 5,
+ 'Boolean' => false,
+ 'Float' => 1.23,
+ ),
+ 'Entry2' => 'String',
+);
--- /dev/null
+en{
+ Entry1{
+ Array{
+ "foo",
+ "bar",
+ {
+ Key{"value"}
+ },
+ }
+ Integer:int{5}
+ IntVector:intvector{
+ 0,
+ 1,
+ 2,
+ 3,
+ }
+ FalseBoolean{"false"}
+ TrueBoolean{"true"}
+ Null{""}
+ Float{"1.23"}
+ }
+ Entry2{"String"}
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Writer;
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Intl\ResourceBundle\Writer\PhpBundleWriter;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PhpBundleWriterTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var PhpBundleWriter
+ */
+ private $writer;
+
+ private $directory;
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ protected function setUp()
+ {
+ $this->writer = new PhpBundleWriter();
+ $this->directory = sys_get_temp_dir() . '/PhpBundleWriterTest/' . rand(1000, 9999);
+ $this->filesystem = new Filesystem();
+
+ $this->filesystem->mkdir($this->directory);
+ }
+
+ protected function tearDown()
+ {
+ $this->filesystem->remove($this->directory);
+ }
+
+ public function testWrite()
+ {
+ $this->writer->write($this->directory, 'en', array(
+ 'Entry1' => array(
+ 'Array' => array('foo', 'bar'),
+ 'Integer' => 5,
+ 'Boolean' => false,
+ 'Float' => 1.23,
+ ),
+ 'Entry2' => 'String',
+ ));
+
+ $this->assertFileEquals(__DIR__ . '/Fixtures/en.php', $this->directory . '/en.php');
+ }
+}
--- /dev/null
+<?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\Intl\Tests\ResourceBundle\Writer;
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt
+ */
+class TextBundleWriterTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var TextBundleWriter
+ */
+ private $writer;
+
+ private $directory;
+
+ /**
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ protected function setUp()
+ {
+ $this->writer = new TextBundleWriter();
+ $this->directory = sys_get_temp_dir() . '/TextBundleWriterTest/' . rand(1000, 9999);
+ $this->filesystem = new Filesystem();
+
+ $this->filesystem->mkdir($this->directory);
+ }
+
+ protected function tearDown()
+ {
+ $this->filesystem->remove($this->directory);
+ }
+
+ public function testWrite()
+ {
+ $this->writer->write($this->directory, 'en', array(
+ 'Entry1' => array(
+ 'Array' => array('foo', 'bar', array('Key' => 'value')),
+ 'Integer' => 5,
+ 'IntVector' => array(0, 1, 2, 3),
+ 'FalseBoolean' => false,
+ 'TrueBoolean' => true,
+ 'Null' => null,
+ 'Float' => 1.23,
+ ),
+ 'Entry2' => 'String',
+ ));
+
+ $this->assertFileEquals(__DIR__ . '/Fixtures/en.txt', $this->directory . '/en.txt');
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Util;
+
+use Symfony\Component\Intl\Util\IcuVersion;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuVersionTest extends \PHPUnit_Framework_TestCase
+{
+ public function normalizeProvider()
+ {
+ return array(
+ array(null, '1', '10'),
+ array(null, '1.2', '12'),
+ array(null, '1.2.3', '12.3'),
+ array(null, '1.2.3.4', '12.3.4'),
+ array(1, '1', '10'),
+ array(1, '1.2', '12'),
+ array(1, '1.2.3', '12'),
+ array(1, '1.2.3.4', '12'),
+ array(2, '1', '10'),
+ array(2, '1.2', '12'),
+ array(2, '1.2.3', '12.3'),
+ array(2, '1.2.3.4', '12.3'),
+ array(3, '1', '10'),
+ array(3, '1.2', '12'),
+ array(3, '1.2.3', '12.3'),
+ array(3, '1.2.3.4', '12.3.4'),
+ );
+ }
+
+ /**
+ * @dataProvider normalizeProvider
+ */
+ public function testNormalize($precision, $version, $result)
+ {
+ $this->assertSame($result, IcuVersion::normalize($version, $precision));
+ }
+
+ public function compareProvider()
+ {
+ return array(
+ array(null, '1', '==', '1', true),
+ array(null, '1.0', '==', '1.1', false),
+ array(null, '1.0.0', '==', '1.0.1', false),
+ array(null, '1.0.0.0', '==', '1.0.0.1', false),
+ array(null, '1.0.0.0.0', '==', '1.0.0.0.1', false),
+
+ array(null, '1', '==', '10', true),
+ array(null, '1.0', '==', '11', false),
+ array(null, '1.0.0', '==', '10.1', false),
+ array(null, '1.0.0.0', '==', '10.0.1', false),
+ array(null, '1.0.0.0.0', '==', '10.0.0.1', false),
+
+ array(1, '1', '==', '1', true),
+ array(1, '1.0', '==', '1.1', false),
+ array(1, '1.0.0', '==', '1.0.1', true),
+ array(1, '1.0.0.0', '==', '1.0.0.1', true),
+ array(1, '1.0.0.0.0', '==', '1.0.0.0.1', true),
+
+ array(1, '1', '==', '10', true),
+ array(1, '1.0', '==', '11', false),
+ array(1, '1.0.0', '==', '10.1', true),
+ array(1, '1.0.0.0', '==', '10.0.1', true),
+ array(1, '1.0.0.0.0', '==', '10.0.0.1', true),
+
+ array(2, '1', '==', '1', true),
+ array(2, '1.0', '==', '1.1', false),
+ array(2, '1.0.0', '==', '1.0.1', false),
+ array(2, '1.0.0.0', '==', '1.0.0.1', true),
+ array(2, '1.0.0.0.0', '==', '1.0.0.0.1', true),
+
+ array(2, '1', '==', '10', true),
+ array(2, '1.0', '==', '11', false),
+ array(2, '1.0.0', '==', '10.1', false),
+ array(2, '1.0.0.0', '==', '10.0.1', true),
+ array(2, '1.0.0.0.0', '==', '10.0.0.1', true),
+
+ array(3, '1', '==', '1', true),
+ array(3, '1.0', '==', '1.1', false),
+ array(3, '1.0.0', '==', '1.0.1', false),
+ array(3, '1.0.0.0', '==', '1.0.0.1', false),
+ array(3, '1.0.0.0.0', '==', '1.0.0.0.1', true),
+
+ array(3, '1', '==', '10', true),
+ array(3, '1.0', '==', '11', false),
+ array(3, '1.0.0', '==', '10.1', false),
+ array(3, '1.0.0.0', '==', '10.0.1', false),
+ array(3, '1.0.0.0.0', '==', '10.0.0.1', true),
+ );
+ }
+
+ /**
+ * @dataProvider compareProvider
+ */
+ public function testCompare($precision, $version1, $operator, $version2, $result)
+ {
+ $this->assertSame($result, IcuVersion::compare($version1, $version2, $operator, $precision));
+ }
+}
--- /dev/null
+<?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\Intl\Tests\Util;
+
+use Symfony\Component\Intl\Util\Version;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class VersionTest extends \PHPUnit_Framework_TestCase
+{
+ public function normalizeProvider()
+ {
+ return array(
+ array(null, '1', '1'),
+ array(null, '1.2', '1.2'),
+ array(null, '1.2.3', '1.2.3'),
+ array(null, '1.2.3.4', '1.2.3.4'),
+ array(1, '1', '1'),
+ array(1, '1.2', '1'),
+ array(1, '1.2.3', '1'),
+ array(1, '1.2.3.4', '1'),
+ array(2, '1', '1'),
+ array(2, '1.2', '1.2'),
+ array(2, '1.2.3', '1.2'),
+ array(2, '1.2.3.4', '1.2'),
+ array(3, '1', '1'),
+ array(3, '1.2', '1.2'),
+ array(3, '1.2.3', '1.2.3'),
+ array(3, '1.2.3.4', '1.2.3'),
+ array(4, '1', '1'),
+ array(4, '1.2', '1.2'),
+ array(4, '1.2.3', '1.2.3'),
+ array(4, '1.2.3.4', '1.2.3.4'),
+ );
+ }
+
+ /**
+ * @dataProvider normalizeProvider
+ */
+ public function testNormalize($precision, $version, $result)
+ {
+ $this->assertSame($result, Version::normalize($version, $precision));
+ }
+
+ public function compareProvider()
+ {
+ return array(
+ array(null, '1', '==', '1', true),
+ array(null, '1.0', '==', '1.1', false),
+ array(null, '1.0.0', '==', '1.0.1', false),
+ array(null, '1.0.0.0', '==', '1.0.0.1', false),
+
+ array(1, '1', '==', '1', true),
+ array(1, '1.0', '==', '1.1', true),
+ array(1, '1.0.0', '==', '1.0.1', true),
+ array(1, '1.0.0.0', '==', '1.0.0.1', true),
+
+ array(2, '1', '==', '1', true),
+ array(2, '1.0', '==', '1.1', false),
+ array(2, '1.0.0', '==', '1.0.1', true),
+ array(2, '1.0.0.0', '==', '1.0.0.1', true),
+
+ array(3, '1', '==', '1', true),
+ array(3, '1.0', '==', '1.1', false),
+ array(3, '1.0.0', '==', '1.0.1', false),
+ array(3, '1.0.0.0', '==', '1.0.0.1', true),
+ );
+ }
+
+ /**
+ * @dataProvider compareProvider
+ */
+ public function testCompare($precision, $version1, $operator, $version2, $result)
+ {
+ $this->assertSame($result, Version::compare($version1, $version2, $operator, $precision));
+ }
+}
--- /dev/null
+<?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\Intl\Util;
+
+/**
+ * Facilitates the comparison of ICU version strings.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IcuVersion
+{
+ /**
+ * Compares two ICU versions with an operator.
+ *
+ * This method is identical to {@link version_compare()}, except that you
+ * can pass the number of regarded version components in the last argument
+ * $precision.
+ *
+ * Also, a single digit release version and a single digit major version
+ * are contracted to a two digit release version. If no major version
+ * is given, it is substituted by zero.
+ *
+ * Examples:
+ *
+ * IcuVersion::compare('1.2.3', '1.2.4', '==')
+ * // => false
+ *
+ * IcuVersion::compare('1.2.3', '1.2.4', '==', 2)
+ * // => true
+ *
+ * IcuVersion::compare('1.2.3', '12.3', '==')
+ * // => true
+ *
+ * IcuVersion::compare('1', '10', '==')
+ * // => true
+ *
+ * @param string $version1 A version string.
+ * @param string $version2 A version string to compare.
+ * @param string $operator The comparison operator.
+ * @param integer|null $precision The number of components to compare. Pass
+ * NULL to compare the versions unchanged.
+ *
+ * @return Boolean Whether the comparison succeeded.
+ *
+ * @see normalize()
+ */
+ public static function compare($version1, $version2, $operator, $precision = null)
+ {
+ $version1 = self::normalize($version1, $precision);
+ $version2 = self::normalize($version2, $precision);
+
+ return version_compare($version1, $version2, $operator);
+ }
+
+ /**
+ * Normalizes a version string to the number of components given in the
+ * parameter $precision.
+ *
+ * A single digit release version and a single digit major version are
+ * contracted to a two digit release version. If no major version is given,
+ * it is substituted by zero.
+ *
+ * Examples:
+ *
+ * IcuVersion::normalize('1.2.3.4');
+ * // => '12.3.4'
+ *
+ * IcuVersion::normalize('1.2.3.4', 1);
+ * // => '12'
+ *
+ * IcuVersion::normalize('1.2.3.4', 2);
+ * // => '12.3'
+ *
+ * @param string $version An ICU version string.
+ * @param integer|null $precision The number of components to include. Pass
+ * NULL to return the version unchanged.
+ *
+ * @return string|null The normalized ICU version or NULL if it couldn't be
+ * normalized.
+ */
+ public static function normalize($version, $precision)
+ {
+ $version = preg_replace('/^(\d)\.(\d)/', '$1$2', $version);
+
+ if (1 === strlen($version)) {
+ $version .= '0';
+ }
+
+ return Version::normalize($version, $precision);
+ }
+
+ /**
+ * Must not be instantiated.
+ */
+ private function __construct() {}
+}
--- /dev/null
+<?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\Intl\Util;
+
+use Symfony\Component\Intl\Intl;
+
+/**
+ * Helper class for preparing test cases that rely on the Intl component.
+ *
+ * Any test that tests functionality relying on either the intl classes or
+ * the resource bundle data should call either of the methods
+ * {@link requireIntl()} or {@link requireFullIntl()}. Calling
+ * {@link requireFullIntl()} is only necessary if you use functionality in the
+ * test that is not provided by the stub intl implementation.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class IntlTestHelper
+{
+ /**
+ * Should be called before tests that work fine with the stub implementation.
+ *
+ * @param \PhpUnit_Framework_TestCase $testCase
+ */
+ public static function requireIntl(\PhpUnit_Framework_TestCase $testCase)
+ {
+ // We only run tests if the version is *one specific version*.
+ // This condition is satisfied if
+ //
+ // * the intl extension is loaded with version Intl::getIcuStubVersion()
+ // * the intl extension is not loaded
+
+ if (IcuVersion::compare(Intl::getIcuVersion(), Intl::getIcuStubVersion(), '!=', 1)) {
+ $testCase->markTestSkipped('Please change ICU version to ' . Intl::getIcuStubVersion());
+ }
+
+ if (IcuVersion::compare(Intl::getIcuDataVersion(), Intl::getIcuStubVersion(), '!=', 1)) {
+ $testCase->markTestSkipped('Please change the Icu component to version 1.0.x or 1.' . IcuVersion::normalize(Intl::getIcuStubVersion(), 1) . '.x');
+ }
+
+ // Normalize the default locale in case this is not done explicitly
+ // in the test
+ \Locale::setDefault('en');
+
+ // Consequently, tests will
+ //
+ // * run only for one ICU version (see Intl::getIcuStubVersion())
+ // there is no need to add control structures to your tests that
+ // change the test depending on the ICU version.
+ //
+ // Tests should only rely on functionality that is implemented in the
+ // stub classes.
+ }
+
+ /**
+ * Should be called before tests that require a feature-complete intl
+ * implementation.
+ *
+ * @param \PhpUnit_Framework_TestCase $testCase
+ */
+ public static function requireFullIntl(\PhpUnit_Framework_TestCase $testCase)
+ {
+ // We only run tests if the intl extension is loaded...
+ if (!Intl::isExtensionLoaded()) {
+ $testCase->markTestSkipped('The intl extension is not available.');
+ }
+
+ // ... and only if the version is *one specific version* ...
+ if (IcuVersion::compare(Intl::getIcuVersion(), Intl::getIcuStubVersion(), '!=', 1)) {
+ $testCase->markTestSkipped('Please change ICU version to ' . Intl::getIcuStubVersion());
+ }
+
+ // ... and only if the data in the Icu component matches that version.
+ if (IcuVersion::compare(Intl::getIcuDataVersion(), Intl::getIcuStubVersion(), '!=', 1)) {
+ $testCase->markTestSkipped('Please change the Icu component to version 1.0.x or 1.' . IcuVersion::normalize(Intl::getIcuStubVersion(), 1) . '.x');
+ }
+
+ // Normalize the default locale in case this is not done explicitly
+ // in the test
+ \Locale::setDefault('en');
+
+ // Consequently, tests will
+ //
+ // * run only for one ICU version (see Intl::getIcuStubVersion())
+ // there is no need to add control structures to your tests that
+ // change the test depending on the ICU version.
+ // * always use the C intl classes
+ // * always use the binary resource bundles (any locale is allowed)
+ }
+
+ /**
+ * Skips the test unless the current system has a 32bit architecture.
+ *
+ * @param \PhpUnit_Framework_TestCase $testCase
+ */
+ public static function require32Bit(\PhpUnit_Framework_TestCase $testCase)
+ {
+ if (4 !== PHP_INT_SIZE) {
+ $testCase->markTestSkipped('PHP must be compiled in 32 bit mode to run this test');
+ }
+ }
+
+ /**
+ * Skips the test unless the current system has a 64bit architecture.
+ *
+ * @param \PhpUnit_Framework_TestCase $testCase
+ */
+ public static function require64Bit(\PhpUnit_Framework_TestCase $testCase)
+ {
+ if (8 !== PHP_INT_SIZE) {
+ $testCase->markTestSkipped('PHP must be compiled in 64 bit mode to run this test');
+ }
+ }
+
+ /**
+ * Must not be instantiated.
+ */
+ private function __construct() {}
+}
--- /dev/null
+<?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\Intl\Util;
+
+/**
+ * An SVN commit.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SvnCommit
+{
+ /**
+ * @var \SimpleXMLElement
+ */
+ private $svnInfo;
+
+ /**
+ * Creates a commit from the given "svn info" data.
+ *
+ * @param \SimpleXMLElement $svnInfo The XML result from the "svn info"
+ * command.
+ */
+ public function __construct(\SimpleXMLElement $svnInfo)
+ {
+ $this->svnInfo = $svnInfo;
+ }
+
+ /**
+ * Returns the revision of the commit.
+ *
+ * @return string The revision of the commit.
+ */
+ public function getRevision()
+ {
+ return (string) $this->svnInfo['revision'];
+ }
+
+ /**
+ * Returns the author of the commit.
+ *
+ * @return string The author name.
+ */
+ public function getAuthor()
+ {
+ return (string) $this->svnInfo->author;
+ }
+
+ /**
+ * Returns the date of the commit.
+ *
+ * @return string The commit date.
+ */
+ public function getDate()
+ {
+ return (string) $this->svnInfo->date;
+ }
+}
--- /dev/null
+<?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\Intl\Util;
+
+use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Intl\Exception\RuntimeException;
+
+/**
+ * A SVN repository containing ICU data.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SvnRepository
+{
+ /**
+ * @var string The path to the repository.
+ */
+ private $path;
+
+ /**
+ * @var \SimpleXMLElement
+ */
+ private $svnInfo;
+
+ /**
+ * @var SvnCommit
+ */
+ private $lastCommit;
+
+ /**
+ * Downloads the ICU data for the given version.
+ *
+ * @param string $url The URL to download from.
+ * @param string $targetDir The directory in which to store the repository.
+ *
+ * @return SvnRepository The directory where the data is stored.
+ *
+ * @throws RuntimeException If an error occurs during the download.
+ */
+ public static function download($url, $targetDir)
+ {
+ exec('which svn', $output, $result);
+
+ if ($result !== 0) {
+ throw new RuntimeException('The command "svn" is not installed.');
+ }
+
+ $filesystem = new Filesystem();
+
+ if (!$filesystem->exists($targetDir . '/.svn')) {
+ $filesystem->remove($targetDir);
+ $filesystem->mkdir($targetDir);
+
+ exec('svn checkout ' . $url . ' ' . $targetDir, $output, $result);
+
+ if ($result !== 0) {
+ throw new RuntimeException('The SVN checkout of ' . $url . 'failed.');
+ }
+ }
+
+ return new static(realpath($targetDir));
+ }
+
+ /**
+ * Reads the SVN repository at the given path.
+ *
+ * @param string $path The path to the repository.
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Returns the path to the repository.
+ *
+ * @return string The path to the repository.
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Returns the URL of the repository.
+ *
+ * @return string The URL of the repository.
+ */
+ public function getUrl()
+ {
+ return (string) $this->getSvnInfo()->entry->url;
+ }
+
+ /**
+ * Returns the last commit of the repository.
+ *
+ * @return SvnCommit The last commit.
+ */
+ public function getLastCommit()
+ {
+ if (null === $this->lastCommit) {
+ $this->lastCommit = new SvnCommit($this->getSvnInfo()->entry->commit);
+ }
+
+ return $this->lastCommit;
+ }
+
+ /**
+ * Returns information about the SVN repository.
+ *
+ * @return \SimpleXMLElement The XML result from the "svn info" command.
+ *
+ * @throws RuntimeException If the "svn info" command failed.
+ */
+ private function getSvnInfo()
+ {
+ if (null === $this->svnInfo) {
+ exec('svn info --xml '.$this->path, $output, $result);
+
+ $svnInfo = simplexml_load_string(implode("\n", $output));
+
+ if ($result !== 0) {
+ throw new RuntimeException('svn info failed');
+ }
+
+ $this->svnInfo = $svnInfo;
+ }
+
+ return $this->svnInfo;
+ }
+
+}
--- /dev/null
+<?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\Intl\Util;
+
+/**
+ * Facilitates the comparison of version strings.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Version
+{
+ /**
+ * Compares two versions with an operator.
+ *
+ * This method is identical to {@link version_compare()}, except that you
+ * can pass the number of regarded version components in the last argument
+ * $precision.
+ *
+ * Examples:
+ *
+ * Version::compare('1.2.3', '1.2.4', '==')
+ * // => false
+ *
+ * Version::compare('1.2.3', '1.2.4', '==', 2)
+ * // => true
+ *
+ * @param string $version1 A version string.
+ * @param string $version2 A version string to compare.
+ * @param string $operator The comparison operator.
+ * @param integer|null $precision The number of components to compare. Pass
+ * NULL to compare the versions unchanged.
+ *
+ * @return Boolean Whether the comparison succeeded.
+ *
+ * @see normalize()
+ */
+ public static function compare($version1, $version2, $operator, $precision = null)
+ {
+ $version1 = self::normalize($version1, $precision);
+ $version2 = self::normalize($version2, $precision);
+
+ return version_compare($version1, $version2, $operator);
+ }
+
+ /**
+ * Normalizes a version string to the number of components given in the
+ * parameter $precision.
+ *
+ * Examples:
+ *
+ * Version::normalize('1.2.3', 1);
+ * // => '1'
+ *
+ * Version::normalize('1.2.3', 2);
+ * // => '1.2'
+ *
+ * @param string $version A version string.
+ * @param integer|null $precision The number of components to include. Pass
+ * NULL to return the version unchanged.
+ *
+ * @return string|null The normalized version or NULL if it couldn't be
+ * normalized.
+ */
+ public static function normalize($version, $precision)
+ {
+ if (null === $precision) {
+ return $version;
+ }
+
+ $pattern = '[^\.]+';
+
+ for ($i = 2; $i <= $precision; ++$i) {
+ $pattern = sprintf('[^\.]+(\.%s)?', $pattern);
+ }
+
+ if (!preg_match('/^' . $pattern . '/', $version, $matches)) {
+ return null;
+ }
+
+ return $matches[0];
+ }
+
+ /**
+ * Must not be instantiated.
+ */
+ private function __construct() {}
+}
--- /dev/null
+{
+ "name": "symfony/intl",
+ "type": "library",
+ "description": "A PHP replacement layer for the C intl extension that includes additional data from the ICU library.",
+ "keywords": ["intl", "icu", "internationalization", "localization", "i18n", "l10n"],
+ "homepage": "http://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ },
+ {
+ "name": "Eriksen Costa",
+ "email": "eriksen.costa@infranology.com.br"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/icu": "~1.0-RC"
+ },
+ "require-dev": {
+ "symfony/filesystem": ">=2.1"
+ },
+ "suggest": {
+ "ext-intl": "to use the component with locales other than \"en\""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Intl\\": "" },
+ "classmap": [ "Symfony/Component/Intl/Resources/stubs" ],
+ "files": [ "Symfony/Component/Intl/Resources/stubs/functions.php" ]
+ },
+ "target-dir": "Symfony/Component/Intl",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Intl Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Tests</directory>
+ <directory>./vendor</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+<?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\OptionsResolver\Exception;
+
+/**
+ * Marker interface for the Options component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\OptionsResolver\Exception;
+
+/**
+ * Exception thrown when an invalid option is passed.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class InvalidOptionsException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\OptionsResolver\Exception;
+
+/**
+ * Exception thrown when a required option is missing.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class MissingOptionsException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\OptionsResolver\Exception;
+
+/**
+ * Thrown when an option definition is invalid.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class OptionDefinitionException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+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.
--- /dev/null
+<?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\OptionsResolver;
+
+use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
+
+/**
+ * Container for resolving inter-dependent options.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class Options implements \ArrayAccess, \Iterator, \Countable
+{
+ /**
+ * A list of option values.
+ * @var array
+ */
+ private $options = array();
+
+ /**
+ * A list of normalizer closures.
+ * @var array
+ */
+ private $normalizers = array();
+
+ /**
+ * A list of closures for evaluating lazy options.
+ * @var array
+ */
+ private $lazy = array();
+
+ /**
+ * A list containing the currently locked options.
+ * @var array
+ */
+ private $lock = array();
+
+ /**
+ * Whether at least one option has already been read.
+ *
+ * Once read, the options cannot be changed anymore. This is
+ * necessary in order to avoid inconsistencies during the resolving
+ * process. If any option is changed after being read, all evaluated
+ * lazy options that depend on this option would become invalid.
+ *
+ * @var Boolean
+ */
+ private $reading = false;
+
+ /**
+ * Sets the value of a given option.
+ *
+ * You can set lazy options by passing a closure with the following
+ * signature:
+ *
+ * <code>
+ * function (Options $options)
+ * </code>
+ *
+ * This closure will be evaluated once the option is read using
+ * {@link get()}. The closure has access to the resolved values of
+ * other options through the passed {@link Options} instance.
+ *
+ * @param string $option The name of the option.
+ * @param mixed $value The value of the option.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function set($option, $value)
+ {
+ // Setting is not possible once an option is read, because then lazy
+ // options could manipulate the state of the object, leading to
+ // inconsistent results.
+ if ($this->reading) {
+ throw new OptionDefinitionException('Options cannot be set anymore once options have been read.');
+ }
+
+ // Setting is equivalent to overloading while discarding the previous
+ // option value
+ unset($this->options[$option]);
+ unset($this->lazy[$option]);
+
+ $this->overload($option, $value);
+ }
+
+ /**
+ * Sets the normalizer for a given option.
+ *
+ * Normalizers should be closures with the following signature:
+ *
+ * <code>
+ * function (Options $options, $value)
+ * </code>
+ *
+ * This closure will be evaluated once the option is read using
+ * {@link get()}. The closure has access to the resolved values of
+ * other options through the passed {@link Options} instance.
+ *
+ * @param string $option The name of the option.
+ * @param \Closure $normalizer The normalizer.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function setNormalizer($option, \Closure $normalizer)
+ {
+ if ($this->reading) {
+ throw new OptionDefinitionException('Normalizers cannot be added anymore once options have been read.');
+ }
+
+ $this->normalizers[$option] = $normalizer;
+ }
+
+ /**
+ * Replaces the contents of the container with the given options.
+ *
+ * This method is a shortcut for {@link clear()} with subsequent
+ * calls to {@link set()}.
+ *
+ * @param array $options The options to set.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function replace(array $options)
+ {
+ if ($this->reading) {
+ throw new OptionDefinitionException('Options cannot be replaced anymore once options have been read.');
+ }
+
+ $this->options = array();
+ $this->lazy = array();
+ $this->normalizers = array();
+
+ foreach ($options as $option => $value) {
+ $this->overload($option, $value);
+ }
+ }
+
+ /**
+ * Overloads the value of a given option.
+ *
+ * Contrary to {@link set()}, this method keeps the previous default
+ * value of the option so that you can access it if you pass a closure.
+ * Passed closures should have the following signature:
+ *
+ * <code>
+ * function (Options $options, $value)
+ * </code>
+ *
+ * The second parameter passed to the closure is the current default
+ * value of the option.
+ *
+ * @param string $option The option name.
+ * @param mixed $value The option value.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function overload($option, $value)
+ {
+ if ($this->reading) {
+ throw new OptionDefinitionException('Options cannot be overloaded anymore once options have been read.');
+ }
+
+ // If an option is a closure that should be evaluated lazily, store it
+ // in the "lazy" property.
+ if ($value instanceof \Closure) {
+ $reflClosure = new \ReflectionFunction($value);
+ $params = $reflClosure->getParameters();
+
+ if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && __CLASS__ === $class->name) {
+ // Initialize the option if no previous value exists
+ if (!isset($this->options[$option])) {
+ $this->options[$option] = null;
+ }
+
+ // Ignore previous lazy options if the closure has no second parameter
+ if (!isset($this->lazy[$option]) || !isset($params[1])) {
+ $this->lazy[$option] = array();
+ }
+
+ // Store closure for later evaluation
+ $this->lazy[$option][] = $value;
+
+ return;
+ }
+ }
+
+ // Remove lazy options by default
+ unset($this->lazy[$option]);
+
+ $this->options[$option] = $value;
+ }
+
+ /**
+ * Returns the value of the given option.
+ *
+ * If the option was a lazy option, it is evaluated now.
+ *
+ * @param string $option The option name.
+ *
+ * @return mixed The option value.
+ *
+ * @throws \OutOfBoundsException If the option does not exist.
+ * @throws OptionDefinitionException If a cyclic dependency is detected
+ * between two lazy options.
+ */
+ public function get($option)
+ {
+ $this->reading = true;
+
+ if (!array_key_exists($option, $this->options)) {
+ throw new \OutOfBoundsException(sprintf('The option "%s" does not exist.', $option));
+ }
+
+ if (isset($this->lazy[$option])) {
+ $this->resolve($option);
+ }
+
+ if (isset($this->normalizers[$option])) {
+ $this->normalize($option);
+ }
+
+ return $this->options[$option];
+ }
+
+ /**
+ * Returns whether the given option exists.
+ *
+ * @param string $option The option name.
+ *
+ * @return Boolean Whether the option exists.
+ */
+ public function has($option)
+ {
+ return array_key_exists($option, $this->options);
+ }
+
+ /**
+ * Removes the option with the given name.
+ *
+ * @param string $option The option name.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function remove($option)
+ {
+ if ($this->reading) {
+ throw new OptionDefinitionException('Options cannot be removed anymore once options have been read.');
+ }
+
+ unset($this->options[$option]);
+ unset($this->lazy[$option]);
+ unset($this->normalizers[$option]);
+ }
+
+ /**
+ * Removes all options.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ */
+ public function clear()
+ {
+ if ($this->reading) {
+ throw new OptionDefinitionException('Options cannot be cleared anymore once options have been read.');
+ }
+
+ $this->options = array();
+ $this->lazy = array();
+ $this->normalizers = array();
+ }
+
+ /**
+ * Returns the values of all options.
+ *
+ * Lazy options are evaluated at this point.
+ *
+ * @return array The option values.
+ */
+ public function all()
+ {
+ $this->reading = true;
+
+ // Performance-wise this is slightly better than
+ // while (null !== $option = key($this->lazy))
+ foreach ($this->lazy as $option => $closures) {
+ // Double check, in case the option has already been resolved
+ // by cascade in the previous cycles
+ if (isset($this->lazy[$option])) {
+ $this->resolve($option);
+ }
+ }
+
+ foreach ($this->normalizers as $option => $normalizer) {
+ if (isset($this->normalizers[$option])) {
+ $this->normalize($option);
+ }
+ }
+
+ return $this->options;
+ }
+
+ /**
+ * Equivalent to {@link has()}.
+ *
+ * @param string $option The option name.
+ *
+ * @return Boolean Whether the option exists.
+ *
+ * @see \ArrayAccess::offsetExists()
+ */
+ public function offsetExists($option)
+ {
+ return $this->has($option);
+ }
+
+ /**
+ * Equivalent to {@link get()}.
+ *
+ * @param string $option The option name.
+ *
+ * @return mixed The option value.
+ *
+ * @throws \OutOfBoundsException If the option does not exist.
+ * @throws OptionDefinitionException If a cyclic dependency is detected
+ * between two lazy options.
+ *
+ * @see \ArrayAccess::offsetGet()
+ */
+ public function offsetGet($option)
+ {
+ return $this->get($option);
+ }
+
+ /**
+ * Equivalent to {@link set()}.
+ *
+ * @param string $option The name of the option.
+ * @param mixed $value The value of the option. May be a closure with a
+ * signature as defined in DefaultOptions::add().
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ *
+ * @see \ArrayAccess::offsetSet()
+ */
+ public function offsetSet($option, $value)
+ {
+ $this->set($option, $value);
+ }
+
+ /**
+ * Equivalent to {@link remove()}.
+ *
+ * @param string $option The option name.
+ *
+ * @throws OptionDefinitionException If options have already been read.
+ * Once options are read, the container
+ * becomes immutable.
+ *
+ * @see \ArrayAccess::offsetUnset()
+ */
+ public function offsetUnset($option)
+ {
+ $this->remove($option);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function current()
+ {
+ return $this->get($this->key());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function next()
+ {
+ next($this->options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function key()
+ {
+ return key($this->options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function valid()
+ {
+ return null !== $this->key();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rewind()
+ {
+ reset($this->options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count()
+ {
+ return count($this->options);
+ }
+
+ /**
+ * Evaluates the given lazy option.
+ *
+ * The evaluated value is written into the options array. The closure for
+ * evaluating the option is discarded afterwards.
+ *
+ * @param string $option The option to evaluate.
+ *
+ * @throws OptionDefinitionException If the option has a cyclic dependency
+ * on another option.
+ */
+ private function resolve($option)
+ {
+ // The code duplication with normalize() exists for performance
+ // reasons, in order to save a method call.
+ // Remember that this method is potentially called a couple of thousand
+ // times and needs to be as efficient as possible.
+ if (isset($this->lock[$option])) {
+ $conflicts = array();
+
+ foreach ($this->lock as $option => $locked) {
+ if ($locked) {
+ $conflicts[] = $option;
+ }
+ }
+
+ throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts)));
+ }
+
+ $this->lock[$option] = true;
+ foreach ($this->lazy[$option] as $closure) {
+ $this->options[$option] = $closure($this, $this->options[$option]);
+ }
+ unset($this->lock[$option]);
+
+ // The option now isn't lazy anymore
+ unset($this->lazy[$option]);
+ }
+
+ /**
+ * Normalizes the given option.
+ *
+ * The evaluated value is written into the options array.
+ *
+ * @param string $option The option to normalizer.
+ *
+ * @throws OptionDefinitionException If the option has a cyclic dependency
+ * on another option.
+ */
+ private function normalize($option)
+ {
+ // The code duplication with resolve() exists for performance
+ // reasons, in order to save a method call.
+ // Remember that this method is potentially called a couple of thousand
+ // times and needs to be as efficient as possible.
+ if (isset($this->lock[$option])) {
+ $conflicts = array();
+
+ foreach ($this->lock as $option => $locked) {
+ if ($locked) {
+ $conflicts[] = $option;
+ }
+ }
+
+ throw new OptionDefinitionException(sprintf('The options "%s" have a cyclic dependency.', implode('", "', $conflicts)));
+ }
+
+ /** @var \Closure $normalizer */
+ $normalizer = $this->normalizers[$option];
+
+ $this->lock[$option] = true;
+ $this->options[$option] = $normalizer($this, array_key_exists($option, $this->options) ? $this->options[$option] : null);
+ unset($this->lock[$option]);
+
+ // The option is now normalized
+ unset($this->normalizers[$option]);
+ }
+}
--- /dev/null
+<?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\OptionsResolver;
+
+use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException;
+use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
+use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
+
+/**
+ * Helper for merging default and concrete option values.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class OptionsResolver implements OptionsResolverInterface
+{
+ /**
+ * The default option values.
+ * @var Options
+ */
+ private $defaultOptions;
+
+ /**
+ * The options known by the resolver.
+ * @var array
+ */
+ private $knownOptions = array();
+
+ /**
+ * The options without defaults that are required to be passed to resolve().
+ * @var array
+ */
+ private $requiredOptions = array();
+
+ /**
+ * A list of accepted values for each option.
+ * @var array
+ */
+ private $allowedValues = array();
+
+ /**
+ * A list of accepted types for each option.
+ * @var array
+ */
+ private $allowedTypes = array();
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct()
+ {
+ $this->defaultOptions = new Options();
+ }
+
+ /**
+ * Clones the resolver.
+ */
+ public function __clone()
+ {
+ $this->defaultOptions = clone $this->defaultOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDefaults(array $defaultValues)
+ {
+ foreach ($defaultValues as $option => $value) {
+ $this->defaultOptions->overload($option, $value);
+ $this->knownOptions[$option] = true;
+ unset($this->requiredOptions[$option]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function replaceDefaults(array $defaultValues)
+ {
+ foreach ($defaultValues as $option => $value) {
+ $this->defaultOptions->set($option, $value);
+ $this->knownOptions[$option] = true;
+ unset($this->requiredOptions[$option]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOptional(array $optionNames)
+ {
+ foreach ($optionNames as $key => $option) {
+ if (!is_int($key)) {
+ throw new OptionDefinitionException('You should not pass default values to setOptional()');
+ }
+
+ $this->knownOptions[$option] = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRequired(array $optionNames)
+ {
+ foreach ($optionNames as $key => $option) {
+ if (!is_int($key)) {
+ throw new OptionDefinitionException('You should not pass default values to setRequired()');
+ }
+
+ $this->knownOptions[$option] = true;
+ // set as required if no default has been set already
+ if (!isset($this->defaultOptions[$option])) {
+ $this->requiredOptions[$option] = true;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAllowedValues(array $allowedValues)
+ {
+ $this->validateOptionsExistence($allowedValues);
+
+ $this->allowedValues = array_replace($this->allowedValues, $allowedValues);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addAllowedValues(array $allowedValues)
+ {
+ $this->validateOptionsExistence($allowedValues);
+
+ $this->allowedValues = array_merge_recursive($this->allowedValues, $allowedValues);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setAllowedTypes(array $allowedTypes)
+ {
+ $this->validateOptionsExistence($allowedTypes);
+
+ $this->allowedTypes = array_replace($this->allowedTypes, $allowedTypes);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addAllowedTypes(array $allowedTypes)
+ {
+ $this->validateOptionsExistence($allowedTypes);
+
+ $this->allowedTypes = array_merge_recursive($this->allowedTypes, $allowedTypes);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setNormalizers(array $normalizers)
+ {
+ $this->validateOptionsExistence($normalizers);
+
+ foreach ($normalizers as $option => $normalizer) {
+ $this->defaultOptions->setNormalizer($option, $normalizer);
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isKnown($option)
+ {
+ return isset($this->knownOptions[$option]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRequired($option)
+ {
+ return isset($this->requiredOptions[$option]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function resolve(array $options = array())
+ {
+ $this->validateOptionsExistence($options);
+ $this->validateOptionsCompleteness($options);
+
+ // Make sure this method can be called multiple times
+ $combinedOptions = clone $this->defaultOptions;
+
+ // Override options set by the user
+ foreach ($options as $option => $value) {
+ $combinedOptions->set($option, $value);
+ }
+
+ // Resolve options
+ $resolvedOptions = $combinedOptions->all();
+
+ $this->validateOptionValues($resolvedOptions);
+ $this->validateOptionTypes($resolvedOptions);
+
+ return $resolvedOptions;
+ }
+
+ /**
+ * Validates that the given option names exist and throws an exception
+ * otherwise.
+ *
+ * @param array $options An list of option names as keys.
+ *
+ * @throws InvalidOptionsException If any of the options has not been defined.
+ */
+ private function validateOptionsExistence(array $options)
+ {
+ $diff = array_diff_key($options, $this->knownOptions);
+
+ if (count($diff) > 0) {
+ ksort($this->knownOptions);
+ ksort($diff);
+
+ throw new InvalidOptionsException(sprintf(
+ (count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Known options are: "%s"',
+ implode('", "', array_keys($diff)),
+ implode('", "', array_keys($this->knownOptions))
+ ));
+ }
+ }
+
+ /**
+ * Validates that all required options are given and throws an exception
+ * otherwise.
+ *
+ * @param array $options An list of option names as keys.
+ *
+ * @throws MissingOptionsException If a required option is missing.
+ */
+ private function validateOptionsCompleteness(array $options)
+ {
+ $diff = array_diff_key($this->requiredOptions, $options);
+
+ if (count($diff) > 0) {
+ ksort($diff);
+
+ throw new MissingOptionsException(sprintf(
+ count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.',
+ implode('", "', array_keys($diff))
+ ));
+ }
+ }
+
+ /**
+ * Validates that the given option values match the allowed values and
+ * throws an exception otherwise.
+ *
+ * @param array $options A list of option values.
+ *
+ * @throws InvalidOptionsException If any of the values does not match the
+ * allowed values of the option.
+ */
+ private function validateOptionValues(array $options)
+ {
+ foreach ($this->allowedValues as $option => $allowedValues) {
+ if (isset($options[$option]) && !in_array($options[$option], $allowedValues, true)) {
+ throw new InvalidOptionsException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
+ }
+ }
+ }
+
+ /**
+ * Validates that the given options match the allowed types and
+ * throws an exception otherwise.
+ *
+ * @param array $options A list of options.
+ *
+ * @throws InvalidOptionsException If any of the types does not match the
+ * allowed types of the option.
+ */
+ private function validateOptionTypes(array $options)
+ {
+ foreach ($this->allowedTypes as $option => $allowedTypes) {
+ if (!array_key_exists($option, $options)) {
+ continue;
+ }
+
+ $value = $options[$option];
+ $allowedTypes = (array) $allowedTypes;
+
+ foreach ($allowedTypes as $type) {
+ $isFunction = 'is_'.$type;
+
+ if (function_exists($isFunction) && $isFunction($value)) {
+ continue 2;
+ } elseif ($value instanceof $type) {
+ continue 2;
+ }
+ }
+
+ $printableValue = is_object($value)
+ ? get_class($value)
+ : (is_array($value)
+ ? 'Array'
+ : (string) $value);
+
+ throw new InvalidOptionsException(sprintf(
+ 'The option "%s" with value "%s" is expected to be of type "%s"',
+ $option,
+ $printableValue,
+ implode('", "', $allowedTypes)
+ ));
+ }
+ }
+}
--- /dev/null
+<?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\OptionsResolver;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface OptionsResolverInterface
+{
+ /**
+ * Sets default option values.
+ *
+ * The options can either be values of any types or closures that
+ * evaluate the option value lazily. These closures must have one
+ * of the following signatures:
+ *
+ * <code>
+ * function (Options $options)
+ * function (Options $options, $value)
+ * </code>
+ *
+ * The second parameter passed to the closure is the previously
+ * set default value, in case you are overwriting an existing
+ * default value.
+ *
+ * The closures should return the lazily created option value.
+ *
+ * @param array $defaultValues A list of option names as keys and default
+ * values or closures as values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ */
+ public function setDefaults(array $defaultValues);
+
+ /**
+ * Replaces default option values.
+ *
+ * Old defaults are erased, which means that closures passed here cannot
+ * access the previous default value. This may be useful to improve
+ * performance if the previous default value is calculated by an expensive
+ * closure.
+ *
+ * @param array $defaultValues A list of option names as keys and default
+ * values or closures as values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ */
+ public function replaceDefaults(array $defaultValues);
+
+ /**
+ * Sets optional options.
+ *
+ * This method declares valid option names without setting default values for them.
+ * If these options are not passed to {@link resolve()} and no default has been set
+ * for them, they will be missing in the final options array. This can be helpful
+ * if you want to determine whether an option has been set or not because otherwise
+ * {@link resolve()} would trigger an exception for unknown options.
+ *
+ * @param array $optionNames A list of option names.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\OptionDefinitionException When trying to pass default values.
+ */
+ public function setOptional(array $optionNames);
+
+ /**
+ * Sets required options.
+ *
+ * If these options are not passed to {@link resolve()} and no default has been set for
+ * them, an exception will be thrown.
+ *
+ * @param array $optionNames A list of option names.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\OptionDefinitionException When trying to pass default values.
+ */
+ public function setRequired(array $optionNames);
+
+ /**
+ * Sets allowed values for a list of options.
+ *
+ * @param array $allowedValues A list of option names as keys and arrays
+ * with values acceptable for that option as
+ * values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\InvalidOptionsException If an option has not been defined
+ * (see {@link isKnown()}) for which
+ * an allowed value is set.
+ */
+ public function setAllowedValues(array $allowedValues);
+
+ /**
+ * Adds allowed values for a list of options.
+ *
+ * The values are merged with the allowed values defined previously.
+ *
+ * @param array $allowedValues A list of option names as keys and arrays
+ * with values acceptable for that option as
+ * values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\InvalidOptionsException If an option has not been defined
+ * (see {@link isKnown()}) for which
+ * an allowed value is set.
+ */
+ public function addAllowedValues(array $allowedValues);
+
+ /**
+ * Sets allowed types for a list of options.
+ *
+ * @param array $allowedTypes A list of option names as keys and type
+ * names passed as string or array as values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\InvalidOptionsException If an option has not been defined for
+ * which an allowed type is set.
+ */
+ public function setAllowedTypes(array $allowedTypes);
+
+ /**
+ * Adds allowed types for a list of options.
+ *
+ * The types are merged with the allowed types defined previously.
+ *
+ * @param array $allowedTypes A list of option names as keys and type
+ * names passed as string or array as values.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ *
+ * @throws Exception\InvalidOptionsException If an option has not been defined for
+ * which an allowed type is set.
+ */
+ public function addAllowedTypes(array $allowedTypes);
+
+ /**
+ * Sets normalizers that are applied on resolved options.
+ *
+ * The normalizers should be closures with the following signature:
+ *
+ * <code>
+ * function (Options $options, $value)
+ * </code>
+ *
+ * The second parameter passed to the closure is the value of
+ * the option.
+ *
+ * The closure should return the normalized value.
+ *
+ * @param array $normalizers An array of closures.
+ *
+ * @return OptionsResolverInterface The resolver instance.
+ */
+ public function setNormalizers(array $normalizers);
+
+ /**
+ * Returns whether an option is known.
+ *
+ * An option is known if it has been passed to either {@link setDefaults()},
+ * {@link setRequired()} or {@link setOptional()} before.
+ *
+ * @param string $option The name of the option.
+ *
+ * @return Boolean Whether the option is known.
+ */
+ public function isKnown($option);
+
+ /**
+ * Returns whether an option is required.
+ *
+ * An option is required if it has been passed to {@link setRequired()},
+ * but not to {@link setDefaults()}. That is, the option has been declared
+ * as required and no default value has been set.
+ *
+ * @param string $option The name of the option.
+ *
+ * @return Boolean Whether the option is required.
+ */
+ public function isRequired($option);
+
+ /**
+ * Returns the combination of the default and the passed options.
+ *
+ * @param array $options The custom option values.
+ *
+ * @return array A list of options and their values.
+ *
+ * @throws Exception\InvalidOptionsException If any of the passed options has not
+ * been defined or does not contain an
+ * allowed value.
+ * @throws Exception\MissingOptionsException If a required option is missing.
+ * @throws Exception\OptionDefinitionException If a cyclic dependency is detected
+ * between two lazy options.
+ */
+ public function resolve(array $options = array());
+}
--- /dev/null
+OptionsResolver Component
+=========================
+
+OptionsResolver helps at configuring objects with option arrays.
+
+It supports default values on different levels of your class hierarchy,
+option constraints (required vs. optional, allowed values) and lazy options
+whose default value depends on the value of another option.
+
+The following example demonstrates a Person class with two required options
+"firstName" and "lastName" and two optional options "age" and "gender", where
+the default value of "gender" is derived from the passed first name, if
+possible, and may only be one of "male" and "female".
+
+ use Symfony\Component\OptionsResolver\OptionsResolver;
+ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
+ use Symfony\Component\OptionsResolver\Options;
+
+ class Person
+ {
+ protected $options;
+
+ public function __construct(array $options = array())
+ {
+ $resolver = new OptionsResolver();
+ $this->setDefaultOptions($resolver);
+
+ $this->options = $resolver->resolve($options);
+ }
+
+ protected function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setRequired(array(
+ 'firstName',
+ 'lastName',
+ ));
+
+ $resolver->setDefaults(array(
+ 'age' => null,
+ 'gender' => function (Options $options) {
+ if (self::isKnownMaleName($options['firstName'])) {
+ return 'male';
+ }
+
+ return 'female';
+ },
+ ));
+
+ $resolver->setAllowedValues(array(
+ 'gender' => array('male', 'female'),
+ ));
+ }
+ }
+
+We can now easily instantiate a Person object:
+
+ // 'gender' is implicitly set to 'female'
+ $person = new Person(array(
+ 'firstName' => 'Jane',
+ 'lastName' => 'Doe',
+ ));
+
+We can also override the default values of the optional options:
+
+ $person = new Person(array(
+ 'firstName' => 'Abdullah',
+ 'lastName' => 'Mogashi',
+ 'gender' => 'male',
+ 'age' => 30,
+ ));
+
+Options can be added or changed in subclasses by overriding the `setDefaultOptions`
+method:
+
+ use Symfony\Component\OptionsResolver\OptionsResolver;
+ use Symfony\Component\OptionsResolver\Options;
+
+ class Employee extends Person
+ {
+ protected function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ parent::setDefaultOptions($resolver);
+
+ $resolver->setRequired(array(
+ 'birthDate',
+ ));
+
+ $resolver->setDefaults(array(
+ // $previousValue contains the default value configured in the
+ // parent class
+ 'age' => function (Options $options, $previousValue) {
+ return self::calculateAge($options['birthDate']);
+ }
+ ));
+ }
+ }
+
+
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/OptionsResolver/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\OptionsResolver\Tests;
+
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\OptionsResolver\Options;
+
+class OptionsResolverTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var OptionsResolver
+ */
+ private $resolver;
+
+ protected function setUp()
+ {
+ $this->resolver = new OptionsResolver();
+ }
+
+ public function testResolve()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => '2',
+ ));
+
+ $options = array(
+ 'two' => '20',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ 'two' => '20',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveLazy()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => function (Options $options) {
+ return '20';
+ },
+ ));
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ 'two' => '20',
+ ), $this->resolver->resolve(array()));
+ }
+
+ public function testResolveLazyDependencyOnOptional()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => function (Options $options) {
+ return $options['one'].'2';
+ },
+ ));
+
+ $options = array(
+ 'one' => '10',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '10',
+ 'two' => '102',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveLazyDependencyOnMissingOptionalWithoutDefault()
+ {
+ $test = $this;
+
+ $this->resolver->setOptional(array(
+ 'one',
+ ));
+
+ $this->resolver->setDefaults(array(
+ 'two' => function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertFalse(isset($options['one']));
+
+ return '2';
+ },
+ ));
+
+ $options = array(
+ );
+
+ $this->assertEquals(array(
+ 'two' => '2',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveLazyDependencyOnOptionalWithoutDefault()
+ {
+ $test = $this;
+
+ $this->resolver->setOptional(array(
+ 'one',
+ ));
+
+ $this->resolver->setDefaults(array(
+ 'two' => function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertTrue(isset($options['one']));
+
+ return $options['one'].'2';
+ },
+ ));
+
+ $options = array(
+ 'one' => '10',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '10',
+ 'two' => '102',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveLazyDependencyOnRequired()
+ {
+ $this->resolver->setRequired(array(
+ 'one',
+ ));
+ $this->resolver->setDefaults(array(
+ 'two' => function (Options $options) {
+ return $options['one'].'2';
+ },
+ ));
+
+ $options = array(
+ 'one' => '10',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '10',
+ 'two' => '102',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveLazyReplaceDefaults()
+ {
+ $test = $this;
+
+ $this->resolver->setDefaults(array(
+ 'one' => function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->fail('Previous closure should not be executed');
+ },
+ ));
+
+ $this->resolver->replaceDefaults(array(
+ 'one' => function (Options $options, $previousValue) {
+ return '1';
+ },
+ ));
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ ), $this->resolver->resolve(array()));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testResolveFailsIfNonExistingOption()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setRequired(array(
+ 'two',
+ ));
+
+ $this->resolver->setOptional(array(
+ 'three',
+ ));
+
+ $this->resolver->resolve(array(
+ 'foo' => 'bar',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
+ */
+ public function testResolveFailsIfMissingRequiredOption()
+ {
+ $this->resolver->setRequired(array(
+ 'one',
+ ));
+
+ $this->resolver->setDefaults(array(
+ 'two' => '2',
+ ));
+
+ $this->resolver->resolve(array(
+ 'two' => '20',
+ ));
+ }
+
+ public function testResolveSucceedsIfOptionValueAllowed()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedValues(array(
+ 'one' => array('1', 'one'),
+ ));
+
+ $options = array(
+ 'one' => 'one',
+ );
+
+ $this->assertEquals(array(
+ 'one' => 'one',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionValueAllowed2()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => '2',
+ ));
+
+ $this->resolver->setAllowedValues(array(
+ 'one' => '1',
+ 'two' => '2',
+ ));
+ $this->resolver->addAllowedValues(array(
+ 'one' => 'one',
+ 'two' => 'two',
+ ));
+
+ $options = array(
+ 'one' => '1',
+ 'two' => 'two',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ 'two' => 'two',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionalWithAllowedValuesNotSet()
+ {
+ $this->resolver->setRequired(array(
+ 'one',
+ ));
+
+ $this->resolver->setOptional(array(
+ 'two',
+ ));
+
+ $this->resolver->setAllowedValues(array(
+ 'one' => array('1', 'one'),
+ 'two' => array('2', 'two'),
+ ));
+
+ $options = array(
+ 'one' => '1',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ ), $this->resolver->resolve($options));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testResolveFailsIfOptionValueNotAllowed()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedValues(array(
+ 'one' => array('1', 'one'),
+ ));
+
+ $this->resolver->resolve(array(
+ 'one' => '2',
+ ));
+ }
+
+ public function testResolveSucceedsIfOptionTypeAllowed()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'string',
+ ));
+
+ $options = array(
+ 'one' => 'one',
+ );
+
+ $this->assertEquals(array(
+ 'one' => 'one',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionTypeAllowedPassArray()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => array('string', 'bool'),
+ ));
+
+ $options = array(
+ 'one' => true,
+ );
+
+ $this->assertEquals(array(
+ 'one' => true,
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionTypeAllowedPassObject()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'object',
+ ));
+
+ $object = new \stdClass();
+ $options = array(
+ 'one' => $object,
+ );
+
+ $this->assertEquals(array(
+ 'one' => $object,
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionTypeAllowedPassClass()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => '\stdClass',
+ ));
+
+ $object = new \stdClass();
+ $options = array(
+ 'one' => $object,
+ );
+
+ $this->assertEquals(array(
+ 'one' => $object,
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionTypeAllowedAddTypes()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => '2',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'string',
+ 'two' => 'bool',
+ ));
+ $this->resolver->addAllowedTypes(array(
+ 'one' => 'float',
+ 'two' => 'integer',
+ ));
+
+ $options = array(
+ 'one' => 1.23,
+ 'two' => false,
+ );
+
+ $this->assertEquals(array(
+ 'one' => 1.23,
+ 'two' => false,
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testResolveSucceedsIfOptionalWithTypeAndWithoutValue()
+ {
+ $this->resolver->setOptional(array(
+ 'one',
+ 'two',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'string',
+ 'two' => 'int',
+ ));
+
+ $options = array(
+ 'two' => 1,
+ );
+
+ $this->assertEquals(array(
+ 'two' => 1,
+ ), $this->resolver->resolve($options));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testResolveFailsIfOptionTypeNotAllowed()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => array('string', 'bool'),
+ ));
+
+ $this->resolver->resolve(array(
+ 'one' => 1.23,
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ 'two' => '2',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'string',
+ 'two' => 'bool',
+ ));
+
+ $this->resolver->resolve(array(
+ 'one' => 'foo',
+ 'two' => 1.23,
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
+ */
+ public function testResolveFailsIfOptionTypeNotAllowedAddTypes()
+ {
+ $this->resolver->setDefaults(array(
+ 'one' => '1',
+ ));
+
+ $this->resolver->setAllowedTypes(array(
+ 'one' => 'string',
+ ));
+ $this->resolver->addAllowedTypes(array(
+ 'one' => 'bool',
+ ));
+
+ $this->resolver->resolve(array(
+ 'one' => 1.23,
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testSetRequiredFailsIfDefaultIsPassed()
+ {
+ $this->resolver->setRequired(array(
+ 'one' => '1',
+ ));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testSetOptionalFailsIfDefaultIsPassed()
+ {
+ $this->resolver->setOptional(array(
+ 'one' => '1',
+ ));
+ }
+
+ public function testFluidInterface()
+ {
+ $this->resolver->setDefaults(array('one' => '1'))
+ ->replaceDefaults(array('one' => '2'))
+ ->setAllowedValues(array('one' => array('1', '2')))
+ ->addAllowedValues(array('one' => array('3')))
+ ->setRequired(array('two'))
+ ->setOptional(array('three'));
+
+ $options = array(
+ 'two' => '2',
+ );
+
+ $this->assertEquals(array(
+ 'one' => '2',
+ 'two' => '2',
+ ), $this->resolver->resolve($options));
+ }
+
+ public function testKnownIfDefaultWasSet()
+ {
+ $this->assertFalse($this->resolver->isKnown('foo'));
+
+ $this->resolver->setDefaults(array(
+ 'foo' => 'bar',
+ ));
+
+ $this->assertTrue($this->resolver->isKnown('foo'));
+ }
+
+ public function testKnownIfRequired()
+ {
+ $this->assertFalse($this->resolver->isKnown('foo'));
+
+ $this->resolver->setRequired(array(
+ 'foo',
+ ));
+
+ $this->assertTrue($this->resolver->isKnown('foo'));
+ }
+
+ public function testKnownIfOptional()
+ {
+ $this->assertFalse($this->resolver->isKnown('foo'));
+
+ $this->resolver->setOptional(array(
+ 'foo',
+ ));
+
+ $this->assertTrue($this->resolver->isKnown('foo'));
+ }
+
+ public function testRequiredIfRequired()
+ {
+ $this->assertFalse($this->resolver->isRequired('foo'));
+
+ $this->resolver->setRequired(array(
+ 'foo',
+ ));
+
+ $this->assertTrue($this->resolver->isRequired('foo'));
+ }
+
+ public function testNotRequiredIfRequiredAndDefaultValue()
+ {
+ $this->assertFalse($this->resolver->isRequired('foo'));
+
+ $this->resolver->setRequired(array(
+ 'foo',
+ ));
+ $this->resolver->setDefaults(array(
+ 'foo' => 'bar',
+ ));
+
+ $this->assertFalse($this->resolver->isRequired('foo'));
+ }
+
+ public function testNormalizersTransformFinalOptions()
+ {
+ $this->resolver->setDefaults(array(
+ 'foo' => 'bar',
+ 'bam' => 'baz',
+ ));
+ $this->resolver->setNormalizers(array(
+ 'foo' => function (Options $options, $value) {
+ return $options['bam'].'['.$value.']';
+ },
+ ));
+
+ $expected = array(
+ 'foo' => 'baz[bar]',
+ 'bam' => 'baz',
+ );
+
+ $this->assertEquals($expected, $this->resolver->resolve(array()));
+
+ $expected = array(
+ 'foo' => 'boo[custom]',
+ 'bam' => 'boo',
+ );
+
+ $this->assertEquals($expected, $this->resolver->resolve(array(
+ 'foo' => 'custom',
+ 'bam' => 'boo',
+ )));
+ }
+
+ public function testResolveWithoutOptionSucceedsIfRequiredAndDefaultValue()
+ {
+ $this->resolver->setRequired(array(
+ 'foo',
+ ));
+ $this->resolver->setDefaults(array(
+ 'foo' => 'bar',
+ ));
+
+ $this->assertEquals(array(
+ 'foo' => 'bar'
+ ), $this->resolver->resolve(array()));
+ }
+
+ public function testResolveWithoutOptionSucceedsIfDefaultValueAndRequired()
+ {
+ $this->resolver->setDefaults(array(
+ 'foo' => 'bar',
+ ));
+ $this->resolver->setRequired(array(
+ 'foo',
+ ));
+
+ $this->assertEquals(array(
+ 'foo' => 'bar'
+ ), $this->resolver->resolve(array()));
+ }
+
+ public function testResolveSucceedsIfOptionRequiredAndValueAllowed()
+ {
+ $this->resolver->setRequired(array(
+ 'one', 'two',
+ ));
+ $this->resolver->setAllowedValues(array(
+ 'two' => array('2'),
+ ));
+
+ $options = array(
+ 'one' => '1',
+ 'two' => '2'
+ );
+
+ $this->assertEquals($options, $this->resolver->resolve($options));
+ }
+
+ public function testClone()
+ {
+ $this->resolver->setDefaults(array('one' => '1'));
+
+ $clone = clone $this->resolver;
+
+ // Changes after cloning don't affect each other
+ $this->resolver->setDefaults(array('two' => '2'));
+ $clone->setDefaults(array('three' => '3'));
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ 'two' => '2',
+ ), $this->resolver->resolve());
+
+ $this->assertEquals(array(
+ 'one' => '1',
+ 'three' => '3',
+ ), $clone->resolve());
+ }
+}
--- /dev/null
+<?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\OptionsResolver\Tests;
+
+use Symfony\Component\OptionsResolver\Options;
+
+class OptionsTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Options
+ */
+ private $options;
+
+ protected function setUp()
+ {
+ $this->options = new Options();
+ }
+
+ public function testArrayAccess()
+ {
+ $this->assertFalse(isset($this->options['foo']));
+ $this->assertFalse(isset($this->options['bar']));
+
+ $this->options['foo'] = 0;
+ $this->options['bar'] = 1;
+
+ $this->assertTrue(isset($this->options['foo']));
+ $this->assertTrue(isset($this->options['bar']));
+
+ unset($this->options['bar']);
+
+ $this->assertTrue(isset($this->options['foo']));
+ $this->assertFalse(isset($this->options['bar']));
+ $this->assertEquals(0, $this->options['foo']);
+ }
+
+ public function testCountable()
+ {
+ $this->options->set('foo', 0);
+ $this->options->set('bar', 1);
+
+ $this->assertCount(2, $this->options);
+ }
+
+ /**
+ * @expectedException \OutOfBoundsException
+ */
+ public function testGetNonExisting()
+ {
+ $this->options->get('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testSetNotSupportedAfterGet()
+ {
+ $this->options->set('foo', 'bar');
+ $this->options->get('foo');
+ $this->options->set('foo', 'baz');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testRemoveNotSupportedAfterGet()
+ {
+ $this->options->set('foo', 'bar');
+ $this->options->get('foo');
+ $this->options->remove('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testSetNormalizerNotSupportedAfterGet()
+ {
+ $this->options->set('foo', 'bar');
+ $this->options->get('foo');
+ $this->options->setNormalizer('foo', function () {});
+ }
+
+ public function testSetLazyOption()
+ {
+ $test = $this;
+
+ $this->options->set('foo', function (Options $options) use ($test) {
+ return 'dynamic';
+ });
+
+ $this->assertEquals('dynamic', $this->options->get('foo'));
+ }
+
+ public function testSetDiscardsPreviousValue()
+ {
+ $test = $this;
+
+ // defined by superclass
+ $this->options->set('foo', 'bar');
+
+ // defined by subclass
+ $this->options->set('foo', function (Options $options, $previousValue) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertNull($previousValue);
+
+ return 'dynamic';
+ });
+
+ $this->assertEquals('dynamic', $this->options->get('foo'));
+ }
+
+ public function testOverloadKeepsPreviousValue()
+ {
+ $test = $this;
+
+ // defined by superclass
+ $this->options->set('foo', 'bar');
+
+ // defined by subclass
+ $this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $previousValue);
+
+ return 'dynamic';
+ });
+
+ $this->assertEquals('dynamic', $this->options->get('foo'));
+ }
+
+ public function testPreviousValueIsEvaluatedIfLazy()
+ {
+ $test = $this;
+
+ // defined by superclass
+ $this->options->set('foo', function (Options $options) {
+ return 'bar';
+ });
+
+ // defined by subclass
+ $this->options->overload('foo', function (Options $options, $previousValue) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $previousValue);
+
+ return 'dynamic';
+ });
+
+ $this->assertEquals('dynamic', $this->options->get('foo'));
+ }
+
+ public function testPreviousValueIsNotEvaluatedIfNoSecondArgument()
+ {
+ $test = $this;
+
+ // defined by superclass
+ $this->options->set('foo', function (Options $options) use ($test) {
+ $test->fail('Should not be called');
+ });
+
+ // defined by subclass, no $previousValue argument defined!
+ $this->options->overload('foo', function (Options $options) use ($test) {
+ return 'dynamic';
+ });
+
+ $this->assertEquals('dynamic', $this->options->get('foo'));
+ }
+
+ public function testLazyOptionCanAccessOtherOptions()
+ {
+ $test = $this;
+
+ $this->options->set('foo', 'bar');
+
+ $this->options->set('bam', function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $options->get('foo'));
+
+ return 'dynamic';
+ });
+
+ $this->assertEquals('bar', $this->options->get('foo'));
+ $this->assertEquals('dynamic', $this->options->get('bam'));
+ }
+
+ public function testLazyOptionCanAccessOtherLazyOptions()
+ {
+ $test = $this;
+
+ $this->options->set('foo', function (Options $options) {
+ return 'bar';
+ });
+
+ $this->options->set('bam', function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $options->get('foo'));
+
+ return 'dynamic';
+ });
+
+ $this->assertEquals('bar', $this->options->get('foo'));
+ $this->assertEquals('dynamic', $this->options->get('bam'));
+ }
+
+ public function testNormalizer()
+ {
+ $this->options->set('foo', 'bar');
+
+ $this->options->setNormalizer('foo', function () {
+ return 'normalized';
+ });
+
+ $this->assertEquals('normalized', $this->options->get('foo'));
+ }
+
+ public function testNormalizerReceivesUnnormalizedValue()
+ {
+ $this->options->set('foo', 'bar');
+
+ $this->options->setNormalizer('foo', function (Options $options, $value) {
+ return 'normalized['.$value.']';
+ });
+
+ $this->assertEquals('normalized[bar]', $this->options->get('foo'));
+ }
+
+ public function testNormalizerCanAccessOtherOptions()
+ {
+ $test = $this;
+
+ $this->options->set('foo', 'bar');
+ $this->options->set('bam', 'baz');
+
+ $this->options->setNormalizer('bam', function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $options->get('foo'));
+
+ return 'normalized';
+ });
+
+ $this->assertEquals('bar', $this->options->get('foo'));
+ $this->assertEquals('normalized', $this->options->get('bam'));
+ }
+
+ public function testNormalizerCanAccessOtherLazyOptions()
+ {
+ $test = $this;
+
+ $this->options->set('foo', function (Options $options) {
+ return 'bar';
+ });
+ $this->options->set('bam', 'baz');
+
+ $this->options->setNormalizer('bam', function (Options $options) use ($test) {
+ /* @var \PHPUnit_Framework_TestCase $test */
+ $test->assertEquals('bar', $options->get('foo'));
+
+ return 'normalized';
+ });
+
+ $this->assertEquals('bar', $this->options->get('foo'));
+ $this->assertEquals('normalized', $this->options->get('bam'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testFailForCyclicDependencies()
+ {
+ $this->options->set('foo', function (Options $options) {
+ $options->get('bam');
+ });
+
+ $this->options->set('bam', function (Options $options) {
+ $options->get('foo');
+ });
+
+ $this->options->get('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testFailForCyclicDependenciesBetweenNormalizers()
+ {
+ $this->options->set('foo', 'bar');
+ $this->options->set('bam', 'baz');
+
+ $this->options->setNormalizer('foo', function (Options $options) {
+ $options->get('bam');
+ });
+
+ $this->options->setNormalizer('bam', function (Options $options) {
+ $options->get('foo');
+ });
+
+ $this->options->get('foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption()
+ {
+ $this->options->set('foo', function (Options $options) {
+ $options->get('bam');
+ });
+ $this->options->set('bam', 'baz');
+
+ $this->options->setNormalizer('bam', function (Options $options) {
+ $options->get('foo');
+ });
+
+ $this->options->get('foo');
+ }
+
+ public function testAllInvokesEachLazyOptionOnlyOnce()
+ {
+ $test = $this;
+ $i = 1;
+
+ $this->options->set('foo', function (Options $options) use ($test, &$i) {
+ $test->assertSame(1, $i);
+ ++$i;
+
+ // Implicitly invoke lazy option for "bam"
+ $options->get('bam');
+ });
+ $this->options->set('bam', function (Options $options) use ($test, &$i) {
+ $test->assertSame(2, $i);
+ ++$i;
+ });
+
+ $this->options->all();
+ }
+
+ public function testAllInvokesEachNormalizerOnlyOnce()
+ {
+ $test = $this;
+ $i = 1;
+
+ $this->options->set('foo', 'bar');
+ $this->options->set('bam', 'baz');
+
+ $this->options->setNormalizer('foo', function (Options $options) use ($test, &$i) {
+ $test->assertSame(1, $i);
+ ++$i;
+
+ // Implicitly invoke normalizer for "bam"
+ $options->get('bam');
+ });
+ $this->options->setNormalizer('bam', function (Options $options) use ($test, &$i) {
+ $test->assertSame(2, $i);
+ ++$i;
+ });
+
+ $this->options->all();
+ }
+
+ public function testReplaceClearsAndSets()
+ {
+ $this->options->set('one', '1');
+
+ $this->options->replace(array(
+ 'two' => '2',
+ 'three' => function (Options $options) {
+ return '2' === $options['two'] ? '3' : 'foo';
+ }
+ ));
+
+ $this->assertEquals(array(
+ 'two' => '2',
+ 'three' => '3',
+ ), $this->options->all());
+ }
+
+ public function testClearRemovesAllOptions()
+ {
+ $this->options->set('one', 1);
+ $this->options->set('two', 2);
+
+ $this->options->clear();
+
+ $this->assertEmpty($this->options->all());
+
+ }
+
+ /**
+ * @covers Symfony\Component\OptionsResolver\Options::replace
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testCannotReplaceAfterOptionWasRead()
+ {
+ $this->options->set('one', 1);
+ $this->options->all();
+
+ $this->options->replace(array(
+ 'two' => '2',
+ ));
+ }
+
+ /**
+ * @covers Symfony\Component\OptionsResolver\Options::overload
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testCannotOverloadAfterOptionWasRead()
+ {
+ $this->options->set('one', 1);
+ $this->options->all();
+
+ $this->options->overload('one', 2);
+ }
+
+ /**
+ * @covers Symfony\Component\OptionsResolver\Options::clear
+ * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
+ */
+ public function testCannotClearAfterOptionWasRead()
+ {
+ $this->options->set('one', 1);
+ $this->options->all();
+
+ $this->options->clear();
+ }
+
+ public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams()
+ {
+ $this->options->set('foo', 'bar');
+
+ $this->options->overload('foo', function () {
+ return 'test';
+ });
+
+ $this->assertNotEquals('test', $this->options->get('foo'));
+ $this->assertTrue(is_callable($this->options->get('foo')));
+ }
+
+ public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint()
+ {
+ $this->options->set('foo', 'bar');
+
+ $this->options->overload('foo', function ($object) {
+ return 'test';
+ });
+
+ $this->assertNotEquals('test', $this->options->get('foo'));
+ $this->assertTrue(is_callable($this->options->get('foo')));
+ }
+
+ public function testOptionsIteration()
+ {
+ $this->options->set('foo', 'bar');
+ $this->options->set('foo1', 'bar1');
+ $expectedResult = array('foo' => 'bar', 'foo1' => 'bar1');
+
+ $this->assertEquals($expectedResult, iterator_to_array($this->options, true));
+ }
+
+ public function testHasWithNullValue()
+ {
+ $this->options->set('foo', null);
+
+ $this->assertTrue($this->options->has('foo'));
+ }
+
+ public function testRemoveOptionAndNormalizer()
+ {
+ $this->options->set('foo1', 'bar');
+ $this->options->setNormalizer('foo1', function (Options $options) {
+ return '';
+ });
+ $this->options->set('foo2', 'bar');
+ $this->options->setNormalizer('foo2', function (Options $options) {
+ return '';
+ });
+
+ $this->options->remove('foo2');
+ $this->assertEquals(array('foo1' => ''), $this->options->all());
+ }
+
+ public function testReplaceOptionAndNormalizer()
+ {
+ $this->options->set('foo1', 'bar');
+ $this->options->setNormalizer('foo1', function (Options $options) {
+ return '';
+ });
+ $this->options->set('foo2', 'bar');
+ $this->options->setNormalizer('foo2', function (Options $options) {
+ return '';
+ });
+
+ $this->options->replace(array('foo1' => 'new'));
+ $this->assertEquals(array('foo1' => 'new'), $this->options->all());
+ }
+
+ public function testClearOptionAndNormalizer()
+ {
+ $this->options->set('foo1', 'bar');
+ $this->options->setNormalizer('foo1', function (Options $options) {
+ return '';
+ });
+ $this->options->set('foo2', 'bar');
+ $this->options->setNormalizer('foo2', function (Options $options) {
+ return '';
+ });
+
+ $this->options->clear();
+ $this->assertEmpty($this->options->all());
+ }
+
+ public function testNormalizerWithoutCorrespondingOption()
+ {
+ $test = $this;
+
+ $this->options->setNormalizer('foo', function (Options $options, $previousValue) use ($test) {
+ $test->assertNull($previousValue);
+
+ return '';
+ });
+ $this->assertEquals(array('foo' => ''), $this->options->all());
+ }
+}
--- /dev/null
+{
+ "name": "symfony/options-resolver",
+ "type": "library",
+ "description": "Symfony OptionsResolver Component",
+ "keywords": ["options", "config", "configuration"],
+ "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\\OptionsResolver\\": "" }
+ },
+ "target-dir": "Symfony/Component/OptionsResolver",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 OptionsResolver Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Resources</directory>
+ <directory>./Tests</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+/Tests export-ignore
+phpunit.xml.dist export-ignore
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+CHANGELOG
+=========
+
+2.3.0
+------
+
+ * added PropertyAccessorBuilder, to enable or disable the support of "__call"
+ * added support for "__call" in the PropertyAccessor (disabled by default)
+ * [BC BREAK] changed PropertyAccessor to continue its search for a property or
+ method even if a non-public match was found. Before, a PropertyAccessDeniedException
+ was thrown in this case. Class PropertyAccessDeniedException was removed
+ now.
+ * deprecated PropertyAccess::getPropertyAccessor
+ * added PropertyAccess::createPropertyAccessor and PropertyAccess::createPropertyAccessorBuilder
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Marker interface for the PropertyAccess component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Thrown when a property path is malformed.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class InvalidPropertyPathException extends RuntimeException
+{
+}
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Thrown when a property cannot be found.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class NoSuchPropertyException extends RuntimeException
+{
+}
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Base OutOfBoundsException for the PropertyAccess component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Base RuntimeException for the PropertyAccess component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\PropertyAccess\Exception;
+
+/**
+ * Thrown when a value does not match an expected type.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class UnexpectedTypeException extends RuntimeException
+{
+ public function __construct($value, $expectedType)
+ {
+ parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
+ }
+}
--- /dev/null
+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.
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * Entry point of the PropertyAccess component.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+final class PropertyAccess
+{
+ /**
+ * Creates a property accessor with the default configuration.
+ *
+ * @return PropertyAccessor The new property accessor
+ */
+ public static function createPropertyAccessor()
+ {
+ return self::createPropertyAccessorBuilder()->getPropertyAccessor();
+ }
+
+ /**
+ * Creates a property accessor builder.
+ *
+ * @return PropertyAccessorBuilder The new property accessor builder
+ */
+ public static function createPropertyAccessorBuilder()
+ {
+ return new PropertyAccessorBuilder();
+ }
+
+ /**
+ * Alias of {@link getPropertyAccessor}.
+ *
+ * @return PropertyAccessor The new property accessor
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * {@link createPropertyAccessor()} instead.
+ */
+ public static function getPropertyAccessor()
+ {
+ return self::createPropertyAccessor();
+ }
+
+ /**
+ * This class cannot be instantiated.
+ */
+ private function __construct()
+ {
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
+use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
+
+/**
+ * Default implementation of {@link PropertyAccessorInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyAccessor implements PropertyAccessorInterface
+{
+ const VALUE = 0;
+ const IS_REF = 1;
+
+ private $magicCall;
+
+ /**
+ * Should not be used by application code. Use
+ * {@link PropertyAccess::getPropertyAccessor()} instead.
+ */
+ public function __construct($magicCall = false)
+ {
+ $this->magicCall = $magicCall;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue($objectOrArray, $propertyPath)
+ {
+ if (is_string($propertyPath)) {
+ $propertyPath = new PropertyPath($propertyPath);
+ } elseif (!$propertyPath instanceof PropertyPathInterface) {
+ throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
+ }
+
+ $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength());
+
+ return $propertyValues[count($propertyValues) - 1][self::VALUE];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValue(&$objectOrArray, $propertyPath, $value)
+ {
+ if (is_string($propertyPath)) {
+ $propertyPath = new PropertyPath($propertyPath);
+ } elseif (!$propertyPath instanceof PropertyPathInterface) {
+ throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface');
+ }
+
+ $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1);
+ $overwrite = true;
+
+ // Add the root object to the list
+ array_unshift($propertyValues, array(
+ self::VALUE => &$objectOrArray,
+ self::IS_REF => true,
+ ));
+
+ for ($i = count($propertyValues) - 1; $i >= 0; --$i) {
+ $objectOrArray =& $propertyValues[$i][self::VALUE];
+
+ if ($overwrite) {
+ if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
+ throw new UnexpectedTypeException($objectOrArray, 'object or array');
+ }
+
+ $property = $propertyPath->getElement($i);
+ //$singular = $propertyPath->singulars[$i];
+ $singular = null;
+
+ if ($propertyPath->isIndex($i)) {
+ $this->writeIndex($objectOrArray, $property, $value);
+ } else {
+ $this->writeProperty($objectOrArray, $property, $singular, $value);
+ }
+ }
+
+ $value =& $objectOrArray;
+ $overwrite = !$propertyValues[$i][self::IS_REF];
+ }
+ }
+
+ /**
+ * Reads the path from an object up to a given path index.
+ *
+ * @param object|array $objectOrArray The object or array to read from
+ * @param PropertyPathInterface $propertyPath The property path to read
+ * @param integer $lastIndex The index up to which should be read
+ *
+ * @return array The values read in the path.
+ *
+ * @throws UnexpectedTypeException If a value within the path is neither object nor array.
+ */
+ private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex)
+ {
+ $propertyValues = array();
+
+ for ($i = 0; $i < $lastIndex; ++$i) {
+ if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
+ throw new UnexpectedTypeException($objectOrArray, 'object or array');
+ }
+
+ $property = $propertyPath->getElement($i);
+ $isIndex = $propertyPath->isIndex($i);
+ $isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess;
+
+ // Create missing nested arrays on demand
+ if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) {
+ $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null;
+ }
+
+ if ($isIndex) {
+ $propertyValue =& $this->readIndex($objectOrArray, $property);
+ } else {
+ $propertyValue =& $this->readProperty($objectOrArray, $property);
+ }
+
+ $objectOrArray =& $propertyValue[self::VALUE];
+
+ $propertyValues[] =& $propertyValue;
+ }
+
+ return $propertyValues;
+ }
+
+ /**
+ * Reads a key from an array-like structure.
+ *
+ * @param \ArrayAccess|array $array The array or \ArrayAccess object to read from
+ * @param string|integer $index The key to read
+ *
+ * @return mixed The value of the key
+ *
+ * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
+ */
+ private function &readIndex(&$array, $index)
+ {
+ if (!$array instanceof \ArrayAccess && !is_array($array)) {
+ throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
+ }
+
+ // Use an array instead of an object since performance is very crucial here
+ $result = array(
+ self::VALUE => null,
+ self::IS_REF => false
+ );
+
+ if (isset($array[$index])) {
+ if (is_array($array)) {
+ $result[self::VALUE] =& $array[$index];
+ $result[self::IS_REF] = true;
+ } else {
+ $result[self::VALUE] = $array[$index];
+ // Objects are always passed around by reference
+ $result[self::IS_REF] = is_object($array[$index]) ? true : false;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Reads the a property from an object or array.
+ *
+ * @param object $object The object to read from.
+ * @param string $property The property to read.
+ *
+ * @return mixed The value of the read property
+ *
+ * @throws NoSuchPropertyException If the property does not exist or is not
+ * public.
+ */
+ private function &readProperty(&$object, $property)
+ {
+ // Use an array instead of an object since performance is
+ // very crucial here
+ $result = array(
+ self::VALUE => null,
+ self::IS_REF => false
+ );
+
+ if (!is_object($object)) {
+ throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
+ }
+
+ $camelProp = $this->camelize($property);
+ $reflClass = new \ReflectionClass($object);
+ $getter = 'get'.$camelProp;
+ $isser = 'is'.$camelProp;
+ $hasser = 'has'.$camelProp;
+ $classHasProperty = $reflClass->hasProperty($property);
+
+ if ($reflClass->hasMethod($getter) && $reflClass->getMethod($getter)->isPublic()) {
+ $result[self::VALUE] = $object->$getter();
+ } elseif ($reflClass->hasMethod($isser) && $reflClass->getMethod($isser)->isPublic()) {
+ $result[self::VALUE] = $object->$isser();
+ } elseif ($reflClass->hasMethod($hasser) && $reflClass->getMethod($hasser)->isPublic()) {
+ $result[self::VALUE] = $object->$hasser();
+ } elseif ($reflClass->hasMethod('__get') && $reflClass->getMethod('__get')->isPublic()) {
+ $result[self::VALUE] = $object->$property;
+ } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
+ $result[self::VALUE] =& $object->$property;
+ $result[self::IS_REF] = true;
+ } elseif (!$classHasProperty && property_exists($object, $property)) {
+ // Needed to support \stdClass instances. We need to explicitly
+ // exclude $classHasProperty, otherwise if in the previous clause
+ // a *protected* property was found on the class, property_exists()
+ // returns true, consequently the following line will result in a
+ // fatal error.
+ $result[self::VALUE] =& $object->$property;
+ $result[self::IS_REF] = true;
+ } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
+ // we call the getter and hope the __call do the job
+ $result[self::VALUE] = $object->$getter();
+ } else {
+ throw new NoSuchPropertyException(sprintf(
+ 'Neither the property "%s" nor one of the methods "%s()", '.
+ '"%s()", "%s()", "__get()" or "__call()" exist and have public access in '.
+ 'class "%s".',
+ $property,
+ $getter,
+ $isser,
+ $hasser,
+ $reflClass->name
+ ));
+ }
+
+ // Objects are always passed around by reference
+ if (is_object($result[self::VALUE])) {
+ $result[self::IS_REF] = true;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sets the value of the property at the given index in the path
+ *
+ * @param \ArrayAccess|array $array An array or \ArrayAccess object to write to
+ * @param string|integer $index The index to write at
+ * @param mixed $value The value to write
+ *
+ * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array
+ */
+ private function writeIndex(&$array, $index, $value)
+ {
+ if (!$array instanceof \ArrayAccess && !is_array($array)) {
+ throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array)));
+ }
+
+ $array[$index] = $value;
+ }
+
+ /**
+ * Sets the value of the property at the given index in the path
+ *
+ * @param object|array $object The object or array to write to
+ * @param string $property The property to write
+ * @param string|null $singular The singular form of the property name or null
+ * @param mixed $value The value to write
+ *
+ * @throws NoSuchPropertyException If the property does not exist or is not
+ * public.
+ */
+ private function writeProperty(&$object, $property, $singular, $value)
+ {
+ $guessedAdders = '';
+
+ if (!is_object($object)) {
+ throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
+ }
+
+ $reflClass = new \ReflectionClass($object);
+ $plural = $this->camelize($property);
+
+ // Any of the two methods is required, but not yet known
+ $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural);
+
+ if (is_array($value) || $value instanceof \Traversable) {
+ $methods = $this->findAdderAndRemover($reflClass, $singulars);
+
+ if (null !== $methods) {
+ // At this point the add and remove methods have been found
+ // Use iterator_to_array() instead of clone in order to prevent side effects
+ // see https://github.com/symfony/symfony/issues/4670
+ $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value;
+ $itemToRemove = array();
+ $propertyValue = $this->readProperty($object, $property);
+ $previousValue = $propertyValue[self::VALUE];
+
+ if (is_array($previousValue) || $previousValue instanceof \Traversable) {
+ foreach ($previousValue as $previousItem) {
+ foreach ($value as $key => $item) {
+ if ($item === $previousItem) {
+ // Item found, don't add
+ unset($itemsToAdd[$key]);
+
+ // Next $previousItem
+ continue 2;
+ }
+ }
+
+ // Item not found, add to remove list
+ $itemToRemove[] = $previousItem;
+ }
+ }
+
+ foreach ($itemToRemove as $item) {
+ call_user_func(array($object, $methods[1]), $item);
+ }
+
+ foreach ($itemsToAdd as $item) {
+ call_user_func(array($object, $methods[0]), $item);
+ }
+
+ return;
+ } else {
+ // It is sufficient to include only the adders in the error
+ // message. If the user implements the adder but not the remover,
+ // an exception will be thrown in findAdderAndRemover() that
+ // the remover has to be implemented as well.
+ $guessedAdders = '"add'.implode('()", "add', $singulars).'()", ';
+ }
+ }
+
+ $setter = 'set'.$this->camelize($property);
+ $classHasProperty = $reflClass->hasProperty($property);
+
+ if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) {
+ $object->$setter($value);
+ } elseif ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) {
+ $object->$property = $value;
+ } elseif ($classHasProperty && $reflClass->getProperty($property)->isPublic()) {
+ $object->$property = $value;
+ } elseif (!$classHasProperty && property_exists($object, $property)) {
+ // Needed to support \stdClass instances. We need to explicitly
+ // exclude $classHasProperty, otherwise if in the previous clause
+ // a *protected* property was found on the class, property_exists()
+ // returns true, consequently the following line will result in a
+ // fatal error.
+ $object->$property = $value;
+ } elseif ($this->magicCall && $reflClass->hasMethod('__call') && $reflClass->getMethod('__call')->isPublic()) {
+ // we call the getter and hope the __call do the job
+ $object->$setter($value);
+ } else {
+ throw new NoSuchPropertyException(sprintf(
+ 'Neither the property "%s" nor one of the methods %s"%s()", '.
+ '"__set()" or "__call()" exist and have public access in class "%s".',
+ $property,
+ $guessedAdders,
+ $setter,
+ $reflClass->name
+ ));
+ }
+ }
+
+ /**
+ * Camelizes a given string.
+ *
+ * @param string $string Some string
+ *
+ * @return string The camelized version of the string
+ */
+ private function camelize($string)
+ {
+ return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string);
+ }
+
+ /**
+ * Searches for add and remove methods.
+ *
+ * @param \ReflectionClass $reflClass The reflection class for the given object
+ * @param array $singulars The singular form of the property name or null
+ *
+ * @return array|null An array containing the adder and remover when found, null otherwise
+ *
+ * @throws NoSuchPropertyException If the property does not exist
+ */
+ private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars)
+ {
+ foreach ($singulars as $singular) {
+ $addMethod = 'add'.$singular;
+ $removeMethod = 'remove'.$singular;
+
+ $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1);
+ $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1);
+
+ if ($addMethodFound && $removeMethodFound) {
+ return array($addMethod, $removeMethod);
+ }
+
+ if ($addMethodFound xor $removeMethodFound) {
+ throw new NoSuchPropertyException(sprintf(
+ 'Found the public method "%s()", but did not find a public "%s()" on class %s',
+ $addMethodFound ? $addMethod : $removeMethod,
+ $addMethodFound ? $removeMethod : $addMethod,
+ $reflClass->name
+ ));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether a method is public and has a specific number of required parameters.
+ *
+ * @param \ReflectionClass $class The class of the method
+ * @param string $methodName The method name
+ * @param integer $parameters The number of parameters
+ *
+ * @return Boolean Whether the method is public and has $parameters
+ * required parameters
+ */
+ private function isAccessible(\ReflectionClass $class, $methodName, $parameters)
+ {
+ if ($class->hasMethod($methodName)) {
+ $method = $class->getMethod($methodName);
+
+ if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * A configurable builder for PropertyAccessorInterface objects.
+ *
+ * @author Jérémie Augustin <jeremie.augustin@pixel-cookers.com>
+ */
+class PropertyAccessorBuilder
+{
+ /**
+ * @var Boolean
+ */
+ private $magicCall = false;
+
+ /**
+ * Enables the use of "__call" by the ProperyAccessor.
+ *
+ * @return PropertyAccessorBuilder The builder object
+ */
+ public function enableMagicCall()
+ {
+ $this->magicCall = true;
+
+ return $this;
+ }
+
+ /**
+ * Disables the use of "__call" by the ProperyAccessor.
+ *
+ * @return PropertyAccessorBuilder The builder object
+ */
+ public function disableMagicCall()
+ {
+ $this->magicCall = false;
+
+ return $this;
+ }
+
+ /**
+ * @return Boolean true if the use of "__call" by the ProperyAccessor is enabled
+ */
+ public function isMagicCallEnabled()
+ {
+ return $this->magicCall;
+ }
+
+ /**
+ * Builds and returns a new propertyAccessor object.
+ *
+ * @return PropertyAccessorInterface The built propertyAccessor
+ */
+ public function getPropertyAccessor()
+ {
+ return new PropertyAccessor($this->magicCall);
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * Writes and reads values to/from an object/array graph.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface PropertyAccessorInterface
+{
+ /**
+ * Sets the value at the end of the property path of the object
+ *
+ * Example:
+ *
+ * use Symfony\Component\PropertyAccess\PropertyAccess;
+ *
+ * $propertyAccessor = PropertyAccess::getPropertyAccessor();
+ *
+ * echo $propertyAccessor->setValue($object, 'child.name', 'Fabien');
+ * // equals echo $object->getChild()->setName('Fabien');
+ *
+ * This method first tries to find a public setter for each property in the
+ * path. The name of the setter must be the camel-cased property name
+ * prefixed with "set".
+ *
+ * If the setter does not exist, this method tries to find a public
+ * property. The value of the property is then changed.
+ *
+ * If neither is found, an exception is thrown.
+ *
+ * @param object|array $objectOrArray The object or array to modify
+ * @param string|PropertyPathInterface $propertyPath The property path to modify
+ * @param mixed $value The value to set at the end of the property path
+ *
+ * @throws Exception\NoSuchPropertyException If a property does not exist or is not public.
+ * @throws Exception\UnexpectedTypeException If a value within the path is neither object
+ * nor array
+ */
+ public function setValue(&$objectOrArray, $propertyPath, $value);
+
+ /**
+ * Returns the value at the end of the property path of the object
+ *
+ * Example:
+ *
+ * use Symfony\Component\PropertyAccess\PropertyAccess;
+ *
+ * $propertyAccessor = PropertyAccess::getPropertyAccessor();
+ *
+ * echo $propertyAccessor->getValue($object, 'child.name);
+ * // equals echo $object->getChild()->getName();
+ *
+ * This method first tries to find a public getter for each property in the
+ * path. The name of the getter must be the camel-cased property name
+ * prefixed with "get", "is", or "has".
+ *
+ * If the getter does not exist, this method tries to find a public
+ * property. The value of the property is then returned.
+ *
+ * If none of them are found, an exception is thrown.
+ *
+ * @param object|array $objectOrArray The object or array to traverse
+ * @param string|PropertyPathInterface $propertyPath The property path to read
+ *
+ * @return mixed The value at the end of the property path
+ *
+ * @throws Exception\NoSuchPropertyException If a property does not exist or is not public.
+ */
+ public function getValue($objectOrArray, $propertyPath);
+}
--- /dev/null
+<?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\PropertyAccess;
+
+use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
+use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
+use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
+
+/**
+ * Default implementation of {@link PropertyPathInterface}.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPath implements \IteratorAggregate, PropertyPathInterface
+{
+ /**
+ * Character used for separating between plural and singular of an element.
+ * @var string
+ */
+ const SINGULAR_SEPARATOR = '|';
+
+ /**
+ * The elements of the property path
+ * @var array
+ */
+ private $elements = array();
+
+ /**
+ * The singular forms of the elements in the property path.
+ * @var array
+ */
+ private $singulars = array();
+
+ /**
+ * The number of elements in the property path
+ * @var integer
+ */
+ private $length;
+
+ /**
+ * Contains a Boolean for each property in $elements denoting whether this
+ * element is an index. It is a property otherwise.
+ * @var array
+ */
+ private $isIndex = array();
+
+ /**
+ * String representation of the path
+ * @var string
+ */
+ private $pathAsString;
+
+ /**
+ * Constructs a property path from a string.
+ *
+ * @param PropertyPath|string $propertyPath The property path as string or instance
+ *
+ * @throws UnexpectedTypeException If the given path is not a string
+ * @throws InvalidPropertyPathException If the syntax of the property path is not valid
+ */
+ public function __construct($propertyPath)
+ {
+ // Can be used as copy constructor
+ if ($propertyPath instanceof PropertyPath) {
+ /* @var PropertyPath $propertyPath */
+ $this->elements = $propertyPath->elements;
+ $this->singulars = $propertyPath->singulars;
+ $this->length = $propertyPath->length;
+ $this->isIndex = $propertyPath->isIndex;
+ $this->pathAsString = $propertyPath->pathAsString;
+
+ return;
+ }
+ if (!is_string($propertyPath)) {
+ throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPath');
+ }
+
+ if ('' === $propertyPath) {
+ throw new InvalidPropertyPathException('The property path should not be empty.');
+ }
+
+ $this->pathAsString = $propertyPath;
+ $position = 0;
+ $remaining = $propertyPath;
+
+ // first element is evaluated differently - no leading dot for properties
+ $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/';
+
+ while (preg_match($pattern, $remaining, $matches)) {
+ if ('' !== $matches[2]) {
+ $element = $matches[2];
+ $this->isIndex[] = false;
+ } else {
+ $element = $matches[3];
+ $this->isIndex[] = true;
+ }
+ // Disabled this behaviour as the syntax is not yet final
+ //$pos = strpos($element, self::SINGULAR_SEPARATOR);
+ $pos = false;
+ $singular = null;
+
+ if (false !== $pos) {
+ $singular = substr($element, $pos + 1);
+ $element = substr($element, 0, $pos);
+ }
+
+ $this->elements[] = $element;
+ $this->singulars[] = $singular;
+
+ $position += strlen($matches[1]);
+ $remaining = $matches[4];
+ $pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/';
+ }
+
+ if ('' !== $remaining) {
+ throw new InvalidPropertyPathException(sprintf(
+ 'Could not parse property path "%s". Unexpected token "%s" at position %d',
+ $propertyPath,
+ $remaining{0},
+ $position
+ ));
+ }
+
+ $this->length = count($this->elements);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ return $this->pathAsString;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLength()
+ {
+ return $this->length;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParent()
+ {
+ if ($this->length <= 1) {
+ return null;
+ }
+
+ $parent = clone $this;
+
+ --$parent->length;
+ $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '[')));
+ array_pop($parent->elements);
+ array_pop($parent->singulars);
+ array_pop($parent->isIndex);
+
+ return $parent;
+ }
+
+ /**
+ * Returns a new iterator for this path
+ *
+ * @return PropertyPathIteratorInterface
+ */
+ public function getIterator()
+ {
+ return new PropertyPathIterator($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElements()
+ {
+ return $this->elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElement($index)
+ {
+ if (!isset($this->elements[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
+ }
+
+ return $this->elements[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isProperty($index)
+ {
+ if (!isset($this->isIndex[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
+ }
+
+ return !$this->isIndex[$index];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isIndex($index)
+ {
+ if (!isset($this->isIndex[$index])) {
+ throw new OutOfBoundsException(sprintf('The index %s is not within the property path', $index));
+ }
+
+ return $this->isIndex[$index];
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPathBuilder
+{
+ /**
+ * @var array
+ */
+ private $elements = array();
+
+ /**
+ * @var array
+ */
+ private $isIndex = array();
+
+ /**
+ * Creates a new property path builder.
+ *
+ * @param null|PropertyPathInterface|string $path The path to initially store
+ * in the builder. Optional.
+ */
+ public function __construct($path = null)
+ {
+ if (null !== $path) {
+ $this->append($path);
+ }
+ }
+
+ /**
+ * Appends a (sub-) path to the current path.
+ *
+ * @param PropertyPathInterface|string $path The path to append.
+ * @param integer $offset The offset where the appended
+ * piece starts in $path.
+ * @param integer $length The length of the appended piece.
+ * If 0, the full path is appended.
+ */
+ public function append($path, $offset = 0, $length = 0)
+ {
+ if (is_string($path)) {
+ $path = new PropertyPath($path);
+ }
+
+ if (0 === $length) {
+ $end = $path->getLength();
+ } else {
+ $end = $offset + $length;
+ }
+
+ for (; $offset < $end; ++$offset) {
+ $this->elements[] = $path->getElement($offset);
+ $this->isIndex[] = $path->isIndex($offset);
+ }
+ }
+
+ /**
+ * Appends an index element to the current path.
+ *
+ * @param string $name The name of the appended index
+ */
+ public function appendIndex($name)
+ {
+ $this->elements[] = $name;
+ $this->isIndex[] = true;
+ }
+
+ /**
+ * Appends a property element to the current path.
+ *
+ * @param string $name The name of the appended property
+ */
+ public function appendProperty($name)
+ {
+ $this->elements[] = $name;
+ $this->isIndex[] = false;
+ }
+
+ /**
+ * Removes elements from the current path.
+ *
+ * @param integer $offset The offset at which to remove
+ * @param integer $length The length of the removed piece
+ *
+ * @throws OutOfBoundsException if offset is invalid
+ */
+ public function remove($offset, $length = 1)
+ {
+ if (!isset($this->elements[$offset])) {
+ throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
+ }
+
+ $this->resize($offset, $length, 0);
+ }
+
+ /**
+ * Replaces a sub-path by a different (sub-) path.
+ *
+ * @param integer $offset The offset at which to replace.
+ * @param integer $length The length of the piece to replace.
+ * @param PropertyPathInterface|string $path The path to insert.
+ * @param integer $pathOffset The offset where the inserted piece
+ * starts in $path.
+ * @param integer $pathLength The length of the inserted piece.
+ * If 0, the full path is inserted.
+ *
+ * @throws OutOfBoundsException If the offset is invalid
+ */
+ public function replace($offset, $length, $path, $pathOffset = 0, $pathLength = 0)
+ {
+ if (is_string($path)) {
+ $path = new PropertyPath($path);
+ }
+
+ if ($offset < 0 && abs($offset) <= $this->getLength()) {
+ $offset = $this->getLength() + $offset;
+ } elseif (!isset($this->elements[$offset])) {
+ throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
+ }
+
+ if (0 === $pathLength) {
+ $pathLength = $path->getLength() - $pathOffset;
+ }
+
+ $this->resize($offset, $length, $pathLength);
+
+ for ($i = 0; $i < $pathLength; ++$i) {
+ $this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
+ $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
+ }
+ }
+
+ /**
+ * Replaces a property element by an index element.
+ *
+ * @param integer $offset The offset at which to replace
+ * @param string $name The new name of the element. Optional.
+ *
+ * @throws OutOfBoundsException If the offset is invalid
+ */
+ public function replaceByIndex($offset, $name = null)
+ {
+ if (!isset($this->elements[$offset])) {
+ throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
+ }
+
+ if (null !== $name) {
+ $this->elements[$offset] = $name;
+ }
+
+ $this->isIndex[$offset] = true;
+ }
+
+ /**
+ * Replaces an index element by a property element.
+ *
+ * @param integer $offset The offset at which to replace
+ * @param string $name The new name of the element. Optional.
+ *
+ * @throws OutOfBoundsException If the offset is invalid
+ */
+ public function replaceByProperty($offset, $name = null)
+ {
+ if (!isset($this->elements[$offset])) {
+ throw new OutOfBoundsException(sprintf('The offset %s is not within the property path', $offset));
+ }
+
+ if (null !== $name) {
+ $this->elements[$offset] = $name;
+ }
+
+ $this->isIndex[$offset] = false;
+ }
+
+ /**
+ * Returns the length of the current path.
+ *
+ * @return integer The path length
+ */
+ public function getLength()
+ {
+ return count($this->elements);
+ }
+
+ /**
+ * Returns the current property path.
+ *
+ * @return PropertyPathInterface The constructed property path
+ */
+ public function getPropertyPath()
+ {
+ $pathAsString = $this->__toString();
+
+ return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
+ }
+
+ /**
+ * Returns the current property path as string.
+ *
+ * @return string The property path as string
+ */
+ public function __toString()
+ {
+ $string = '';
+
+ foreach ($this->elements as $offset => $element) {
+ if ($this->isIndex[$offset]) {
+ $element = '['.$element.']';
+ } elseif ('' !== $string) {
+ $string .= '.';
+ }
+
+ $string .= $element;
+ }
+
+ return $string;
+ }
+
+ /**
+ * Resizes the path so that a chunk of length $cutLength is
+ * removed at $offset and another chunk of length $insertionLength
+ * can be inserted.
+ *
+ * @param integer $offset The offset where the removed chunk starts
+ * @param integer $cutLength The length of the removed chunk
+ * @param integer $insertionLength The length of the inserted chunk
+ */
+ private function resize($offset, $cutLength, $insertionLength)
+ {
+ // Nothing else to do in this case
+ if ($insertionLength === $cutLength) {
+ return;
+ }
+
+ $length = count($this->elements);
+
+ if ($cutLength > $insertionLength) {
+ // More elements should be removed than inserted
+ $diff = $cutLength - $insertionLength;
+ $newLength = $length - $diff;
+
+ // Shift elements to the left (left-to-right until the new end)
+ // Max allowed offset to be shifted is such that
+ // $offset + $diff < $length (otherwise invalid index access)
+ // i.e. $offset < $length - $diff = $newLength
+ for ($i = $offset; $i < $newLength; ++$i) {
+ $this->elements[$i] = $this->elements[$i + $diff];
+ $this->isIndex[$i] = $this->isIndex[$i + $diff];
+ }
+
+ // All remaining elements should be removed
+ for (; $i < $length; ++$i) {
+ unset($this->elements[$i]);
+ unset($this->isIndex[$i]);
+ }
+ } else {
+ $diff = $insertionLength - $cutLength;
+
+ $newLength = $length + $diff;
+ $indexAfterInsertion = $offset + $insertionLength;
+
+ // $diff <= $insertionLength
+ // $indexAfterInsertion >= $insertionLength
+ // => $diff <= $indexAfterInsertion
+
+ // In each of the following loops, $i >= $diff must hold,
+ // otherwise ($i - $diff) becomes negative.
+
+ // Shift old elements to the right to make up space for the
+ // inserted elements. This needs to be done left-to-right in
+ // order to preserve an ascending array index order
+ // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff,
+ // $i >= $diff is guaranteed.
+ for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) {
+ $this->elements[$i] = $this->elements[$i - $diff];
+ $this->isIndex[$i] = $this->isIndex[$i - $diff];
+ }
+
+ // Shift remaining elements to the right. Do this right-to-left
+ // so we don't overwrite elements before copying them
+ // The last written index is the immediate index after the inserted
+ // string, because the indices before that will be overwritten
+ // anyway.
+ // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff,
+ // $i >= $diff is guaranteed.
+ for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
+ $this->elements[$i] = $this->elements[$i - $diff];
+ $this->isIndex[$i] = $this->isIndex[$i - $diff];
+ }
+ }
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * A sequence of property names or array indices.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface PropertyPathInterface extends \Traversable
+{
+ /**
+ * Returns the string representation of the property path
+ *
+ * @return string The path as string
+ */
+ public function __toString();
+
+ /**
+ * Returns the length of the property path, i.e. the number of elements.
+ *
+ * @return integer The path length
+ */
+ public function getLength();
+
+ /**
+ * Returns the parent property path.
+ *
+ * The parent property path is the one that contains the same items as
+ * this one except for the last one.
+ *
+ * If this property path only contains one item, null is returned.
+ *
+ * @return PropertyPath The parent path or null
+ */
+ public function getParent();
+
+ /**
+ * Returns the elements of the property path as array
+ *
+ * @return array An array of property/index names
+ */
+ public function getElements();
+
+ /**
+ * Returns the element at the given index in the property path
+ *
+ * @param integer $index The index key
+ *
+ * @return string A property or index name
+ *
+ * @throws Exception\OutOfBoundsException If the offset is invalid
+ */
+ public function getElement($index);
+
+ /**
+ * Returns whether the element at the given index is a property
+ *
+ * @param integer $index The index in the property path
+ *
+ * @return Boolean Whether the element at this index is a property
+ *
+ * @throws Exception\OutOfBoundsException If the offset is invalid
+ */
+ public function isProperty($index);
+
+ /**
+ * Returns whether the element at the given index is an array index
+ *
+ * @param integer $index The index in the property path
+ *
+ * @return Boolean Whether the element at this index is an array index
+ *
+ * @throws Exception\OutOfBoundsException If the offset is invalid
+ */
+ public function isIndex($index);
+}
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * Traverses a property path and provides additional methods to find out
+ * information about the current element
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
+{
+ /**
+ * The traversed property path
+ * @var PropertyPathInterface
+ */
+ protected $path;
+
+ /**
+ * Constructor.
+ *
+ * @param PropertyPathInterface $path The property path to traverse
+ */
+ public function __construct(PropertyPathInterface $path)
+ {
+ parent::__construct($path->getElements());
+
+ $this->path = $path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isIndex()
+ {
+ return $this->path->isIndex($this->key());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isProperty()
+ {
+ return $this->path->isProperty($this->key());
+ }
+}
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator
+{
+ /**
+ * Returns whether the current element in the property path is an array
+ * index.
+ *
+ * @return Boolean
+ */
+ public function isIndex();
+
+ /**
+ * Returns whether the current element in the property path is a property
+ * name.
+ *
+ * @return Boolean
+ */
+ public function isProperty();
+}
--- /dev/null
+PropertyAccess Component
+========================
+
+PropertyAccess reads/writes values from/to object/array graphs using a simple
+string notation.
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/PropertyAccess/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\PropertyAccess;
+
+/**
+ * Creates singulars from plurals.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class StringUtil
+{
+ /**
+ * Map english plural to singular suffixes
+ *
+ * @var array
+ *
+ * @see http://english-zone.com/spelling/plurals.html
+ * @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
+ */
+ private static $pluralMap = array(
+ // First entry: plural suffix, reversed
+ // Second entry: length of plural suffix
+ // Third entry: Whether the suffix may succeed a vocal
+ // Fourth entry: Whether the suffix may succeed a consonant
+ // Fifth entry: singular suffix, normal
+
+ // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
+ array('a', 1, true, true, array('on', 'um')),
+
+ // nebulae (nebula)
+ array('ea', 2, true, true, 'a'),
+
+ // mice (mouse), lice (louse)
+ array('eci', 3, false, true, 'ouse'),
+
+ // geese (goose)
+ array('esee', 4, false, true, 'oose'),
+
+ // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
+ array('i', 1, true, true, 'us'),
+
+ // men (man), women (woman)
+ array('nem', 3, true, true, 'man'),
+
+ // children (child)
+ array('nerdlihc', 8, true, true, 'child'),
+
+ // oxen (ox)
+ array('nexo', 4, false, false, 'ox'),
+
+ // indices (index), appendices (appendix), prices (price)
+ array('seci', 4, false, true, array('ex', 'ix', 'ice')),
+
+ // babies (baby)
+ array('sei', 3, false, true, 'y'),
+
+ // analyses (analysis), ellipses (ellipsis), funguses (fungus),
+ // neuroses (neurosis), theses (thesis), emphases (emphasis),
+ // oases (oasis), crises (crisis), houses (house), bases (base),
+ // atlases (atlas), kisses (kiss)
+ array('ses', 3, true, true, array('s', 'se', 'sis')),
+
+ // objectives (objective), alternative (alternatives)
+ array('sevit', 5, true, true, 'tive'),
+
+ // lives (life), wives (wife)
+ array('sevi', 4, false, true, 'ife'),
+
+ // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
+ array('sev', 3, true, true, 'f'),
+
+ // axes (axis), axes (ax), axes (axe)
+ array('sexa', 4, false, false, array('ax', 'axe', 'axis')),
+
+ // indexes (index), matrixes (matrix)
+ array('sex', 3, true, false, 'x'),
+
+ // quizzes (quiz)
+ array('sezz', 4, true, false, 'z'),
+
+ // bureaus (bureau)
+ array('suae', 4, false, true, 'eau'),
+
+ // roses (rose), garages (garage), cassettes (cassette),
+ // waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
+ // shoes (shoe)
+ array('se', 2, true, true, array('', 'e')),
+
+ // tags (tag)
+ array('s', 1, true, true, ''),
+
+ // chateaux (chateau)
+ array('xuae', 4, false, true, 'eau'),
+ );
+
+ /**
+ * This class should not be instantiated
+ */
+ private function __construct() {}
+
+ /**
+ * Returns the singular form of a word
+ *
+ * If the method can't determine the form with certainty, an array of the
+ * possible singulars is returned.
+ *
+ * @param string $plural A word in plural form
+ * @return string|array The singular form or an array of possible singular
+ * forms
+ */
+ public static function singularify($plural)
+ {
+ $pluralRev = strrev($plural);
+ $lowerPluralRev = strtolower($pluralRev);
+ $pluralLength = strlen($lowerPluralRev);
+
+ // The outer loop iterates over the entries of the plural table
+ // The inner loop $j iterates over the characters of the plural suffix
+ // in the plural table to compare them with the characters of the actual
+ // given plural suffix
+ foreach (self::$pluralMap as $map) {
+ $suffix = $map[0];
+ $suffixLength = $map[1];
+ $j = 0;
+
+ // Compare characters in the plural table and of the suffix of the
+ // given plural one by one
+ while ($suffix[$j] === $lowerPluralRev[$j]) {
+ // Let $j point to the next character
+ ++$j;
+
+ // Successfully compared the last character
+ // Add an entry with the singular suffix to the singular array
+ if ($j === $suffixLength) {
+ // Is there any character preceding the suffix in the plural string?
+ if ($j < $pluralLength) {
+ $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
+
+ if (!$map[2] && $nextIsVocal) {
+ // suffix may not succeed a vocal but next char is one
+ break;
+ }
+
+ if (!$map[3] && !$nextIsVocal) {
+ // suffix may not succeed a consonant but next char is one
+ break;
+ }
+ }
+
+ $newBase = substr($plural, 0, $pluralLength - $suffixLength);
+ $newSuffix = $map[4];
+
+ // Check whether the first character in the plural suffix
+ // is uppercased. If yes, uppercase the first character in
+ // the singular suffix too
+ $firstUpper = ctype_upper($pluralRev[$j - 1]);
+
+ if (is_array($newSuffix)) {
+ $singulars = array();
+
+ foreach ($newSuffix as $newSuffixEntry) {
+ $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
+ }
+
+ return $singulars;
+ }
+
+ return $newBase.($firstUpper ? ucFirst($newSuffix) : $newSuffix);
+ }
+
+ // Suffix is longer than word
+ if ($j === $pluralLength) {
+ break;
+ }
+ }
+ }
+
+ // Convert teeth to tooth, feet to foot
+ if (false !== ($pos = strpos($plural, 'ee'))) {
+ return substr_replace($plural, 'oo', $pos, 2);
+ }
+
+ // Assume that plural and singular is identical
+ return $plural;
+ }
+}
--- /dev/null
+{
+ "name": "symfony/property-access",
+ "type": "library",
+ "description": "Symfony PropertyAccess Component",
+ "keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"],
+ "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\\PropertyAccess\\": "" }
+ },
+ "target-dir": "Symfony/Component/PropertyAccess",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+<?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\Routing\Annotation;
+
+/**
+ * Annotation class for @Route().
+ *
+ * @Annotation
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Route
+{
+ private $path;
+ private $name;
+ private $requirements;
+ private $options;
+ private $defaults;
+ private $host;
+ private $methods;
+ private $schemes;
+
+ /**
+ * Constructor.
+ *
+ * @param array $data An array of key/value parameters.
+ *
+ * @throws \BadMethodCallException
+ */
+ public function __construct(array $data)
+ {
+ $this->requirements = array();
+ $this->options = array();
+ $this->defaults = array();
+ $this->methods = array();
+ $this->schemes = array();
+
+ if (isset($data['value'])) {
+ $data['path'] = $data['value'];
+ unset($data['value']);
+ }
+
+ foreach ($data as $key => $value) {
+ $method = 'set'.str_replace('_', '', $key);
+ if (!method_exists($this, $method)) {
+ throw new \BadMethodCallException(sprintf("Unknown property '%s' on annotation '%s'.", $key, get_class($this)));
+ }
+ $this->$method($value);
+ }
+ }
+
+ /**
+ * @deprecated Deprecated in 2.2, to be removed in 3.0. Use setPath instead.
+ */
+ public function setPattern($pattern)
+ {
+ $this->path = $pattern;
+ }
+
+ /**
+ * @deprecated Deprecated in 2.2, to be removed in 3.0. Use getPath instead.
+ */
+ public function getPattern()
+ {
+ return $this->path;
+ }
+
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function setHost($pattern)
+ {
+ $this->host = $pattern;
+ }
+
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setRequirements($requirements)
+ {
+ $this->requirements = $requirements;
+ }
+
+ public function getRequirements()
+ {
+ return $this->requirements;
+ }
+
+ public function setOptions($options)
+ {
+ $this->options = $options;
+ }
+
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ public function setDefaults($defaults)
+ {
+ $this->defaults = $defaults;
+ }
+
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+
+ public function setSchemes($schemes)
+ {
+ $this->schemes = is_array($schemes) ? $schemes : array($schemes);
+ }
+
+ public function getSchemes()
+ {
+ return $this->schemes;
+ }
+
+ public function setMethods($methods)
+ {
+ $this->methods = is_array($methods) ? $methods : array($methods);
+ }
+
+ public function getMethods()
+ {
+ return $this->methods;
+ }
+}
--- /dev/null
+CHANGELOG
+=========
+
+2.3.0
+-----
+
+ * added RequestContext::getQueryString()
+
+2.2.0
+-----
+
+ * [DEPRECATION] Several route settings have been renamed (the old ones will be removed in 3.0):
+
+ * The `pattern` setting for a route has been deprecated in favor of `path`
+ * The `_scheme` and `_method` requirements have been moved to the `schemes` and `methods` settings
+
+ Before:
+
+ ```
+ article_edit:
+ pattern: /article/{id}
+ requirements: { '_method': 'POST|PUT', '_scheme': 'https', 'id': '\d+' }
+
+ <route id="article_edit" pattern="/article/{id}">
+ <requirement key="_method">POST|PUT</requirement>
+ <requirement key="_scheme">https</requirement>
+ <requirement key="id">\d+</requirement>
+ </route>
+
+ $route = new Route();
+ $route->setPattern('/article/{id}');
+ $route->setRequirement('_method', 'POST|PUT');
+ $route->setRequirement('_scheme', 'https');
+ ```
+
+ After:
+
+ ```
+ article_edit:
+ path: /article/{id}
+ methods: [POST, PUT]
+ schemes: https
+ requirements: { 'id': '\d+' }
+
+ <route id="article_edit" pattern="/article/{id}" methods="POST PUT" schemes="https">
+ <requirement key="id">\d+</requirement>
+ </route>
+
+ $route = new Route();
+ $route->setPath('/article/{id}');
+ $route->setMethods(array('POST', 'PUT'));
+ $route->setSchemes('https');
+ ```
+
+ * [BC BREAK] RouteCollection does not behave like a tree structure anymore but as
+ a flat array of Routes. So when using PHP to build the RouteCollection, you must
+ make sure to add routes to the sub-collection before adding it to the parent
+ collection (this is not relevant when using YAML or XML for Route definitions).
+
+ Before:
+
+ ```
+ $rootCollection = new RouteCollection();
+ $subCollection = new RouteCollection();
+ $rootCollection->addCollection($subCollection);
+ $subCollection->add('foo', new Route('/foo'));
+ ```
+
+ After:
+
+ ```
+ $rootCollection = new RouteCollection();
+ $subCollection = new RouteCollection();
+ $subCollection->add('foo', new Route('/foo'));
+ $rootCollection->addCollection($subCollection);
+ ```
+
+ Also one must call `addCollection` from the bottom to the top hierarchy.
+ So the correct sequence is the following (and not the reverse):
+
+ ```
+ $childCollection->->addCollection($grandchildCollection);
+ $rootCollection->addCollection($childCollection);
+ ```
+
+ * [DEPRECATION] The methods `RouteCollection::getParent()` and `RouteCollection::getRoot()`
+ have been deprecated and will be removed in Symfony 2.3.
+ * [BC BREAK] Misusing the `RouteCollection::addPrefix` method to add defaults, requirements
+ or options without adding a prefix is not supported anymore. So if you called `addPrefix`
+ with an empty prefix or `/` only (both have no relevance), like
+ `addPrefix('', $defaultsArray, $requirementsArray, $optionsArray)`
+ you need to use the new dedicated methods `addDefaults($defaultsArray)`,
+ `addRequirements($requirementsArray)` or `addOptions($optionsArray)` instead.
+ * [DEPRECATION] The `$options` parameter to `RouteCollection::addPrefix()` has been deprecated
+ because adding options has nothing to do with adding a path prefix. If you want to add options
+ to all child routes of a RouteCollection, you can use `addOptions()`.
+ * [DEPRECATION] The method `RouteCollection::getPrefix()` has been deprecated
+ because it suggested that all routes in the collection would have this prefix, which is
+ not necessarily true. On top of that, since there is no tree structure anymore, this method
+ is also useless. Don't worry about performance, prefix optimization for matching is still done
+ in the dumper, which was also improved in 2.2.0 to find even more grouping possibilities.
+ * [DEPRECATION] `RouteCollection::addCollection(RouteCollection $collection)` should now only be
+ used with a single parameter. The other params `$prefix`, `$default`, `$requirements` and `$options`
+ will still work, but have been deprecated. The `addPrefix` method should be used for this
+ use-case instead.
+ Before: `$parentCollection->addCollection($collection, '/prefix', array(...), array(...))`
+ After:
+ ```
+ $collection->addPrefix('/prefix', array(...), array(...));
+ $parentCollection->addCollection($collection);
+ ```
+ * added support for the method default argument values when defining a @Route
+ * Adjacent placeholders without separator work now, e.g. `/{x}{y}{z}.{_format}`.
+ * Characters that function as separator between placeholders are now whitelisted
+ to fix routes with normal text around a variable, e.g. `/prefix{var}suffix`.
+ * [BC BREAK] The default requirement of a variable has been changed slightly.
+ Previously it disallowed the previous and the next char around a variable. Now
+ it disallows the slash (`/`) and the next char. Using the previous char added
+ no value and was problematic because the route `/index.{_format}` would be
+ matched by `/index.ht/ml`.
+ * The default requirement now uses possessive quantifiers when possible which
+ improves matching performance by up to 20% because it prevents backtracking
+ when it's not needed.
+ * The ConfigurableRequirementsInterface can now also be used to disable the requirements
+ check on URL generation completely by calling `setStrictRequirements(null)`. It
+ improves performance in production environment as you should know that params always
+ pass the requirements (otherwise it would break your link anyway).
+ * There is no restriction on the route name anymore. So non-alphanumeric characters
+ are now also allowed.
+ * [BC BREAK] `RouteCompilerInterface::compile(Route $route)` was made static
+ (only relevant if you implemented your own RouteCompiler).
+ * Added possibility to generate relative paths and network paths in the UrlGenerator, e.g.
+ "../parent-file" and "//example.com/dir/file". The third parameter in
+ `UrlGeneratorInterface::generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)`
+ now accepts more values and you should use the constants defined in `UrlGeneratorInterface` for
+ claritiy. The old method calls with a Boolean parameter will continue to work because they
+ equal the signature using the constants.
+
+2.1.0
+-----
+
+ * added RequestMatcherInterface
+ * added RequestContext::fromRequest()
+ * the UrlMatcher does not throw a \LogicException anymore when the required
+ scheme is not the current one
+ * added TraceableUrlMatcher
+ * added the possibility to define options, default values and requirements
+ for placeholders in prefix, including imported routes
+ * added RouterInterface::getRouteCollection
+ * [BC BREAK] the UrlMatcher urldecodes the route parameters only once, they
+ were decoded twice before. Note that the `urldecode()` calls have been
+ changed for a single `rawurldecode()` in order to support `+` for input
+ paths.
+ * added RouteCollection::getRoot method to retrieve the root of a
+ RouteCollection tree
+ * [BC BREAK] made RouteCollection::setParent private which could not have
+ been used anyway without creating inconsistencies
+ * [BC BREAK] RouteCollection::remove also removes a route from parent
+ collections (not only from its children)
+ * added ConfigurableRequirementsInterface that allows to disable exceptions
+ (and generate empty URLs instead) when generating a route with an invalid
+ parameter value
--- /dev/null
+<?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\Routing;
+
+/**
+ * CompiledRoutes are returned by the RouteCompiler class.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class CompiledRoute
+{
+ private $variables;
+ private $tokens;
+ private $staticPrefix;
+ private $regex;
+ private $pathVariables;
+ private $hostVariables;
+ private $hostRegex;
+ private $hostTokens;
+
+ /**
+ * Constructor.
+ *
+ * @param string $staticPrefix The static prefix of the compiled route
+ * @param string $regex The regular expression to use to match this route
+ * @param array $tokens An array of tokens to use to generate URL for this route
+ * @param array $pathVariables An array of path variables
+ * @param string|null $hostRegex Host regex
+ * @param array $hostTokens Host tokens
+ * @param array $hostVariables An array of host variables
+ * @param array $variables An array of variables (variables defined in the path and in the host patterns)
+ */
+ public function __construct($staticPrefix, $regex, array $tokens, array $pathVariables, $hostRegex = null, array $hostTokens = array(), array $hostVariables = array(), array $variables = array())
+ {
+ $this->staticPrefix = (string) $staticPrefix;
+ $this->regex = $regex;
+ $this->tokens = $tokens;
+ $this->pathVariables = $pathVariables;
+ $this->hostRegex = $hostRegex;
+ $this->hostTokens = $hostTokens;
+ $this->hostVariables = $hostVariables;
+ $this->variables = $variables;
+ }
+
+ /**
+ * Returns the static prefix.
+ *
+ * @return string The static prefix
+ */
+ public function getStaticPrefix()
+ {
+ return $this->staticPrefix;
+ }
+
+ /**
+ * Returns the regex.
+ *
+ * @return string The regex
+ */
+ public function getRegex()
+ {
+ return $this->regex;
+ }
+
+ /**
+ * Returns the host regex
+ *
+ * @return string|null The host regex or null
+ */
+ public function getHostRegex()
+ {
+ return $this->hostRegex;
+ }
+
+ /**
+ * Returns the tokens.
+ *
+ * @return array The tokens
+ */
+ public function getTokens()
+ {
+ return $this->tokens;
+ }
+
+ /**
+ * Returns the host tokens.
+ *
+ * @return array The tokens
+ */
+ public function getHostTokens()
+ {
+ return $this->hostTokens;
+ }
+
+ /**
+ * Returns the variables.
+ *
+ * @return array The variables
+ */
+ public function getVariables()
+ {
+ return $this->variables;
+ }
+
+ /**
+ * Returns the path variables.
+ *
+ * @return array The variables
+ */
+ public function getPathVariables()
+ {
+ return $this->pathVariables;
+ }
+
+ /**
+ * Returns the host variables.
+ *
+ * @return array The variables
+ */
+ public function getHostVariables()
+ {
+ return $this->hostVariables;
+ }
+
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * ExceptionInterface
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * Exception thrown when a parameter is not valid
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class InvalidParameterException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * The resource was found but the request method is not allowed.
+ *
+ * This exception should trigger an HTTP 405 response in your application code.
+ *
+ * @author Kris Wallsmith <kris@symfony.com>
+ *
+ * @api
+ */
+class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface
+{
+ /**
+ * @var array
+ */
+ protected $allowedMethods = array();
+
+ public function __construct(array $allowedMethods, $message = null, $code = 0, \Exception $previous = null)
+ {
+ $this->allowedMethods = array_map('strtoupper', $allowedMethods);
+
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Gets the allowed HTTP methods.
+ *
+ * @return array
+ */
+ public function getAllowedMethods()
+ {
+ return $this->allowedMethods;
+ }
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * Exception thrown when a route cannot be generated because of missing
+ * mandatory parameters.
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * The resource was not found.
+ *
+ * This exception should trigger an HTTP 404 response in your application code.
+ *
+ * @author Kris Wallsmith <kris@symfony.com>
+ *
+ * @api
+ */
+class ResourceNotFoundException extends \RuntimeException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Routing\Exception;
+
+/**
+ * Exception thrown when a route does not exists
+ *
+ * @author Alexandre Salomé <alexandre.salome@gmail.com>
+ *
+ * @api
+ */
+class RouteNotFoundException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Routing\Generator;
+
+/**
+ * ConfigurableRequirementsInterface must be implemented by URL generators that
+ * can be configured whether an exception should be generated when the parameters
+ * do not match the requirements. It is also possible to disable the requirements
+ * check for URL generation completely.
+ *
+ * The possible configurations and use-cases:
+ * - setStrictRequirements(true): Throw an exception for mismatching requirements. This
+ * is mostly useful in development environment.
+ * - setStrictRequirements(false): Don't throw an exception but return null as URL for
+ * mismatching requirements and log the problem. Useful when you cannot control all
+ * params because they come from third party libs but don't want to have a 404 in
+ * production environment. It should log the mismatch so one can review it.
+ * - setStrictRequirements(null): Return the URL with the given parameters without
+ * checking the requirements at all. When generating an URL you should either trust
+ * your params or you validated them beforehand because otherwise it would break your
+ * link anyway. So in production environment you should know that params always pass
+ * the requirements. Thus this option allows to disable the check on URL generation for
+ * performance reasons (saving a preg_match for each requirement every time a URL is
+ * generated).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ */
+interface ConfigurableRequirementsInterface
+{
+ /**
+ * Enables or disables the exception on incorrect parameters.
+ * Passing null will deactivate the requirements check completely.
+ *
+ * @param Boolean|null $enabled
+ */
+ public function setStrictRequirements($enabled);
+
+ /**
+ * Returns whether to throw an exception on incorrect parameters.
+ * Null means the requirements check is deactivated completely.
+ *
+ * @return Boolean|null
+ */
+ public function isStrictRequirements();
+}
--- /dev/null
+<?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\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumper is the base class for all built-in generator dumpers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class GeneratorDumper implements GeneratorDumperInterface
+{
+ /**
+ * @var RouteCollection
+ */
+ private $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes The RouteCollection to dump
+ */
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
--- /dev/null
+<?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\Routing\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * GeneratorDumperInterface is the interface that all generator dumper classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface GeneratorDumperInterface
+{
+ /**
+ * Dumps a set of routes to a string representation of executable code
+ * that can then be used to generate a URL of such a route.
+ *
+ * @param array $options An array of options
+ *
+ * @return string Executable code
+ */
+ public function dump(array $options = array());
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRoutes();
+}
--- /dev/null
+<?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\Routing\Generator\Dumper;
+
+/**
+ * PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class PhpGeneratorDumper extends GeneratorDumper
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the generator class
+ *
+ * @api
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_merge(array(
+ 'class' => 'ProjectUrlGenerator',
+ 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ ), $options);
+
+ return <<<EOF
+<?php
+
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * {$options['class']}
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class {$options['class']} extends {$options['base_class']}
+{
+ static private \$declaredRoutes = {$this->generateDeclaredRoutes()};
+
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext \$context, LoggerInterface \$logger = null)
+ {
+ \$this->context = \$context;
+ \$this->logger = \$logger;
+ }
+
+{$this->generateGenerateMethod()}
+}
+
+EOF;
+ }
+
+ /**
+ * Generates PHP code representing an array of defined routes
+ * together with the routes properties (e.g. requirements).
+ *
+ * @return string PHP code
+ */
+ private function generateDeclaredRoutes()
+ {
+ $routes = "array(\n";
+ foreach ($this->getRoutes()->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ $properties = array();
+ $properties[] = $compiledRoute->getVariables();
+ $properties[] = $route->getDefaults();
+ $properties[] = $route->getRequirements();
+ $properties[] = $compiledRoute->getTokens();
+ $properties[] = $compiledRoute->getHostTokens();
+
+ $routes .= sprintf(" '%s' => %s,\n", $name, str_replace("\n", '', var_export($properties, true)));
+ }
+ $routes .= ' )';
+
+ return $routes;
+ }
+
+ /**
+ * Generates PHP code representing the `generate` method that implements the UrlGeneratorInterface.
+ *
+ * @return string PHP code
+ */
+ private function generateGenerateMethod()
+ {
+ return <<<EOF
+ public function generate(\$name, \$parameters = array(), \$referenceType = self::ABSOLUTE_PATH)
+ {
+ if (!isset(self::\$declaredRoutes[\$name])) {
+ throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', \$name));
+ }
+
+ list(\$variables, \$defaults, \$requirements, \$tokens, \$hostTokens) = self::\$declaredRoutes[\$name];
+
+ return \$this->doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostTokens);
+ }
+EOF;
+ }
+}
--- /dev/null
+<?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\Routing\Generator;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * UrlGenerator can generate a URL or a path for any route in the RouteCollection
+ * based on the passed parameters.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface
+{
+ /**
+ * @var RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ /**
+ * @var Boolean|null
+ */
+ protected $strictRequirements = true;
+
+ /**
+ * @var LoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL.
+ *
+ * PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars
+ * to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g.
+ * "?" and "#" (would be interpreted wrongly as query and fragment identifier),
+ * "'" and """ (are used as delimiters in HTML).
+ */
+ protected $decodedChars = array(
+ // the slash can be used to designate a hierarchical structure and we want allow using it with this meaning
+ // some webservers don't allow the slash in encoded form in the path for security reasons anyway
+ // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss
+ '%2F' => '/',
+ // the following chars are general delimiters in the URI specification but have only special meaning in the authority component
+ // so they can safely be used in the path in unencoded form
+ '%40' => '@',
+ '%3A' => ':',
+ // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally
+ // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability
+ '%3B' => ';',
+ '%2C' => ',',
+ '%3D' => '=',
+ '%2B' => '+',
+ '%21' => '!',
+ '%2A' => '*',
+ '%7C' => '|',
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes A RouteCollection instance
+ * @param RequestContext $context The context
+ * @param LoggerInterface|null $logger A logger instance
+ *
+ * @api
+ */
+ public function __construct(RouteCollection $routes, RequestContext $context, LoggerInterface $logger = null)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStrictRequirements($enabled)
+ {
+ $this->strictRequirements = null === $enabled ? null : (Boolean) $enabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isStrictRequirements()
+ {
+ return $this->strictRequirements;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
+ {
+ if (null === $route = $this->routes->get($name)) {
+ throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name));
+ }
+
+ // the Route has a cache of its own and is not recompiled as long as it does not get modified
+ $compiledRoute = $route->compile();
+
+ return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens());
+ }
+
+ /**
+ * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
+ * @throws InvalidParameterException When a parameter value for a placeholder is not correct because
+ * it does not match the requirement
+ */
+ protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens)
+ {
+ $variables = array_flip($variables);
+ $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters);
+
+ // all params must be given
+ if ($diff = array_diff_key($variables, $mergedParams)) {
+ throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name));
+ }
+
+ $url = '';
+ $optional = true;
+ foreach ($tokens as $token) {
+ if ('variable' === $token[0]) {
+ if (!$optional || !array_key_exists($token[3], $defaults) || null !== $mergedParams[$token[3]] && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]]) {
+ // check requirement
+ if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
+ $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]);
+ if ($this->strictRequirements) {
+ throw new InvalidParameterException($message);
+ }
+
+ if ($this->logger) {
+ $this->logger->error($message);
+ }
+
+ return null;
+ }
+
+ $url = $token[1].$mergedParams[$token[3]].$url;
+ $optional = false;
+ }
+ } else {
+ // static text
+ $url = $token[1].$url;
+ $optional = false;
+ }
+ }
+
+ if ('' === $url) {
+ $url = '/';
+ }
+
+ // the contexts base url is already encoded (see Symfony\Component\HttpFoundation\Request)
+ $url = strtr(rawurlencode($url), $this->decodedChars);
+
+ // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3
+ // so we need to encode them as they are not used for this purpose here
+ // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route
+ $url = strtr($url, array('/../' => '/%2E%2E/', '/./' => '/%2E/'));
+ if ('/..' === substr($url, -3)) {
+ $url = substr($url, 0, -2).'%2E%2E';
+ } elseif ('/.' === substr($url, -2)) {
+ $url = substr($url, 0, -1).'%2E';
+ }
+
+ $schemeAuthority = '';
+ if ($host = $this->context->getHost()) {
+ $scheme = $this->context->getScheme();
+ if (isset($requirements['_scheme']) && ($req = strtolower($requirements['_scheme'])) && $scheme !== $req) {
+ $referenceType = self::ABSOLUTE_URL;
+ $scheme = $req;
+ }
+
+ if ($hostTokens) {
+ $routeHost = '';
+ foreach ($hostTokens as $token) {
+ if ('variable' === $token[0]) {
+ if (null !== $this->strictRequirements && !preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) {
+ $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]);
+
+ if ($this->strictRequirements) {
+ throw new InvalidParameterException($message);
+ }
+
+ if ($this->logger) {
+ $this->logger->error($message);
+ }
+
+ return null;
+ }
+
+ $routeHost = $token[1].$mergedParams[$token[3]].$routeHost;
+ } else {
+ $routeHost = $token[1].$routeHost;
+ }
+ }
+
+ if ($routeHost !== $host) {
+ $host = $routeHost;
+ if (self::ABSOLUTE_URL !== $referenceType) {
+ $referenceType = self::NETWORK_PATH;
+ }
+ }
+ }
+
+ if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
+ $port = '';
+ if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
+ $port = ':'.$this->context->getHttpPort();
+ } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
+ $port = ':'.$this->context->getHttpsPort();
+ }
+
+ $schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
+ $schemeAuthority .= $host.$port;
+ }
+ }
+
+ if (self::RELATIVE_PATH === $referenceType) {
+ $url = self::getRelativePath($this->context->getPathInfo(), $url);
+ } else {
+ $url = $schemeAuthority.$this->context->getBaseUrl().$url;
+ }
+
+ // add a query string if needed
+ $extra = array_diff_key($parameters, $variables, $defaults);
+ if ($extra && $query = http_build_query($extra, '', '&')) {
+ $url .= '?'.$query;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Returns the target path as relative reference from the base path.
+ *
+ * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash.
+ * Both paths must be absolute and not contain relative parts.
+ * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives.
+ * Furthermore, they can be used to reduce the link size in documents.
+ *
+ * Example target paths, given a base path of "/a/b/c/d":
+ * - "/a/b/c/d" -> ""
+ * - "/a/b/c/" -> "./"
+ * - "/a/b/" -> "../"
+ * - "/a/b/c/other" -> "other"
+ * - "/a/x/y" -> "../../x/y"
+ *
+ * @param string $basePath The base path
+ * @param string $targetPath The target path
+ *
+ * @return string The relative target path
+ */
+ public static function getRelativePath($basePath, $targetPath)
+ {
+ if ($basePath === $targetPath) {
+ return '';
+ }
+
+ $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath);
+ $targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath);
+ array_pop($sourceDirs);
+ $targetFile = array_pop($targetDirs);
+
+ foreach ($sourceDirs as $i => $dir) {
+ if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) {
+ unset($sourceDirs[$i], $targetDirs[$i]);
+ } else {
+ break;
+ }
+ }
+
+ $targetDirs[] = $targetFile;
+ $path = str_repeat('../', count($sourceDirs)).implode('/', $targetDirs);
+
+ // A reference to the same base directory or an empty subdirectory must be prefixed with "./".
+ // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used
+ // as the first segment of a relative-path reference, as it would be mistaken for a scheme name
+ // (see http://tools.ietf.org/html/rfc3986#section-4.2).
+ return '' === $path || '/' === $path[0]
+ || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos)
+ ? "./$path" : $path;
+ }
+}
--- /dev/null
+<?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\Routing\Generator;
+
+use Symfony\Component\Routing\Exception\InvalidParameterException;
+use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\RequestContextAwareInterface;
+
+/**
+ * UrlGeneratorInterface is the interface that all URL generator classes must implement.
+ *
+ * The constants in this interface define the different types of resource references that
+ * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986
+ * We are using the term "URL" instead of "URI" as this is more common in web applications
+ * and we do not need to distinguish them as the difference is mostly semantical and
+ * less technical. Generating URIs, i.e. representation-independent resource identifiers,
+ * is also possible.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+interface UrlGeneratorInterface extends RequestContextAwareInterface
+{
+ /**
+ * Generates an absolute URL, e.g. "http://example.com/dir/file".
+ */
+ const ABSOLUTE_URL = true;
+
+ /**
+ * Generates an absolute path, e.g. "/dir/file".
+ */
+ const ABSOLUTE_PATH = false;
+
+ /**
+ * Generates a relative path based on the current request path, e.g. "../parent-file".
+ * @see UrlGenerator::getRelativePath()
+ */
+ const RELATIVE_PATH = 'relative';
+
+ /**
+ * Generates a network path, e.g. "//example.com/dir/file".
+ * Such reference reuses the current scheme but specifies the host.
+ */
+ const NETWORK_PATH = 'network';
+
+ /**
+ * Generates a URL or path for a specific route based on the given parameters.
+ *
+ * Parameters that reference placeholders in the route pattern will substitute them in the
+ * path or host. Extra params are added as query string to the URL.
+ *
+ * When the passed reference type cannot be generated for the route because it requires a different
+ * host or scheme than the current one, the method will return a more comprehensive reference
+ * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH
+ * but the route requires the https scheme whereas the current scheme is http, it will instead return an
+ * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches
+ * the route in any case.
+ *
+ * If there is no route with the given name, the generator must throw the RouteNotFoundException.
+ *
+ * @param string $name The name of the route
+ * @param mixed $parameters An array of parameters
+ * @param Boolean|string $referenceType The type of reference to be generated (one of the constants)
+ *
+ * @return string The generated URL
+ *
+ * @throws RouteNotFoundException If the named route doesn't exist
+ * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route
+ * @throws InvalidParameterException When a parameter value for a placeholder is not correct because
+ * it does not match the requirement
+ *
+ * @api
+ */
+ public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH);
+}
--- /dev/null
+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.
--- /dev/null
+<?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\Routing\Loader;
+
+use Doctrine\Common\Annotations\Reader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\Loader\LoaderResolverInterface;
+
+/**
+ * AnnotationClassLoader loads routing information from a PHP class and its methods.
+ *
+ * You need to define an implementation for the getRouteDefaults() method. Most of the
+ * time, this method should define some PHP callable to be called for the route
+ * (a controller in MVC speak).
+ *
+ * The @Route annotation can be set on the class (for global parameters),
+ * and on each method.
+ *
+ * The @Route annotation main value is the route path. The annotation also
+ * recognizes several parameters: requirements, options, defaults, schemes,
+ * methods, host, and name. The name parameter is mandatory.
+ * Here is an example of how you should be able to use it:
+ *
+ * /**
+ * * @Route("/Blog")
+ * * /
+ * class Blog
+ * {
+ * /**
+ * * @Route("/", name="blog_index")
+ * * /
+ * public function index()
+ * {
+ * }
+ *
+ * /**
+ * * @Route("/{id}", name="blog_post", requirements = {"id" = "\d+"})
+ * * /
+ * public function show()
+ * {
+ * }
+ * }
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class AnnotationClassLoader implements LoaderInterface
+{
+ /**
+ * @var Reader
+ */
+ protected $reader;
+
+ /**
+ * @var string
+ */
+ protected $routeAnnotationClass = 'Symfony\\Component\\Routing\\Annotation\\Route';
+
+ /**
+ * @var integer
+ */
+ protected $defaultRouteIndex = 0;
+
+ /**
+ * Constructor.
+ *
+ * @param Reader $reader
+ */
+ public function __construct(Reader $reader)
+ {
+ $this->reader = $reader;
+ }
+
+ /**
+ * Sets the annotation class to read route properties from.
+ *
+ * @param string $class A fully-qualified class name
+ */
+ public function setRouteAnnotationClass($class)
+ {
+ $this->routeAnnotationClass = $class;
+ }
+
+ /**
+ * Loads from annotations from a class.
+ *
+ * @param string $class A class name
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When route can't be parsed
+ */
+ public function load($class, $type = null)
+ {
+ if (!class_exists($class)) {
+ throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class));
+ }
+
+ $globals = array(
+ 'path' => '',
+ 'requirements' => array(),
+ 'options' => array(),
+ 'defaults' => array(),
+ 'schemes' => array(),
+ 'methods' => array(),
+ 'host' => '',
+ );
+
+ $class = new \ReflectionClass($class);
+ if ($class->isAbstract()) {
+ throw new \InvalidArgumentException(sprintf('Annotations from class "%s" cannot be read as it is abstract.', $class));
+ }
+
+ if ($annot = $this->reader->getClassAnnotation($class, $this->routeAnnotationClass)) {
+ // for BC reasons
+ if (null !== $annot->getPath()) {
+ $globals['path'] = $annot->getPath();
+ } elseif (null !== $annot->getPattern()) {
+ $globals['path'] = $annot->getPattern();
+ }
+
+ if (null !== $annot->getRequirements()) {
+ $globals['requirements'] = $annot->getRequirements();
+ }
+
+ if (null !== $annot->getOptions()) {
+ $globals['options'] = $annot->getOptions();
+ }
+
+ if (null !== $annot->getDefaults()) {
+ $globals['defaults'] = $annot->getDefaults();
+ }
+
+ if (null !== $annot->getSchemes()) {
+ $globals['schemes'] = $annot->getSchemes();
+ }
+
+ if (null !== $annot->getMethods()) {
+ $globals['methods'] = $annot->getMethods();
+ }
+
+ if (null !== $annot->getHost()) {
+ $globals['host'] = $annot->getHost();
+ }
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($class->getFileName()));
+
+ foreach ($class->getMethods() as $method) {
+ $this->defaultRouteIndex = 0;
+ foreach ($this->reader->getMethodAnnotations($method) as $annot) {
+ if ($annot instanceof $this->routeAnnotationClass) {
+ $this->addRoute($collection, $annot, $globals, $class, $method);
+ }
+ }
+ }
+
+ return $collection;
+ }
+
+ protected function addRoute(RouteCollection $collection, $annot, $globals, \ReflectionClass $class, \ReflectionMethod $method)
+ {
+ $name = $annot->getName();
+ if (null === $name) {
+ $name = $this->getDefaultRouteName($class, $method);
+ }
+
+ $defaults = array_replace($globals['defaults'], $annot->getDefaults());
+ foreach ($method->getParameters() as $param) {
+ if ($param->isOptional()) {
+ $defaults[$param->getName()] = $param->getDefaultValue();
+ }
+ }
+ $requirements = array_replace($globals['requirements'], $annot->getRequirements());
+ $options = array_replace($globals['options'], $annot->getOptions());
+ $schemes = array_replace($globals['schemes'], $annot->getSchemes());
+ $methods = array_replace($globals['methods'], $annot->getMethods());
+
+ $host = $annot->getHost();
+ if (null === $host) {
+ $host = $globals['host'];
+ }
+
+ $route = new Route($globals['path'].$annot->getPath(), $defaults, $requirements, $options, $host, $schemes, $methods);
+
+ $this->configureRoute($route, $class, $method, $annot);
+
+ $collection->add($name, $route);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResolver(LoaderResolverInterface $resolver)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResolver()
+ {
+ }
+
+ /**
+ * Gets the default route name for a class method.
+ *
+ * @param \ReflectionClass $class
+ * @param \ReflectionMethod $method
+ *
+ * @return string
+ */
+ protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method)
+ {
+ $name = strtolower(str_replace('\\', '_', $class->name).'_'.$method->name);
+ if ($this->defaultRouteIndex > 0) {
+ $name .= '_'.$this->defaultRouteIndex;
+ }
+ $this->defaultRouteIndex++;
+
+ return $name;
+ }
+
+ abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot);
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Resource\DirectoryResource;
+
+/**
+ * AnnotationDirectoryLoader loads routing information from annotations set
+ * on PHP classes and methods.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class AnnotationDirectoryLoader extends AnnotationFileLoader
+{
+ /**
+ * Loads from annotations from a directory.
+ *
+ * @param string $path A directory path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed
+ */
+ public function load($path, $type = null)
+ {
+ $dir = $this->locator->locate($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new DirectoryResource($dir, '/\.php$/'));
+ $files = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY));
+ usort($files, function (\SplFileInfo $a, \SplFileInfo $b) {
+ return (string) $a > (string) $b ? 1 : -1;
+ });
+
+ foreach ($files as $file) {
+ if (!$file->isFile() || '.php' !== substr($file->getFilename(), -4)) {
+ continue;
+ }
+
+ if ($class = $this->findClass($file)) {
+ $refl = new \ReflectionClass($class);
+ if ($refl->isAbstract()) {
+ continue;
+ }
+
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, $type = null)
+ {
+ try {
+ $path = $this->locator->locate($resource);
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return is_string($resource) && is_dir($path) && (!$type || 'annotation' === $type);
+ }
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\FileLocatorInterface;
+
+/**
+ * AnnotationFileLoader loads routing information from annotations set
+ * on a PHP class and its methods.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class AnnotationFileLoader extends FileLoader
+{
+ protected $loader;
+
+ /**
+ * Constructor.
+ *
+ * @param FileLocatorInterface $locator A FileLocator instance
+ * @param AnnotationClassLoader $loader An AnnotationClassLoader instance
+ * @param string|array $paths A path or an array of paths where to look for resources
+ *
+ * @throws \RuntimeException
+ */
+ public function __construct(FileLocatorInterface $locator, AnnotationClassLoader $loader, $paths = array())
+ {
+ if (!function_exists('token_get_all')) {
+ throw new \RuntimeException('The Tokenizer extension is required for the routing annotation loaders.');
+ }
+
+ parent::__construct($locator, $paths);
+
+ $this->loader = $loader;
+ }
+
+ /**
+ * Loads from annotations from a file.
+ *
+ * @param string $file A PHP file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $collection = new RouteCollection();
+ if ($class = $this->findClass($path)) {
+ $collection->addResource(new FileResource($path));
+ $collection->addCollection($this->loader->load($class, $type));
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'annotation' === $type);
+ }
+
+ /**
+ * Returns the full class name for the first class in the file.
+ *
+ * @param string $file A PHP file path
+ *
+ * @return string|false Full class name if found, false otherwise
+ */
+ protected function findClass($file)
+ {
+ $class = false;
+ $namespace = false;
+ $tokens = token_get_all(file_get_contents($file));
+ for ($i = 0, $count = count($tokens); $i < $count; $i++) {
+ $token = $tokens[$i];
+
+ if (!is_array($token)) {
+ continue;
+ }
+
+ if (true === $class && T_STRING === $token[0]) {
+ return $namespace.'\\'.$token[1];
+ }
+
+ if (true === $namespace && T_STRING === $token[0]) {
+ $namespace = '';
+ do {
+ $namespace .= $token[1];
+ $token = $tokens[++$i];
+ } while ($i < $count && is_array($token) && in_array($token[0], array(T_NS_SEPARATOR, T_STRING)));
+ }
+
+ if (T_CLASS === $token[0]) {
+ $class = true;
+ }
+
+ if (T_NAMESPACE === $token[0]) {
+ $namespace = true;
+ }
+ }
+
+ return false;
+ }
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Config\Loader\Loader;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * ClosureLoader loads routes from a PHP closure.
+ *
+ * The Closure must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class ClosureLoader extends Loader
+{
+ /**
+ * Loads a Closure.
+ *
+ * @param \Closure $closure A Closure
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @api
+ */
+ public function load($closure, $type = null)
+ {
+ return call_user_func($closure);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return $resource instanceof \Closure && (!$type || 'closure' === $type);
+ }
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * PhpFileLoader loads routes from a PHP file.
+ *
+ * The file must return a RouteCollection instance.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class PhpFileLoader extends FileLoader
+{
+ /**
+ * Loads a PHP file.
+ *
+ * @param string $file A PHP file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ // the loader variable is exposed to the included file below
+ $loader = $this;
+
+ $path = $this->locator->locate($file);
+ $this->setCurrentDir(dirname($path));
+
+ $collection = include $path;
+ $collection->addResource(new FileResource($path));
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type);
+ }
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Config\Loader\FileLoader;
+use Symfony\Component\Config\Util\XmlUtils;
+
+/**
+ * XmlFileLoader loads XML routing files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class XmlFileLoader extends FileLoader
+{
+ const NAMESPACE_URI = 'http://symfony.com/schema/routing';
+ const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
+
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When the file cannot be loaded or when the XML cannot be
+ * parsed because it does not validate against the scheme.
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ $xml = $this->loadFile($path);
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // process routes and imports
+ foreach ($xml->documentElement->childNodes as $node) {
+ if (!$node instanceof \DOMElement) {
+ continue;
+ }
+
+ $this->parseNode($collection, $node, $path, $file);
+ }
+
+ return $collection;
+ }
+
+ /**
+ * Parses a node from a loaded XML file.
+ *
+ * @param RouteCollection $collection Collection to associate with the node
+ * @param \DOMElement $node Element to parse
+ * @param string $path Full path of the XML file being processed
+ * @param string $file Loaded file name
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
+ {
+ if (self::NAMESPACE_URI !== $node->namespaceURI) {
+ return;
+ }
+
+ switch ($node->localName) {
+ case 'route':
+ $this->parseRoute($collection, $node, $path);
+ break;
+ case 'import':
+ $this->parseImport($collection, $node, $path, $file);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ *
+ * @param RouteCollection $collection RouteCollection instance
+ * @param \DOMElement $node Element to parse that represents a Route
+ * @param string $path Full path of the XML file being processed
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path)
+ {
+ if ('' === ($id = $node->getAttribute('id')) || (!$node->hasAttribute('pattern') && !$node->hasAttribute('path'))) {
+ throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" and a "path" attribute.', $path));
+ }
+
+ if ($node->hasAttribute('pattern')) {
+ if ($node->hasAttribute('path')) {
+ throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path));
+ }
+
+ $node->setAttribute('path', $node->getAttribute('pattern'));
+ $node->removeAttribute('pattern');
+ }
+
+ $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY);
+ $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY);
+
+ list($defaults, $requirements, $options) = $this->parseConfigs($node, $path);
+
+ $route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods);
+ $collection->add($id, $route);
+ }
+
+ /**
+ * Parses an import and adds the routes in the resource to the RouteCollection.
+ *
+ * @param RouteCollection $collection RouteCollection instance
+ * @param \DOMElement $node Element to parse that represents a Route
+ * @param string $path Full path of the XML file being processed
+ * @param string $file Loaded file name
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file)
+ {
+ if ('' === $resource = $node->getAttribute('resource')) {
+ throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute.', $path));
+ }
+
+ $type = $node->getAttribute('type');
+ $prefix = $node->getAttribute('prefix');
+ $host = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
+ $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null;
+ $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null;
+
+ list($defaults, $requirements, $options) = $this->parseConfigs($node, $path);
+
+ $this->setCurrentDir(dirname($path));
+
+ $subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file);
+ /* @var $subCollection RouteCollection */
+ $subCollection->addPrefix($prefix);
+ if (null !== $host) {
+ $subCollection->setHost($host);
+ }
+ if (null !== $schemes) {
+ $subCollection->setSchemes($schemes);
+ }
+ if (null !== $methods) {
+ $subCollection->setMethods($methods);
+ }
+ $subCollection->addDefaults($defaults);
+ $subCollection->addRequirements($requirements);
+ $subCollection->addOptions($options);
+
+ $collection->addCollection($subCollection);
+ }
+
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ *
+ * @return \DOMDocument
+ *
+ * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
+ * or when the XML structure is not as expected by the scheme -
+ * see validate()
+ */
+ protected function loadFile($file)
+ {
+ return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
+ }
+
+ /**
+ * Parses the config elements (default, requirement, option).
+ *
+ * @param \DOMElement $node Element to parse that contains the configs
+ * @param string $path Full path of the XML file being processed
+ *
+ * @return array An array with the defaults as first item, requirements as second and options as third.
+ *
+ * @throws \InvalidArgumentException When the XML is invalid
+ */
+ private function parseConfigs(\DOMElement $node, $path)
+ {
+ $defaults = array();
+ $requirements = array();
+ $options = array();
+
+ foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
+ switch ($n->localName) {
+ case 'default':
+ if ($n->hasAttribute('xsi:nil') && 'true' == $n->getAttribute('xsi:nil')) {
+ $defaults[$n->getAttribute('key')] = null;
+ } else {
+ $defaults[$n->getAttribute('key')] = trim($n->textContent);
+ }
+
+ break;
+ case 'requirement':
+ $requirements[$n->getAttribute('key')] = trim($n->textContent);
+ break;
+ case 'option':
+ $options[$n->getAttribute('key')] = trim($n->textContent);
+ break;
+ default:
+ throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path));
+ }
+ }
+
+ return array($defaults, $requirements, $options);
+ }
+}
--- /dev/null
+<?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\Routing\Loader;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Yaml\Parser as YamlParser;
+use Symfony\Component\Config\Loader\FileLoader;
+
+/**
+ * YamlFileLoader loads Yaml routing files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class YamlFileLoader extends FileLoader
+{
+ private static $availableKeys = array(
+ 'resource', 'type', 'prefix', 'pattern', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options',
+ );
+ private $yamlParser;
+
+ /**
+ * Loads a Yaml file.
+ *
+ * @param string $file A Yaml file path
+ * @param string|null $type The resource type
+ *
+ * @return RouteCollection A RouteCollection instance
+ *
+ * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid
+ *
+ * @api
+ */
+ public function load($file, $type = null)
+ {
+ $path = $this->locator->locate($file);
+
+ if (!stream_is_local($path)) {
+ throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path));
+ }
+
+ if (!file_exists($path)) {
+ throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path));
+ }
+
+ if (null === $this->yamlParser) {
+ $this->yamlParser = new YamlParser();
+ }
+
+ $config = $this->yamlParser->parse(file_get_contents($path));
+
+ $collection = new RouteCollection();
+ $collection->addResource(new FileResource($path));
+
+ // empty file
+ if (null === $config) {
+ return $collection;
+ }
+
+ // not an array
+ if (!is_array($config)) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path));
+ }
+
+ foreach ($config as $name => $config) {
+ if (isset($config['pattern'])) {
+ if (isset($config['path'])) {
+ throw new \InvalidArgumentException(sprintf('The file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path));
+ }
+
+ $config['path'] = $config['pattern'];
+ unset($config['pattern']);
+ }
+
+ $this->validate($config, $name, $path);
+
+ if (isset($config['resource'])) {
+ $this->parseImport($collection, $config, $path, $file);
+ } else {
+ $this->parseRoute($collection, $name, $config, $path);
+ }
+ }
+
+ return $collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function supports($resource, $type = null)
+ {
+ return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'yaml' === $type);
+ }
+
+ /**
+ * Parses a route and adds it to the RouteCollection.
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ * @param string $name Route name
+ * @param array $config Route definition
+ * @param string $path Full path of the YAML file being processed
+ */
+ protected function parseRoute(RouteCollection $collection, $name, array $config, $path)
+ {
+ $defaults = isset($config['defaults']) ? $config['defaults'] : array();
+ $requirements = isset($config['requirements']) ? $config['requirements'] : array();
+ $options = isset($config['options']) ? $config['options'] : array();
+ $host = isset($config['host']) ? $config['host'] : '';
+ $schemes = isset($config['schemes']) ? $config['schemes'] : array();
+ $methods = isset($config['methods']) ? $config['methods'] : array();
+
+ $route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods);
+
+ $collection->add($name, $route);
+ }
+
+ /**
+ * Parses an import and adds the routes in the resource to the RouteCollection.
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ * @param array $config Route definition
+ * @param string $path Full path of the YAML file being processed
+ * @param string $file Loaded file name
+ */
+ protected function parseImport(RouteCollection $collection, array $config, $path, $file)
+ {
+ $type = isset($config['type']) ? $config['type'] : null;
+ $prefix = isset($config['prefix']) ? $config['prefix'] : '';
+ $defaults = isset($config['defaults']) ? $config['defaults'] : array();
+ $requirements = isset($config['requirements']) ? $config['requirements'] : array();
+ $options = isset($config['options']) ? $config['options'] : array();
+ $host = isset($config['host']) ? $config['host'] : null;
+ $schemes = isset($config['schemes']) ? $config['schemes'] : null;
+ $methods = isset($config['methods']) ? $config['methods'] : null;
+
+ $this->setCurrentDir(dirname($path));
+
+ $subCollection = $this->import($config['resource'], $type, false, $file);
+ /* @var $subCollection RouteCollection */
+ $subCollection->addPrefix($prefix);
+ if (null !== $host) {
+ $subCollection->setHost($host);
+ }
+ if (null !== $schemes) {
+ $subCollection->setSchemes($schemes);
+ }
+ if (null !== $methods) {
+ $subCollection->setMethods($methods);
+ }
+ $subCollection->addDefaults($defaults);
+ $subCollection->addRequirements($requirements);
+ $subCollection->addOptions($options);
+
+ $collection->addCollection($subCollection);
+ }
+
+ /**
+ * Validates the route configuration.
+ *
+ * @param array $config A resource config
+ * @param string $name The config key
+ * @param string $path The loaded file path
+ *
+ * @throws \InvalidArgumentException If one of the provided config keys is not supported,
+ * something is missing or the combination is nonsense
+ */
+ protected function validate($config, $name, $path)
+ {
+ if (!is_array($config)) {
+ throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path));
+ }
+ if ($extraKeys = array_diff(array_keys($config), self::$availableKeys)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".',
+ $path, $name, implode('", "', $extraKeys), implode('", "', self::$availableKeys)
+ ));
+ }
+ if (isset($config['resource']) && isset($config['path'])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.',
+ $path, $name
+ ));
+ }
+ if (!isset($config['resource']) && isset($config['type'])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.',
+ $name, $path
+ ));
+ }
+ if (!isset($config['resource']) && !isset($config['path'])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'You must define a "path" for the route "%s" in file "%s".',
+ $name, $path
+ ));
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<xsd:schema xmlns="http://symfony.com/schema/routing"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ targetNamespace="http://symfony.com/schema/routing"
+ elementFormDefault="qualified">
+
+ <xsd:annotation>
+ <xsd:documentation><![CDATA[
+ Symfony XML Routing Schema, version 1.0
+ Authors: Fabien Potencier, Tobias Schultze
+
+ This scheme defines the elements and attributes that can be used to define
+ routes. A route maps an HTTP request to a set of configuration variables.
+ ]]></xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:element name="routes" type="routes" />
+
+ <xsd:complexType name="routes">
+ <xsd:choice minOccurs="0" maxOccurs="unbounded">
+ <xsd:element name="import" type="import" />
+ <xsd:element name="route" type="route" />
+ </xsd:choice>
+ </xsd:complexType>
+
+ <xsd:group name="configs">
+ <xsd:choice>
+ <xsd:element name="default" nillable="true" type="element" />
+ <xsd:element name="requirement" type="element" />
+ <xsd:element name="option" type="element" />
+ </xsd:choice>
+ </xsd:group>
+
+ <xsd:complexType name="route">
+ <xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
+
+ <xsd:attribute name="id" type="xsd:string" use="required" />
+ <xsd:attribute name="path" type="xsd:string" />
+ <xsd:attribute name="pattern" type="xsd:string" />
+ <xsd:attribute name="host" type="xsd:string" />
+ <xsd:attribute name="schemes" type="xsd:string" />
+ <xsd:attribute name="methods" type="xsd:string" />
+ </xsd:complexType>
+
+ <xsd:complexType name="import">
+ <xsd:group ref="configs" minOccurs="0" maxOccurs="unbounded" />
+
+ <xsd:attribute name="resource" type="xsd:string" use="required" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="prefix" type="xsd:string" />
+ <xsd:attribute name="host" type="xsd:string" />
+ <xsd:attribute name="schemes" type="xsd:string" />
+ <xsd:attribute name="methods" type="xsd:string" />
+ </xsd:complexType>
+
+ <xsd:complexType name="element">
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute name="key" type="xsd:string" use="required" />
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+</xsd:schema>
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+/**
+ * ApacheUrlMatcher matches URL based on Apache mod_rewrite matching (see ApacheMatcherDumper).
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+class ApacheUrlMatcher extends UrlMatcher
+{
+ /**
+ * Tries to match a URL based on Apache mod_rewrite matching.
+ *
+ * Returns false if no route matches the URL.
+ *
+ * @param string $pathinfo The pathinfo to be parsed
+ *
+ * @return array An array of parameters
+ *
+ * @throws MethodNotAllowedException If the current method is not allowed
+ */
+ public function match($pathinfo)
+ {
+ $parameters = array();
+ $defaults = array();
+ $allow = array();
+ $route = null;
+
+ foreach ($_SERVER as $key => $value) {
+ $name = $key;
+
+ // skip non-routing variables
+ // this improves performance when $_SERVER contains many usual
+ // variables like HTTP_*, DOCUMENT_ROOT, REQUEST_URI, ...
+ if (false === strpos($name, '_ROUTING_')) {
+ continue;
+ }
+
+ while (0 === strpos($name, 'REDIRECT_')) {
+ $name = substr($name, 9);
+ }
+
+ // expect _ROUTING_<type>_<name>
+ // or _ROUTING_<type>
+
+ if (0 !== strpos($name, '_ROUTING_')) {
+ continue;
+ }
+ if (false !== $pos = strpos($name, '_', 9)) {
+ $type = substr($name, 9, $pos-9);
+ $name = substr($name, $pos+1);
+ } else {
+ $type = substr($name, 9);
+ }
+
+ if ('param' === $type) {
+ if ('' !== $value) {
+ $parameters[$name] = $value;
+ }
+ } elseif ('default' === $type) {
+ $defaults[$name] = $value;
+ } elseif ('route' === $type) {
+ $route = $value;
+ } elseif ('allow' === $type) {
+ $allow[] = $name;
+ }
+
+ unset($_SERVER[$key]);
+ }
+
+ if (null !== $route) {
+ $parameters['_route'] = $route;
+
+ return $this->mergeDefaults($parameters, $defaults);
+ } elseif (0 < count($allow)) {
+ throw new MethodNotAllowedException($allow);
+ } else {
+ return parent::match($pathinfo);
+ }
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Dumps a set of Apache mod_rewrite rules.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Kris Wallsmith <kris@symfony.com>
+ */
+class ApacheMatcherDumper extends MatcherDumper
+{
+ /**
+ * Dumps a set of Apache mod_rewrite rules.
+ *
+ * Available options:
+ *
+ * * script_name: The script name (app.php by default)
+ * * base_uri: The base URI ("" by default)
+ *
+ * @param array $options An array of options
+ *
+ * @return string A string to be used as Apache rewrite rules
+ *
+ * @throws \LogicException When the route regex is invalid
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_merge(array(
+ 'script_name' => 'app.php',
+ 'base_uri' => '',
+ ), $options);
+
+ $options['script_name'] = self::escape($options['script_name'], ' ', '\\');
+
+ $rules = array("# skip \"real\" requests\nRewriteCond %{REQUEST_FILENAME} -f\nRewriteRule .* - [QSA,L]");
+ $methodVars = array();
+ $hostRegexUnique = 0;
+ $prevHostRegex = '';
+
+ foreach ($this->getRoutes()->all() as $name => $route) {
+
+ $compiledRoute = $route->compile();
+ $hostRegex = $compiledRoute->getHostRegex();
+
+ if (null !== $hostRegex && $prevHostRegex !== $hostRegex) {
+ $prevHostRegex = $hostRegex;
+ $hostRegexUnique++;
+
+ $rule = array();
+
+ $regex = $this->regexToApacheRegex($hostRegex);
+ $regex = self::escape($regex, ' ', '\\');
+
+ $rule[] = sprintf('RewriteCond %%{HTTP:Host} %s', $regex);
+
+ $variables = array();
+ $variables[] = sprintf('E=__ROUTING_host_%s:1', $hostRegexUnique);
+
+ foreach ($compiledRoute->getHostVariables() as $i => $variable) {
+ $variables[] = sprintf('E=__ROUTING_host_%s_%s:%%%d', $hostRegexUnique, $variable, $i+1);
+ }
+
+ $variables = implode(',', $variables);
+
+ $rule[] = sprintf('RewriteRule .? - [%s]', $variables);
+
+ $rules[] = implode("\n", $rule);
+ }
+
+ $rules[] = $this->dumpRoute($name, $route, $options, $hostRegexUnique);
+
+ if ($req = $route->getRequirement('_method')) {
+ $methods = explode('|', strtoupper($req));
+ $methodVars = array_merge($methodVars, $methods);
+ }
+ }
+ if (0 < count($methodVars)) {
+ $rule = array('# 405 Method Not Allowed');
+ $methodVars = array_values(array_unique($methodVars));
+ if (in_array('GET', $methodVars) && !in_array('HEAD', $methodVars)) {
+ $methodVars[] = 'HEAD';
+ }
+ foreach ($methodVars as $i => $methodVar) {
+ $rule[] = sprintf('RewriteCond %%{ENV:_ROUTING__allow_%s} =1%s', $methodVar, isset($methodVars[$i + 1]) ? ' [OR]' : '');
+ }
+ $rule[] = sprintf('RewriteRule .* %s [QSA,L]', $options['script_name']);
+
+ $rules[] = implode("\n", $rule);
+ }
+
+ return implode("\n\n", $rules)."\n";
+ }
+
+ /**
+ * Dumps a single route
+ *
+ * @param string $name Route name
+ * @param Route $route The route
+ * @param array $options Options
+ * @param bool $hostRegexUnique Unique identifier for the host regex
+ *
+ * @return string The compiled route
+ */
+ private function dumpRoute($name, $route, array $options, $hostRegexUnique)
+ {
+ $compiledRoute = $route->compile();
+
+ // prepare the apache regex
+ $regex = $this->regexToApacheRegex($compiledRoute->getRegex());
+ $regex = '^'.self::escape(preg_quote($options['base_uri']).substr($regex, 1), ' ', '\\');
+
+ $methods = $this->getRouteMethods($route);
+
+ $hasTrailingSlash = (!$methods || in_array('HEAD', $methods)) && '/$' === substr($regex, -2) && '^/$' !== $regex;
+
+ $variables = array('E=_ROUTING_route:'.$name);
+ foreach ($compiledRoute->getHostVariables() as $variable) {
+ $variables[] = sprintf('E=_ROUTING_param_%s:%%{ENV:__ROUTING_host_%s_%s}', $variable, $hostRegexUnique, $variable);
+ }
+ foreach ($compiledRoute->getPathVariables() as $i => $variable) {
+ $variables[] = 'E=_ROUTING_param_'.$variable.':%'.($i + 1);
+ }
+ foreach ($route->getDefaults() as $key => $value) {
+ $variables[] = 'E=_ROUTING_default_'.$key.':'.strtr($value, array(
+ ':' => '\\:',
+ '=' => '\\=',
+ '\\' => '\\\\',
+ ' ' => '\\ ',
+ ));
+ }
+ $variables = implode(',', $variables);
+
+ $rule = array("# $name");
+
+ // method mismatch
+ if (0 < count($methods)) {
+ $allow = array();
+ foreach ($methods as $method) {
+ $allow[] = 'E=_ROUTING_allow_'.$method.':1';
+ }
+
+ if ($hostRegex = $compiledRoute->getHostRegex()) {
+ $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_host_%s} =1", $hostRegexUnique);
+ }
+
+ $rule[] = "RewriteCond %{REQUEST_URI} $regex";
+ $rule[] = sprintf("RewriteCond %%{REQUEST_METHOD} !^(%s)$ [NC]", implode('|', $methods));
+ $rule[] = sprintf('RewriteRule .* - [S=%d,%s]', $hasTrailingSlash ? 2 : 1, implode(',', $allow));
+ }
+
+ // redirect with trailing slash appended
+ if ($hasTrailingSlash) {
+
+ if ($hostRegex = $compiledRoute->getHostRegex()) {
+ $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_host_%s} =1", $hostRegexUnique);
+ }
+
+ $rule[] = 'RewriteCond %{REQUEST_URI} '.substr($regex, 0, -2).'$';
+ $rule[] = 'RewriteRule .* $0/ [QSA,L,R=301]';
+ }
+
+ // the main rule
+
+ if ($hostRegex = $compiledRoute->getHostRegex()) {
+ $rule[] = sprintf("RewriteCond %%{ENV:__ROUTING_host_%s} =1", $hostRegexUnique);
+ }
+
+ $rule[] = "RewriteCond %{REQUEST_URI} $regex";
+ $rule[] = "RewriteRule .* {$options['script_name']} [QSA,L,$variables]";
+
+ return implode("\n", $rule);
+ }
+
+ /**
+ * Returns methods allowed for a route
+ *
+ * @param Route $route The route
+ *
+ * @return array The methods
+ */
+ private function getRouteMethods(Route $route)
+ {
+ $methods = array();
+ if ($req = $route->getRequirement('_method')) {
+ $methods = explode('|', strtoupper($req));
+ // GET and HEAD are equivalent
+ if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
+ $methods[] = 'HEAD';
+ }
+ }
+
+ return $methods;
+ }
+
+ /**
+ * Converts a regex to make it suitable for mod_rewrite
+ *
+ * @param string $regex The regex
+ *
+ * @return string The converted regex
+ */
+ private function regexToApacheRegex($regex)
+ {
+ $regexPatternEnd = strrpos($regex, $regex[0]);
+
+ return preg_replace('/\?P<.+?>/', '', substr($regex, 1, $regexPatternEnd - 1));
+ }
+
+ /**
+ * Escapes a string.
+ *
+ * @param string $string The string to be escaped
+ * @param string $char The character to be escaped
+ * @param string $with The character to be used for escaping
+ *
+ * @return string The escaped string
+ */
+ private static function escape($string, $char, $with)
+ {
+ $escaped = false;
+ $output = '';
+ foreach (str_split($string) as $symbol) {
+ if ($escaped) {
+ $output .= $symbol;
+ $escaped = false;
+ continue;
+ }
+ if ($symbol === $char) {
+ $output .= $with.$char;
+ continue;
+ }
+ if ($symbol === $with) {
+ $escaped = true;
+ }
+ $output .= $symbol;
+ }
+
+ return $output;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+/**
+ * Collection of routes.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+class DumperCollection implements \IteratorAggregate
+{
+ /**
+ * @var DumperCollection|null
+ */
+ private $parent;
+
+ /**
+ * @var (DumperCollection|DumperRoute)[]
+ */
+ private $children = array();
+
+ /**
+ * @var array
+ */
+ private $attributes = array();
+
+ /**
+ * Returns the children routes and collections.
+ *
+ * @return (DumperCollection|DumperRoute)[] Array of DumperCollection|DumperRoute
+ */
+ public function all()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Adds a route or collection
+ *
+ * @param DumperRoute|DumperCollection The route or collection
+ */
+ public function add($child)
+ {
+ if ($child instanceof DumperCollection) {
+ $child->setParent($this);
+ }
+ $this->children[] = $child;
+ }
+
+ /**
+ * Sets children.
+ *
+ * @param array $children The children
+ */
+ public function setAll(array $children)
+ {
+ foreach ($children as $child) {
+ if ($child instanceof DumperCollection) {
+ $child->setParent($this);
+ }
+ }
+ $this->children = $children;
+ }
+
+ /**
+ * Returns an iterator over the children.
+ *
+ * @return \Iterator The iterator
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->children);
+ }
+
+ /**
+ * Returns the root of the collection.
+ *
+ * @return DumperCollection The root collection
+ */
+ public function getRoot()
+ {
+ return (null !== $this->parent) ? $this->parent->getRoot() : $this;
+ }
+
+ /**
+ * Returns the parent collection.
+ *
+ * @return DumperCollection|null The parent collection or null if the collection has no parent
+ */
+ protected function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Sets the parent collection.
+ *
+ * @param DumperCollection $parent The parent collection
+ */
+ protected function setParent(DumperCollection $parent)
+ {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Returns true if the attribute is defined.
+ *
+ * @param string $name The attribute name
+ *
+ * @return Boolean true if the attribute is defined, false otherwise
+ */
+ public function hasAttribute($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ /**
+ * Returns an attribute by name.
+ *
+ * @param string $name The attribute name
+ * @param mixed $default Default value is the attribute doesn't exist
+ *
+ * @return mixed The attribute value
+ */
+ public function getAttribute($name, $default = null)
+ {
+ return $this->hasAttribute($name) ? $this->attributes[$name] : $default;
+ }
+
+ /**
+ * Sets an attribute by name.
+ *
+ * @param string $name The attribute name
+ * @param mixed $value The attribute value
+ */
+ public function setAttribute($name, $value)
+ {
+ $this->attributes[$name] = $value;
+ }
+
+ /**
+ * Sets multiple attributes.
+ *
+ * @param array $attributes The attributes
+ */
+ public function setAttributes($attributes)
+ {
+ $this->attributes = $attributes;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+/**
+ * Prefix tree of routes preserving routes order.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+class DumperPrefixCollection extends DumperCollection
+{
+ /**
+ * @var string
+ */
+ private $prefix = '';
+
+ /**
+ * Returns the prefix.
+ *
+ * @return string The prefix
+ */
+ public function getPrefix()
+ {
+ return $this->prefix;
+ }
+
+ /**
+ * Sets the prefix.
+ *
+ * @param string $prefix The prefix
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = $prefix;
+ }
+
+ /**
+ * Adds a route in the tree.
+ *
+ * @param DumperRoute $route The route
+ *
+ * @return DumperPrefixCollection The node the route was added to
+ *
+ * @throws \LogicException
+ */
+ public function addPrefixRoute(DumperRoute $route)
+ {
+ $prefix = $route->getRoute()->compile()->getStaticPrefix();
+
+ // Same prefix, add to current leave
+ if ($this->prefix === $prefix) {
+ $this->add($route);
+
+ return $this;
+ }
+
+ // Prefix starts with route's prefix
+ if ('' === $this->prefix || 0 === strpos($prefix, $this->prefix)) {
+ $collection = new DumperPrefixCollection();
+ $collection->setPrefix(substr($prefix, 0, strlen($this->prefix)+1));
+ $this->add($collection);
+
+ return $collection->addPrefixRoute($route);
+ }
+
+ // No match, fallback to parent (recursively)
+
+ if (null === $parent = $this->getParent()) {
+ throw new \LogicException("The collection root must not have a prefix");
+ }
+
+ return $parent->addPrefixRoute($route);
+ }
+
+ /**
+ * Merges nodes whose prefix ends with a slash
+ *
+ * Children of a node whose prefix ends with a slash are moved to the parent node
+ */
+ public function mergeSlashNodes()
+ {
+ $children = array();
+
+ foreach ($this as $child) {
+ if ($child instanceof self) {
+ $child->mergeSlashNodes();
+ if ('/' === substr($child->prefix, -1)) {
+ $children = array_merge($children, $child->all());
+ } else {
+ $children[] = $child;
+ }
+ } else {
+ $children[] = $child;
+ }
+ }
+
+ $this->setAll($children);
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+
+/**
+ * Container for a Route.
+ *
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+class DumperRoute
+{
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var Route
+ */
+ private $route;
+
+ /**
+ * Constructor.
+ *
+ * @param string $name The route name
+ * @param Route $route The route
+ */
+ public function __construct($name, Route $route)
+ {
+ $this->name = $name;
+ $this->route = $route;
+ }
+
+ /**
+ * Returns the route name.
+ *
+ * @return string The route name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Returns the route.
+ *
+ * @return Route The route
+ */
+ public function getRoute()
+ {
+ return $this->route;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * MatcherDumper is the abstract class for all built-in matcher dumpers.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class MatcherDumper implements MatcherDumperInterface
+{
+ /**
+ * @var RouteCollection
+ */
+ private $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes The RouteCollection to dump
+ */
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRoutes()
+ {
+ return $this->routes;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+/**
+ * MatcherDumperInterface is the interface that all matcher dumper classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface MatcherDumperInterface
+{
+ /**
+ * Dumps a set of routes to a string representation of executable code
+ * that can then be used to match a request against these routes.
+ *
+ * @param array $options An array of options
+ *
+ * @return string Executable code
+ */
+ public function dump(array $options = array());
+
+ /**
+ * Gets the routes to dump.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRoutes();
+}
--- /dev/null
+<?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\Routing\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+class PhpMatcherDumper extends MatcherDumper
+{
+ /**
+ * Dumps a set of routes to a PHP class.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return string A PHP class representing the matcher class
+ */
+ public function dump(array $options = array())
+ {
+ $options = array_replace(array(
+ 'class' => 'ProjectUrlMatcher',
+ 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ ), $options);
+
+ // trailing slash support is only enabled if we know how to redirect the user
+ $interfaces = class_implements($options['base_class']);
+ $supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']);
+
+ return <<<EOF
+<?php
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * {$options['class']}
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class {$options['class']} extends {$options['base_class']}
+{
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext \$context)
+ {
+ \$this->context = \$context;
+ }
+
+{$this->generateMatchMethod($supportsRedirections)}
+}
+
+EOF;
+ }
+
+ /**
+ * Generates the code for the match method implementing UrlMatcherInterface.
+ *
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
+ *
+ * @return string Match method as PHP code
+ */
+ private function generateMatchMethod($supportsRedirections)
+ {
+ $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
+
+ return <<<EOF
+ public function match(\$pathinfo)
+ {
+ \$allow = array();
+ \$pathinfo = rawurldecode(\$pathinfo);
+
+$code
+
+ throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
+ }
+EOF;
+ }
+
+ /**
+ * Generates PHP code to match a RouteCollection with all its routes.
+ *
+ * @param RouteCollection $routes A RouteCollection instance
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
+ *
+ * @return string PHP code
+ */
+ private function compileRoutes(RouteCollection $routes, $supportsRedirections)
+ {
+ $fetchedHost = false;
+
+ $groups = $this->groupRoutesByHostRegex($routes);
+ $code = '';
+
+ foreach ($groups as $collection) {
+ if (null !== $regex = $collection->getAttribute('host_regex')) {
+ if (!$fetchedHost) {
+ $code .= " \$host = \$this->context->getHost();\n\n";
+ $fetchedHost = true;
+ }
+
+ $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
+ }
+
+ $tree = $this->buildPrefixTree($collection);
+ $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections);
+
+ if (null !== $regex) {
+ // apply extra indention at each line (except empty ones)
+ $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode);
+ $code .= $groupCode;
+ $code .= " }\n\n";
+ } else {
+ $code .= $groupCode;
+ }
+ }
+
+ return $code;
+ }
+
+ /**
+ * Generates PHP code recursively to match a tree of routes
+ *
+ * @param DumperPrefixCollection $collection A DumperPrefixCollection instance
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
+ * @param string $parentPrefix Prefix of the parent collection
+ *
+ * @return string PHP code
+ */
+ private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '')
+ {
+ $code = '';
+ $prefix = $collection->getPrefix();
+ $optimizable = 1 < strlen($prefix) && 1 < count($collection->all());
+ $optimizedPrefix = $parentPrefix;
+
+ if ($optimizable) {
+ $optimizedPrefix = $prefix;
+
+ $code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true));
+ }
+
+ foreach ($collection as $route) {
+ if ($route instanceof DumperCollection) {
+ $code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix);
+ } else {
+ $code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n";
+ }
+ }
+
+ if ($optimizable) {
+ $code .= " }\n\n";
+ // apply extra indention at each line (except empty ones)
+ $code = preg_replace('/^.{2,}$/m', ' $0', $code);
+ }
+
+ return $code;
+ }
+
+ /**
+ * Compiles a single Route to PHP code used to match it against the path info.
+ *
+ * @param Route $route A Route instance
+ * @param string $name The name of the Route
+ * @param Boolean $supportsRedirections Whether redirections are supported by the base class
+ * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
+ *
+ * @return string PHP code
+ *
+ * @throws \LogicException
+ */
+ private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
+ {
+ $code = '';
+ $compiledRoute = $route->compile();
+ $conditions = array();
+ $hasTrailingSlash = false;
+ $matches = false;
+ $hostMatches = false;
+ $methods = array();
+
+ if ($req = $route->getRequirement('_method')) {
+ $methods = explode('|', strtoupper($req));
+ // GET and HEAD are equivalent
+ if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
+ $methods[] = 'HEAD';
+ }
+ }
+
+ $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));
+
+ if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
+ if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
+ $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
+ $hasTrailingSlash = true;
+ } else {
+ $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true));
+ }
+ } else {
+ if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
+ $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
+ }
+
+ $regex = $compiledRoute->getRegex();
+ if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
+ $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
+ $hasTrailingSlash = true;
+ }
+ $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true));
+
+ $matches = true;
+ }
+
+ if ($compiledRoute->getHostVariables()) {
+ $hostMatches = true;
+ }
+
+ $conditions = implode(' && ', $conditions);
+
+ $code .= <<<EOF
+ // $name
+ if ($conditions) {
+
+EOF;
+
+ if ($methods) {
+ $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
+
+ if (1 === count($methods)) {
+ $code .= <<<EOF
+ if (\$this->context->getMethod() != '$methods[0]') {
+ \$allow[] = '$methods[0]';
+ goto $gotoname;
+ }
+
+
+EOF;
+ } else {
+ $methods = implode("', '", $methods);
+ $code .= <<<EOF
+ if (!in_array(\$this->context->getMethod(), array('$methods'))) {
+ \$allow = array_merge(\$allow, array('$methods'));
+ goto $gotoname;
+ }
+
+
+EOF;
+ }
+ }
+
+ if ($hasTrailingSlash) {
+ $code .= <<<EOF
+ if (substr(\$pathinfo, -1) !== '/') {
+ return \$this->redirect(\$pathinfo.'/', '$name');
+ }
+
+
+EOF;
+ }
+
+ if ($scheme = $route->getRequirement('_scheme')) {
+ if (!$supportsRedirections) {
+ throw new \LogicException('The "_scheme" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
+ }
+
+ $code .= <<<EOF
+ if (\$this->context->getScheme() !== '$scheme') {
+ return \$this->redirect(\$pathinfo, '$name', '$scheme');
+ }
+
+
+EOF;
+ }
+
+ // optimize parameters array
+ if ($matches || $hostMatches) {
+ $vars = array();
+ if ($hostMatches) {
+ $vars[] = '$hostMatches';
+ }
+ if ($matches) {
+ $vars[] = '$matches';
+ }
+ $vars[] = "array('_route' => '$name')";
+
+ $code .= sprintf(" return \$this->mergeDefaults(array_replace(%s), %s);\n"
+ , implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true)));
+
+ } elseif ($route->getDefaults()) {
+ $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
+ } else {
+ $code .= sprintf(" return array('_route' => '%s');\n", $name);
+ }
+ $code .= " }\n";
+
+ if ($methods) {
+ $code .= " $gotoname:\n";
+ }
+
+ return $code;
+ }
+
+ /**
+ * Groups consecutive routes having the same host regex.
+ *
+ * The result is a collection of collections of routes having the same host regex.
+ *
+ * @param RouteCollection $routes A flat RouteCollection
+ *
+ * @return DumperCollection A collection with routes grouped by host regex in sub-collections
+ */
+ private function groupRoutesByHostRegex(RouteCollection $routes)
+ {
+ $groups = new DumperCollection();
+
+ $currentGroup = new DumperCollection();
+ $currentGroup->setAttribute('host_regex', null);
+ $groups->add($currentGroup);
+
+ foreach ($routes as $name => $route) {
+ $hostRegex = $route->compile()->getHostRegex();
+ if ($currentGroup->getAttribute('host_regex') !== $hostRegex) {
+ $currentGroup = new DumperCollection();
+ $currentGroup->setAttribute('host_regex', $hostRegex);
+ $groups->add($currentGroup);
+ }
+ $currentGroup->add(new DumperRoute($name, $route));
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Organizes the routes into a prefix tree.
+ *
+ * Routes order is preserved such that traversing the tree will traverse the
+ * routes in the origin order.
+ *
+ * @param DumperCollection $collection A collection of routes
+ *
+ * @return DumperPrefixCollection
+ */
+ private function buildPrefixTree(DumperCollection $collection)
+ {
+ $tree = new DumperPrefixCollection();
+ $current = $tree;
+
+ foreach ($collection as $route) {
+ $current = $current->addPrefixRoute($route);
+ }
+
+ $tree->mergeSlashNodes();
+
+ return $tree;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function match($pathinfo)
+ {
+ try {
+ $parameters = parent::match($pathinfo);
+ } catch (ResourceNotFoundException $e) {
+ if ('/' === substr($pathinfo, -1) || !in_array($this->context->getMethod(), array('HEAD', 'GET'))) {
+ throw $e;
+ }
+
+ try {
+ parent::match($pathinfo.'/');
+
+ return $this->redirect($pathinfo.'/', null);
+ } catch (ResourceNotFoundException $e2) {
+ throw $e;
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function handleRouteRequirements($pathinfo, $name, Route $route)
+ {
+ // check HTTP scheme requirement
+ $scheme = $route->getRequirement('_scheme');
+ if ($scheme && $this->context->getScheme() !== $scheme) {
+ return array(self::ROUTE_MATCH, $this->redirect($pathinfo, $name, $scheme));
+ }
+
+ return array(self::REQUIREMENT_MATCH, null);
+ }
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+/**
+ * RedirectableUrlMatcherInterface knows how to redirect the user.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface RedirectableUrlMatcherInterface
+{
+ /**
+ * Redirects the user to another URL.
+ *
+ * @param string $path The path info to redirect to.
+ * @param string $route The route name that matched
+ * @param string|null $scheme The URL scheme (null to keep the current one)
+ *
+ * @return array An array of parameters
+ *
+ * @api
+ */
+ public function redirect($path, $route, $scheme = null);
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+/**
+ * RequestMatcherInterface is the interface that all request matcher classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RequestMatcherInterface
+{
+ /**
+ * Tries to match a request with a set of routes.
+ *
+ * If the matcher can not find information, it must throw one of the exceptions documented
+ * below.
+ *
+ * @param Request $request The request to match
+ *
+ * @return array An array of parameters
+ *
+ * @throws ResourceNotFoundException If no matching resource could be found
+ * @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed
+ */
+ public function matchRequest(Request $request);
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\ExceptionInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+
+/**
+ * TraceableUrlMatcher helps debug path info matching by tracing the match.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TraceableUrlMatcher extends UrlMatcher
+{
+ const ROUTE_DOES_NOT_MATCH = 0;
+ const ROUTE_ALMOST_MATCHES = 1;
+ const ROUTE_MATCHES = 2;
+
+ protected $traces;
+
+ public function getTraces($pathinfo)
+ {
+ $this->traces = array();
+
+ try {
+ $this->match($pathinfo);
+ } catch (ExceptionInterface $e) {
+ }
+
+ return $this->traces;
+ }
+
+ protected function matchCollection($pathinfo, RouteCollection $routes)
+ {
+ foreach ($routes as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) {
+ // does it match without any requirements?
+ $r = new Route($route->getPath(), $route->getDefaults(), array(), $route->getOptions());
+ $cr = $r->compile();
+ if (!preg_match($cr->getRegex(), $pathinfo)) {
+ $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route);
+
+ continue;
+ }
+
+ foreach ($route->getRequirements() as $n => $regex) {
+ $r = new Route($route->getPath(), $route->getDefaults(), array($n => $regex), $route->getOptions());
+ $cr = $r->compile();
+
+ if (in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) {
+ $this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route);
+
+ continue 2;
+ }
+ }
+
+ continue;
+ }
+
+ // check host requirement
+ $hostMatches = array();
+ if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
+ $this->addTrace(sprintf('Host "%s" does not match the requirement ("%s")', $this->context->getHost(), $route->getHost()), self::ROUTE_ALMOST_MATCHES, $name, $route);
+
+ return true;
+ }
+
+ // check HTTP method requirement
+ if ($req = $route->getRequirement('_method')) {
+ // HEAD and GET are equivalent as per RFC
+ if ('HEAD' === $method = $this->context->getMethod()) {
+ $method = 'GET';
+ }
+
+ if (!in_array($method, $req = explode('|', strtoupper($req)))) {
+ $this->allow = array_merge($this->allow, $req);
+
+ $this->addTrace(sprintf('Method "%s" does not match the requirement ("%s")', $this->context->getMethod(), implode(', ', $req)), self::ROUTE_ALMOST_MATCHES, $name, $route);
+
+ continue;
+ }
+ }
+
+ // check HTTP scheme requirement
+ if ($scheme = $route->getRequirement('_scheme')) {
+ if ($this->context->getScheme() !== $scheme) {
+ $this->addTrace(sprintf('Scheme "%s" does not match the requirement ("%s"); the user will be redirected', $this->context->getScheme(), $scheme), self::ROUTE_ALMOST_MATCHES, $name, $route);
+
+ return true;
+ }
+ }
+
+ $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route);
+
+ return true;
+ }
+ }
+
+ private function addTrace($log, $level = self::ROUTE_DOES_NOT_MATCH, $name = null, $route = null)
+ {
+ $this->traces[] = array(
+ 'log' => $log,
+ 'name' => $name,
+ 'level' => $level,
+ 'path' => null !== $route ? $route->getPath() : null,
+ );
+ }
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Route;
+
+/**
+ * UrlMatcher matches URL based on a set of routes.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class UrlMatcher implements UrlMatcherInterface
+{
+ const REQUIREMENT_MATCH = 0;
+ const REQUIREMENT_MISMATCH = 1;
+ const ROUTE_MATCH = 2;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ /**
+ * @var array
+ */
+ protected $allow = array();
+
+ /**
+ * @var RouteCollection
+ */
+ protected $routes;
+
+ /**
+ * Constructor.
+ *
+ * @param RouteCollection $routes A RouteCollection instance
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function __construct(RouteCollection $routes, RequestContext $context)
+ {
+ $this->routes = $routes;
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function match($pathinfo)
+ {
+ $this->allow = array();
+
+ if ($ret = $this->matchCollection(rawurldecode($pathinfo), $this->routes)) {
+ return $ret;
+ }
+
+ throw 0 < count($this->allow)
+ ? new MethodNotAllowedException(array_unique(array_map('strtoupper', $this->allow)))
+ : new ResourceNotFoundException();
+ }
+
+ /**
+ * Tries to match a URL with a set of routes.
+ *
+ * @param string $pathinfo The path info to be parsed
+ * @param RouteCollection $routes The set of routes
+ *
+ * @return array An array of parameters
+ *
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ */
+ protected function matchCollection($pathinfo, RouteCollection $routes)
+ {
+ foreach ($routes as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ // check the static prefix of the URL first. Only use the more expensive preg_match when it matches
+ if ('' !== $compiledRoute->getStaticPrefix() && 0 !== strpos($pathinfo, $compiledRoute->getStaticPrefix())) {
+ continue;
+ }
+
+ if (!preg_match($compiledRoute->getRegex(), $pathinfo, $matches)) {
+ continue;
+ }
+
+ $hostMatches = array();
+ if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) {
+ continue;
+ }
+
+ // check HTTP method requirement
+ if ($req = $route->getRequirement('_method')) {
+ // HEAD and GET are equivalent as per RFC
+ if ('HEAD' === $method = $this->context->getMethod()) {
+ $method = 'GET';
+ }
+
+ if (!in_array($method, $req = explode('|', strtoupper($req)))) {
+ $this->allow = array_merge($this->allow, $req);
+
+ continue;
+ }
+ }
+
+ $status = $this->handleRouteRequirements($pathinfo, $name, $route);
+
+ if (self::ROUTE_MATCH === $status[0]) {
+ return $status[1];
+ }
+
+ if (self::REQUIREMENT_MISMATCH === $status[0]) {
+ continue;
+ }
+
+ return $this->getAttributes($route, $name, array_replace($matches, $hostMatches));
+ }
+ }
+
+ /**
+ * Returns an array of values to use as request attributes.
+ *
+ * As this method requires the Route object, it is not available
+ * in matchers that do not have access to the matched Route instance
+ * (like the PHP and Apache matcher dumpers).
+ *
+ * @param Route $route The route we are matching against
+ * @param string $name The name of the route
+ * @param array $attributes An array of attributes from the matcher
+ *
+ * @return array An array of parameters
+ */
+ protected function getAttributes(Route $route, $name, array $attributes)
+ {
+ $attributes['_route'] = $name;
+
+ return $this->mergeDefaults($attributes, $route->getDefaults());
+ }
+
+ /**
+ * Handles specific route requirements.
+ *
+ * @param string $pathinfo The path
+ * @param string $name The route name
+ * @param Route $route The route
+ *
+ * @return array The first element represents the status, the second contains additional information
+ */
+ protected function handleRouteRequirements($pathinfo, $name, Route $route)
+ {
+ // check HTTP scheme requirement
+ $scheme = $route->getRequirement('_scheme');
+ $status = $scheme && $scheme !== $this->context->getScheme() ? self::REQUIREMENT_MISMATCH : self::REQUIREMENT_MATCH;
+
+ return array($status, null);
+ }
+
+ /**
+ * Get merged default parameters.
+ *
+ * @param array $params The parameters
+ * @param array $defaults The defaults
+ *
+ * @return array Merged default parameters
+ */
+ protected function mergeDefaults($params, $defaults)
+ {
+ foreach ($params as $key => $value) {
+ if (!is_int($key)) {
+ $defaults[$key] = $value;
+ }
+ }
+
+ return $defaults;
+ }
+}
--- /dev/null
+<?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\Routing\Matcher;
+
+use Symfony\Component\Routing\RequestContextAwareInterface;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+
+/**
+ * UrlMatcherInterface is the interface that all URL matcher classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface UrlMatcherInterface extends RequestContextAwareInterface
+{
+ /**
+ * Tries to match a URL path with a set of routes.
+ *
+ * If the matcher can not find information, it must throw one of the exceptions documented
+ * below.
+ *
+ * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded)
+ *
+ * @return array An array of parameters
+ *
+ * @throws ResourceNotFoundException If the resource could not be found
+ * @throws MethodNotAllowedException If the resource was found but the request method is not allowed
+ *
+ * @api
+ */
+ public function match($pathinfo);
+}
--- /dev/null
+Routing Component
+=================
+
+Routing associates a request with the code that will convert it to a response.
+
+The example below demonstrates how you can set up a fully working routing
+system:
+
+ use Symfony\Component\HttpFoundation\Request;
+ use Symfony\Component\Routing\Matcher\UrlMatcher;
+ use Symfony\Component\Routing\RequestContext;
+ use Symfony\Component\Routing\RouteCollection;
+ use Symfony\Component\Routing\Route;
+
+ $routes = new RouteCollection();
+ $routes->add('hello', new Route('/hello', array('controller' => 'foo')));
+
+ $context = new RequestContext();
+
+ // this is optional and can be done without a Request instance
+ $context->fromRequest(Request::createFromGlobals());
+
+ $matcher = new UrlMatcher($routes, $context);
+
+ $parameters = $matcher->match('/hello');
+
+Resources
+---------
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Routing/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\Routing;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Holds information about the current request.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class RequestContext
+{
+ private $baseUrl;
+ private $pathInfo;
+ private $method;
+ private $host;
+ private $scheme;
+ private $httpPort;
+ private $httpsPort;
+ private $queryString;
+
+ /**
+ * @var array
+ */
+ private $parameters = array();
+
+ /**
+ * Constructor.
+ *
+ * @param string $baseUrl The base URL
+ * @param string $method The HTTP method
+ * @param string $host The HTTP host name
+ * @param string $scheme The HTTP scheme
+ * @param integer $httpPort The HTTP port
+ * @param integer $httpsPort The HTTPS port
+ * @param string $path The path
+ * @param string $queryString The query string
+ *
+ * @api
+ */
+ public function __construct($baseUrl = '', $method = 'GET', $host = 'localhost', $scheme = 'http', $httpPort = 80, $httpsPort = 443, $path = '/', $queryString = '')
+ {
+ $this->baseUrl = $baseUrl;
+ $this->method = strtoupper($method);
+ $this->host = $host;
+ $this->scheme = strtolower($scheme);
+ $this->httpPort = $httpPort;
+ $this->httpsPort = $httpsPort;
+ $this->pathInfo = $path;
+ $this->queryString = $queryString;
+ }
+
+ public function fromRequest(Request $request)
+ {
+ $this->setBaseUrl($request->getBaseUrl());
+ $this->setPathInfo($request->getPathInfo());
+ $this->setMethod($request->getMethod());
+ $this->setHost($request->getHost());
+ $this->setScheme($request->getScheme());
+ $this->setHttpPort($request->isSecure() ? $this->httpPort : $request->getPort());
+ $this->setHttpsPort($request->isSecure() ? $request->getPort() : $this->httpsPort);
+ $this->setQueryString($request->server->get('QUERY_STRING'));
+ }
+
+ /**
+ * Gets the base URL.
+ *
+ * @return string The base URL
+ */
+ public function getBaseUrl()
+ {
+ return $this->baseUrl;
+ }
+
+ /**
+ * Sets the base URL.
+ *
+ * @param string $baseUrl The base URL
+ *
+ * @api
+ */
+ public function setBaseUrl($baseUrl)
+ {
+ $this->baseUrl = $baseUrl;
+ }
+
+ /**
+ * Gets the path info.
+ *
+ * @return string The path info
+ */
+ public function getPathInfo()
+ {
+ return $this->pathInfo;
+ }
+
+ /**
+ * Sets the path info.
+ *
+ * @param string $pathInfo The path info
+ */
+ public function setPathInfo($pathInfo)
+ {
+ $this->pathInfo = $pathInfo;
+ }
+
+ /**
+ * Gets the HTTP method.
+ *
+ * The method is always an uppercased string.
+ *
+ * @return string The HTTP method
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * Sets the HTTP method.
+ *
+ * @param string $method The HTTP method
+ *
+ * @api
+ */
+ public function setMethod($method)
+ {
+ $this->method = strtoupper($method);
+ }
+
+ /**
+ * Gets the HTTP host.
+ *
+ * @return string The HTTP host
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Sets the HTTP host.
+ *
+ * @param string $host The HTTP host
+ *
+ * @api
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+ }
+
+ /**
+ * Gets the HTTP scheme.
+ *
+ * @return string The HTTP scheme
+ */
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * Sets the HTTP scheme.
+ *
+ * @param string $scheme The HTTP scheme
+ *
+ * @api
+ */
+ public function setScheme($scheme)
+ {
+ $this->scheme = strtolower($scheme);
+ }
+
+ /**
+ * Gets the HTTP port.
+ *
+ * @return string The HTTP port
+ */
+ public function getHttpPort()
+ {
+ return $this->httpPort;
+ }
+
+ /**
+ * Sets the HTTP port.
+ *
+ * @param string $httpPort The HTTP port
+ *
+ * @api
+ */
+ public function setHttpPort($httpPort)
+ {
+ $this->httpPort = $httpPort;
+ }
+
+ /**
+ * Gets the HTTPS port.
+ *
+ * @return string The HTTPS port
+ */
+ public function getHttpsPort()
+ {
+ return $this->httpsPort;
+ }
+
+ /**
+ * Sets the HTTPS port.
+ *
+ * @param string $httpsPort The HTTPS port
+ *
+ * @api
+ */
+ public function setHttpsPort($httpsPort)
+ {
+ $this->httpsPort = $httpsPort;
+ }
+
+ /**
+ * Gets the query string.
+ *
+ * @return string The query string
+ */
+ public function getQueryString()
+ {
+ return $this->queryString;
+ }
+
+ /**
+ * Sets the query string.
+ *
+ * @param string $queryString The query string
+ *
+ * @api
+ */
+ public function setQueryString($queryString)
+ {
+ $this->queryString = $queryString;
+ }
+
+ /**
+ * Returns the parameters.
+ *
+ * @return array The parameters
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * Sets the parameters.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $parameters The parameters
+ *
+ * @return Route The current Route instance
+ */
+ public function setParameters(array $parameters)
+ {
+ $this->parameters = $parameters;
+
+ return $this;
+ }
+
+ /**
+ * Gets a parameter value.
+ *
+ * @param string $name A parameter name
+ *
+ * @return mixed The parameter value
+ */
+ public function getParameter($name)
+ {
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+ }
+
+ /**
+ * Checks if a parameter value is set for the given parameter.
+ *
+ * @param string $name A parameter name
+ *
+ * @return Boolean true if the parameter value is set, false otherwise
+ */
+ public function hasParameter($name)
+ {
+ return array_key_exists($name, $this->parameters);
+ }
+
+ /**
+ * Sets a parameter value.
+ *
+ * @param string $name A parameter name
+ * @param mixed $parameter The parameter value
+ *
+ * @api
+ */
+ public function setParameter($name, $parameter)
+ {
+ $this->parameters[$name] = $parameter;
+ }
+}
--- /dev/null
+<?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\Routing;
+
+/**
+ * @api
+ */
+interface RequestContextAwareInterface
+{
+ /**
+ * Sets the request context.
+ *
+ * @param RequestContext $context The context
+ *
+ * @api
+ */
+ public function setContext(RequestContext $context);
+
+ /**
+ * Gets the request context.
+ *
+ * @return RequestContext The context
+ *
+ * @api
+ */
+ public function getContext();
+}
--- /dev/null
+<?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\Routing;
+
+/**
+ * A Route describes a route and its parameters.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class Route implements \Serializable
+{
+ /**
+ * @var string
+ */
+ private $path = '/';
+
+ /**
+ * @var string
+ */
+ private $host = '';
+
+ /**
+ * @var array
+ */
+ private $schemes = array();
+
+ /**
+ * @var array
+ */
+ private $methods = array();
+
+ /**
+ * @var array
+ */
+ private $defaults = array();
+
+ /**
+ * @var array
+ */
+ private $requirements = array();
+
+ /**
+ * @var array
+ */
+ private $options = array();
+
+ /**
+ * @var null|RouteCompiler
+ */
+ private $compiled;
+
+ /**
+ * Constructor.
+ *
+ * Available options:
+ *
+ * * compiler_class: A class name able to compile this route instance (RouteCompiler by default)
+ *
+ * @param string $path The path pattern to match
+ * @param array $defaults An array of default parameter values
+ * @param array $requirements An array of requirements for parameters (regexes)
+ * @param array $options An array of options
+ * @param string $host The host pattern to match
+ * @param string|array $schemes A required URI scheme or an array of restricted schemes
+ * @param string|array $methods A required HTTP method or an array of restricted methods
+ *
+ * @api
+ */
+ public function __construct($path, array $defaults = array(), array $requirements = array(), array $options = array(), $host = '', $schemes = array(), $methods = array())
+ {
+ $this->setPath($path);
+ $this->setDefaults($defaults);
+ $this->setRequirements($requirements);
+ $this->setOptions($options);
+ $this->setHost($host);
+ // The conditions make sure that an initial empty $schemes/$methods does not override the corresponding requirement.
+ // They can be removed when the BC layer is removed.
+ if ($schemes) {
+ $this->setSchemes($schemes);
+ }
+ if ($methods) {
+ $this->setMethods($methods);
+ }
+ }
+
+ public function serialize()
+ {
+ return serialize(array(
+ 'path' => $this->path,
+ 'host' => $this->host,
+ 'defaults' => $this->defaults,
+ 'requirements' => $this->requirements,
+ 'options' => $this->options,
+ 'schemes' => $this->schemes,
+ 'methods' => $this->methods,
+ ));
+ }
+
+ public function unserialize($data)
+ {
+ $data = unserialize($data);
+ $this->path = $data['path'];
+ $this->host = $data['host'];
+ $this->defaults = $data['defaults'];
+ $this->requirements = $data['requirements'];
+ $this->options = $data['options'];
+ $this->schemes = $data['schemes'];
+ $this->methods = $data['methods'];
+ }
+
+ /**
+ * Returns the pattern for the path.
+ *
+ * @return string The pattern
+ *
+ * @deprecated Deprecated in 2.2, to be removed in 3.0. Use getPath instead.
+ */
+ public function getPattern()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Sets the pattern for the path.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $pattern The path pattern
+ *
+ * @return Route The current Route instance
+ *
+ * @deprecated Deprecated in 2.2, to be removed in 3.0. Use setPath instead.
+ */
+ public function setPattern($pattern)
+ {
+ return $this->setPath($pattern);
+ }
+
+ /**
+ * Returns the pattern for the path.
+ *
+ * @return string The path pattern
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Sets the pattern for the path.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $pattern The path pattern
+ *
+ * @return Route The current Route instance
+ */
+ public function setPath($pattern)
+ {
+ // A pattern must start with a slash and must not have multiple slashes at the beginning because the
+ // generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
+ $this->path = '/'.ltrim(trim($pattern), '/');
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the pattern for the host.
+ *
+ * @return string The host pattern
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Sets the pattern for the host.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $pattern The host pattern
+ *
+ * @return Route The current Route instance
+ */
+ public function setHost($pattern)
+ {
+ $this->host = (string) $pattern;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the lowercased schemes this route is restricted to.
+ * So an empty array means that any scheme is allowed.
+ *
+ * @return array The schemes
+ */
+ public function getSchemes()
+ {
+ return $this->schemes;
+ }
+
+ /**
+ * Sets the schemes (e.g. 'https') this route is restricted to.
+ * So an empty array means that any scheme is allowed.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string|array $schemes The scheme or an array of schemes
+ *
+ * @return Route The current Route instance
+ */
+ public function setSchemes($schemes)
+ {
+ $this->schemes = array_map('strtolower', (array) $schemes);
+
+ // this is to keep BC and will be removed in a future version
+ if ($this->schemes) {
+ $this->requirements['_scheme'] = implode('|', $this->schemes);
+ } else {
+ unset($this->requirements['_scheme']);
+ }
+
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the uppercased HTTP methods this route is restricted to.
+ * So an empty array means that any method is allowed.
+ *
+ * @return array The schemes
+ */
+ public function getMethods()
+ {
+ return $this->methods;
+ }
+
+ /**
+ * Sets the HTTP methods (e.g. 'POST') this route is restricted to.
+ * So an empty array means that any method is allowed.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string|array $methods The method or an array of methods
+ *
+ * @return Route The current Route instance
+ */
+ public function setMethods($methods)
+ {
+ $this->methods = array_map('strtoupper', (array) $methods);
+
+ // this is to keep BC and will be removed in a future version
+ if ($this->methods) {
+ $this->requirements['_method'] = implode('|', $this->methods);
+ } else {
+ unset($this->requirements['_method']);
+ }
+
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the options.
+ *
+ * @return array The options
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Sets the options.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $options The options
+ *
+ * @return Route The current Route instance
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = array(
+ 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
+ );
+
+ return $this->addOptions($options);
+ }
+
+ /**
+ * Adds options.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $options The options
+ *
+ * @return Route The current Route instance
+ */
+ public function addOptions(array $options)
+ {
+ foreach ($options as $name => $option) {
+ $this->options[$name] = $option;
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Sets an option value.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param string $name An option name
+ * @param mixed $value The option value
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setOption($name, $value)
+ {
+ $this->options[$name] = $value;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Get an option value.
+ *
+ * @param string $name An option name
+ *
+ * @return mixed The option value or null when not given
+ */
+ public function getOption($name)
+ {
+ return isset($this->options[$name]) ? $this->options[$name] : null;
+ }
+
+ /**
+ * Checks if an option has been set
+ *
+ * @param string $name An option name
+ *
+ * @return Boolean true if the option is set, false otherwise
+ */
+ public function hasOption($name)
+ {
+ return array_key_exists($name, $this->options);
+ }
+
+ /**
+ * Returns the defaults.
+ *
+ * @return array The defaults
+ */
+ public function getDefaults()
+ {
+ return $this->defaults;
+ }
+
+ /**
+ * Sets the defaults.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $defaults The defaults
+ *
+ * @return Route The current Route instance
+ */
+ public function setDefaults(array $defaults)
+ {
+ $this->defaults = array();
+
+ return $this->addDefaults($defaults);
+ }
+
+ /**
+ * Adds defaults.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $defaults The defaults
+ *
+ * @return Route The current Route instance
+ */
+ public function addDefaults(array $defaults)
+ {
+ foreach ($defaults as $name => $default) {
+ $this->defaults[$name] = $default;
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Gets a default value.
+ *
+ * @param string $name A variable name
+ *
+ * @return mixed The default value or null when not given
+ */
+ public function getDefault($name)
+ {
+ return isset($this->defaults[$name]) ? $this->defaults[$name] : null;
+ }
+
+ /**
+ * Checks if a default value is set for the given variable.
+ *
+ * @param string $name A variable name
+ *
+ * @return Boolean true if the default value is set, false otherwise
+ */
+ public function hasDefault($name)
+ {
+ return array_key_exists($name, $this->defaults);
+ }
+
+ /**
+ * Sets a default value.
+ *
+ * @param string $name A variable name
+ * @param mixed $default The default value
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setDefault($name, $default)
+ {
+ $this->defaults[$name] = $default;
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the requirements.
+ *
+ * @return array The requirements
+ */
+ public function getRequirements()
+ {
+ return $this->requirements;
+ }
+
+ /**
+ * Sets the requirements.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $requirements The requirements
+ *
+ * @return Route The current Route instance
+ */
+ public function setRequirements(array $requirements)
+ {
+ $this->requirements = array();
+
+ return $this->addRequirements($requirements);
+ }
+
+ /**
+ * Adds requirements.
+ *
+ * This method implements a fluent interface.
+ *
+ * @param array $requirements The requirements
+ *
+ * @return Route The current Route instance
+ */
+ public function addRequirements(array $requirements)
+ {
+ foreach ($requirements as $key => $regex) {
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+ }
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Returns the requirement for the given key.
+ *
+ * @param string $key The key
+ *
+ * @return string|null The regex or null when not given
+ */
+ public function getRequirement($key)
+ {
+ return isset($this->requirements[$key]) ? $this->requirements[$key] : null;
+ }
+
+ /**
+ * Checks if a requirement is set for the given key.
+ *
+ * @param string $key A variable name
+ *
+ * @return Boolean true if a requirement is specified, false otherwise
+ */
+ public function hasRequirement($key)
+ {
+ return array_key_exists($key, $this->requirements);
+ }
+
+ /**
+ * Sets a requirement for the given key.
+ *
+ * @param string $key The key
+ * @param string $regex The regex
+ *
+ * @return Route The current Route instance
+ *
+ * @api
+ */
+ public function setRequirement($key, $regex)
+ {
+ $this->requirements[$key] = $this->sanitizeRequirement($key, $regex);
+ $this->compiled = null;
+
+ return $this;
+ }
+
+ /**
+ * Compiles the route.
+ *
+ * @return CompiledRoute A CompiledRoute instance
+ *
+ * @throws \LogicException If the Route cannot be compiled because the
+ * path or host pattern is invalid
+ *
+ * @see RouteCompiler which is responsible for the compilation process
+ */
+ public function compile()
+ {
+ if (null !== $this->compiled) {
+ return $this->compiled;
+ }
+
+ $class = $this->getOption('compiler_class');
+
+ return $this->compiled = $class::compile($this);
+ }
+
+ private function sanitizeRequirement($key, $regex)
+ {
+ if (!is_string($regex)) {
+ throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" must be a string.', $key));
+ }
+
+ if ('' !== $regex && '^' === $regex[0]) {
+ $regex = (string) substr($regex, 1); // returns false for a single character
+ }
+
+ if ('$' === substr($regex, -1)) {
+ $regex = substr($regex, 0, -1);
+ }
+
+ if ('' === $regex) {
+ throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key));
+ }
+
+ // this is to keep BC and will be removed in a future version
+ if ('_scheme' === $key) {
+ $this->setSchemes(explode('|', $regex));
+ } elseif ('_method' === $key) {
+ $this->setMethods(explode('|', $regex));
+ }
+
+ return $regex;
+ }
+}
--- /dev/null
+<?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\Routing;
+
+use Symfony\Component\Config\Resource\ResourceInterface;
+
+/**
+ * A RouteCollection represents a set of Route instances.
+ *
+ * When adding a route at the end of the collection, an existing route
+ * with the same name is removed first. So there can only be one route
+ * with a given name.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ *
+ * @api
+ */
+class RouteCollection implements \IteratorAggregate, \Countable
+{
+ /**
+ * @var Route[]
+ */
+ private $routes = array();
+
+ /**
+ * @var array
+ */
+ private $resources = array();
+
+ public function __clone()
+ {
+ foreach ($this->routes as $name => $route) {
+ $this->routes[$name] = clone $route;
+ }
+ }
+
+ /**
+ * Gets the current RouteCollection as an Iterator that includes all routes.
+ *
+ * It implements \IteratorAggregate.
+ *
+ * @see all()
+ *
+ * @return \ArrayIterator An \ArrayIterator object for iterating over routes
+ */
+ public function getIterator()
+ {
+ return new \ArrayIterator($this->routes);
+ }
+
+ /**
+ * Gets the number of Routes in this collection.
+ *
+ * @return int The number of routes
+ */
+ public function count()
+ {
+ return count($this->routes);
+ }
+
+ /**
+ * Adds a route.
+ *
+ * @param string $name The route name
+ * @param Route $route A Route instance
+ *
+ * @api
+ */
+ public function add($name, Route $route)
+ {
+ unset($this->routes[$name]);
+
+ $this->routes[$name] = $route;
+ }
+
+ /**
+ * Returns all routes in this collection.
+ *
+ * @return Route[] An array of routes
+ */
+ public function all()
+ {
+ return $this->routes;
+ }
+
+ /**
+ * Gets a route by name.
+ *
+ * @param string $name The route name
+ *
+ * @return Route|null A Route instance or null when not found
+ */
+ public function get($name)
+ {
+ return isset($this->routes[$name]) ? $this->routes[$name] : null;
+ }
+
+ /**
+ * Removes a route or an array of routes by name from the collection
+ *
+ * @param string|array $name The route name or an array of route names
+ */
+ public function remove($name)
+ {
+ foreach ((array) $name as $n) {
+ unset($this->routes[$n]);
+ }
+ }
+
+ /**
+ * Adds a route collection at the end of the current set by appending all
+ * routes of the added collection.
+ *
+ * @param RouteCollection $collection A RouteCollection instance
+ *
+ * @api
+ */
+ public function addCollection(RouteCollection $collection)
+ {
+ // we need to remove all routes with the same names first because just replacing them
+ // would not place the new route at the end of the merged array
+ foreach ($collection->all() as $name => $route) {
+ unset($this->routes[$name]);
+ $this->routes[$name] = $route;
+ }
+
+ $this->resources = array_merge($this->resources, $collection->getResources());
+ }
+
+ /**
+ * Adds a prefix to the path of all child routes.
+ *
+ * @param string $prefix An optional prefix to add before each pattern of the route collection
+ * @param array $defaults An array of default values
+ * @param array $requirements An array of requirements
+ *
+ * @api
+ */
+ public function addPrefix($prefix, array $defaults = array(), array $requirements = array())
+ {
+ $prefix = trim(trim($prefix), '/');
+
+ if ('' === $prefix) {
+ return;
+ }
+
+ foreach ($this->routes as $route) {
+ $route->setPath('/'.$prefix.$route->getPath());
+ $route->addDefaults($defaults);
+ $route->addRequirements($requirements);
+ }
+ }
+
+ /**
+ * Sets the host pattern on all routes.
+ *
+ * @param string $pattern The pattern
+ * @param array $defaults An array of default values
+ * @param array $requirements An array of requirements
+ */
+ public function setHost($pattern, array $defaults = array(), array $requirements = array())
+ {
+ foreach ($this->routes as $route) {
+ $route->setHost($pattern);
+ $route->addDefaults($defaults);
+ $route->addRequirements($requirements);
+ }
+ }
+
+ /**
+ * Adds defaults to all routes.
+ *
+ * An existing default value under the same name in a route will be overridden.
+ *
+ * @param array $defaults An array of default values
+ */
+ public function addDefaults(array $defaults)
+ {
+ if ($defaults) {
+ foreach ($this->routes as $route) {
+ $route->addDefaults($defaults);
+ }
+ }
+ }
+
+ /**
+ * Adds requirements to all routes.
+ *
+ * An existing requirement under the same name in a route will be overridden.
+ *
+ * @param array $requirements An array of requirements
+ */
+ public function addRequirements(array $requirements)
+ {
+ if ($requirements) {
+ foreach ($this->routes as $route) {
+ $route->addRequirements($requirements);
+ }
+ }
+ }
+
+ /**
+ * Adds options to all routes.
+ *
+ * An existing option value under the same name in a route will be overridden.
+ *
+ * @param array $options An array of options
+ */
+ public function addOptions(array $options)
+ {
+ if ($options) {
+ foreach ($this->routes as $route) {
+ $route->addOptions($options);
+ }
+ }
+ }
+
+ /**
+ * Sets the schemes (e.g. 'https') all child routes are restricted to.
+ *
+ * @param string|array $schemes The scheme or an array of schemes
+ */
+ public function setSchemes($schemes)
+ {
+ foreach ($this->routes as $route) {
+ $route->setSchemes($schemes);
+ }
+ }
+
+ /**
+ * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to.
+ *
+ * @param string|array $methods The method or an array of methods
+ */
+ public function setMethods($methods)
+ {
+ foreach ($this->routes as $route) {
+ $route->setMethods($methods);
+ }
+ }
+
+ /**
+ * Returns an array of resources loaded to build this collection.
+ *
+ * @return ResourceInterface[] An array of resources
+ */
+ public function getResources()
+ {
+ return array_unique($this->resources);
+ }
+
+ /**
+ * Adds a resource for this collection.
+ *
+ * @param ResourceInterface $resource A resource instance
+ */
+ public function addResource(ResourceInterface $resource)
+ {
+ $this->resources[] = $resource;
+ }
+}
--- /dev/null
+<?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\Routing;
+
+/**
+ * RouteCompiler compiles Route instances to CompiledRoute instances.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Tobias Schultze <http://tobion.de>
+ */
+class RouteCompiler implements RouteCompilerInterface
+{
+ const REGEX_DELIMITER = '#';
+
+ /**
+ * This string defines the characters that are automatically considered separators in front of
+ * optional placeholders (with default and no static text following). Such a single separator
+ * can be left out together with the optional placeholder from matching and generating URLs.
+ */
+ const SEPARATORS = '/,;.:-_~+*=@|';
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws \LogicException If a variable is referenced more than once
+ * @throws \DomainException If a variable name is numeric because PHP raises an error for such
+ * subpatterns in PCRE and thus would break matching, e.g. "(?P<123>.+)".
+ */
+ public static function compile(Route $route)
+ {
+ $staticPrefix = null;
+ $hostVariables = array();
+ $pathVariables = array();
+ $variables = array();
+ $tokens = array();
+ $regex = null;
+ $hostRegex = null;
+ $hostTokens = array();
+
+ if ('' !== $host = $route->getHost()) {
+ $result = self::compilePattern($route, $host, true);
+
+ $hostVariables = $result['variables'];
+ $variables = array_merge($variables, $hostVariables);
+
+ $hostTokens = $result['tokens'];
+ $hostRegex = $result['regex'];
+ }
+
+ $path = $route->getPath();
+
+ $result = self::compilePattern($route, $path, false);
+
+ $staticPrefix = $result['staticPrefix'];
+
+ $pathVariables = $result['variables'];
+ $variables = array_merge($variables, $pathVariables);
+
+ $tokens = $result['tokens'];
+ $regex = $result['regex'];
+
+ return new CompiledRoute(
+ $staticPrefix,
+ $regex,
+ $tokens,
+ $pathVariables,
+ $hostRegex,
+ $hostTokens,
+ $hostVariables,
+ array_unique($variables)
+ );
+ }
+
+ private static function compilePattern(Route $route, $pattern, $isHost)
+ {
+ $tokens = array();
+ $variables = array();
+ $matches = array();
+ $pos = 0;
+ $defaultSeparator = $isHost ? '.' : '/';
+
+ // Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable
+ // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself.
+ preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $varName = substr($match[0][0], 1, -1);
+ // get all static text preceding the current variable
+ $precedingText = substr($pattern, $pos, $match[0][1] - $pos);
+ $pos = $match[0][1] + strlen($match[0][0]);
+ $precedingChar = strlen($precedingText) > 0 ? substr($precedingText, -1) : '';
+ $isSeparator = '' !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);
+
+ if (is_numeric($varName)) {
+ throw new \DomainException(sprintf('Variable name "%s" cannot be numeric in route pattern "%s". Please use a different name.', $varName, $pattern));
+ }
+ if (in_array($varName, $variables)) {
+ throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName));
+ }
+
+ if ($isSeparator && strlen($precedingText) > 1) {
+ $tokens[] = array('text', substr($precedingText, 0, -1));
+ } elseif (!$isSeparator && strlen($precedingText) > 0) {
+ $tokens[] = array('text', $precedingText);
+ }
+
+ $regexp = $route->getRequirement($varName);
+ if (null === $regexp) {
+ $followingPattern = (string) substr($pattern, $pos);
+ // Find the next static character after the variable that functions as a separator. By default, this separator and '/'
+ // are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all
+ // and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are
+ // the same that will be matched. Example: new Route('/{page}.{_format}', array('_format' => 'html'))
+ // If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything.
+ // Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally
+ // part of {_format} when generating the URL, e.g. _format = 'mobile.html'.
+ $nextSeparator = self::findNextSeparator($followingPattern);
+ $regexp = sprintf(
+ '[^%s%s]+',
+ preg_quote($defaultSeparator, self::REGEX_DELIMITER),
+ $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ''
+ );
+ if (('' !== $nextSeparator && !preg_match('#^\{\w+\}#', $followingPattern)) || '' === $followingPattern) {
+ // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive
+ // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns.
+ // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow
+ // after it. This optimization cannot be applied when the next char is no real separator or when the next variable is
+ // directly adjacent, e.g. '/{x}{y}'.
+ $regexp .= '+';
+ }
+ }
+
+ $tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
+ $variables[] = $varName;
+ }
+
+ if ($pos < strlen($pattern)) {
+ $tokens[] = array('text', substr($pattern, $pos));
+ }
+
+ // find the first optional token
+ $firstOptional = PHP_INT_MAX;
+ if (!$isHost) {
+ for ($i = count($tokens) - 1; $i >= 0; $i--) {
+ $token = $tokens[$i];
+ if ('variable' === $token[0] && $route->hasDefault($token[3])) {
+ $firstOptional = $i;
+ } else {
+ break;
+ }
+ }
+ }
+
+ // compute the matching regexp
+ $regexp = '';
+ for ($i = 0, $nbToken = count($tokens); $i < $nbToken; $i++) {
+ $regexp .= self::computeRegexp($tokens, $i, $firstOptional);
+ }
+
+ return array(
+ 'staticPrefix' => 'text' === $tokens[0][0] ? $tokens[0][1] : '',
+ 'regex' => self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'s',
+ 'tokens' => array_reverse($tokens),
+ 'variables' => $variables,
+ );
+ }
+
+ /**
+ * Returns the next static character in the Route pattern that will serve as a separator.
+ *
+ * @param string $pattern The route pattern
+ *
+ * @return string The next static character that functions as separator (or empty string when none available)
+ */
+ private static function findNextSeparator($pattern)
+ {
+ if ('' == $pattern) {
+ // return empty string if pattern is empty or false (false which can be returned by substr)
+ return '';
+ }
+ // first remove all placeholders from the pattern so we can find the next real static character
+ $pattern = preg_replace('#\{\w+\}#', '', $pattern);
+
+ return isset($pattern[0]) && false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
+ }
+
+ /**
+ * Computes the regexp used to match a specific token. It can be static text or a subpattern.
+ *
+ * @param array $tokens The route tokens
+ * @param integer $index The index of the current token
+ * @param integer $firstOptional The index of the first optional token
+ *
+ * @return string The regexp pattern for a single token
+ */
+ private static function computeRegexp(array $tokens, $index, $firstOptional)
+ {
+ $token = $tokens[$index];
+ if ('text' === $token[0]) {
+ // Text tokens
+ return preg_quote($token[1], self::REGEX_DELIMITER);
+ } else {
+ // Variable tokens
+ if (0 === $index && 0 === $firstOptional) {
+ // When the only token is an optional variable token, the separator is required
+ return sprintf('%s(?P<%s>%s)?', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
+ } else {
+ $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
+ if ($index >= $firstOptional) {
+ // Enclose each optional token in a subpattern to make it optional.
+ // "?:" means it is non-capturing, i.e. the portion of the subject string that
+ // matched the optional subpattern is not passed back.
+ $regexp = "(?:$regexp";
+ $nbTokens = count($tokens);
+ if ($nbTokens - 1 == $index) {
+ // Close the optional subpatterns
+ $regexp .= str_repeat(")?", $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
+ }
+ }
+
+ return $regexp;
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Routing;
+
+/**
+ * RouteCompilerInterface is the interface that all RouteCompiler classes must implement.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RouteCompilerInterface
+{
+ /**
+ * Compiles the current route instance.
+ *
+ * @param Route $route A Route instance
+ *
+ * @return CompiledRoute A CompiledRoute instance
+ *
+ * @throws \LogicException If the Route cannot be compiled because the
+ * path or host pattern is invalid
+ */
+ public static function compile(Route $route);
+}
--- /dev/null
+<?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\Routing;
+
+use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\ConfigCache;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * The Router class is an example of the integration of all pieces of the
+ * routing system for easier use.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Router implements RouterInterface
+{
+ /**
+ * @var UrlMatcherInterface|null
+ */
+ protected $matcher;
+
+ /**
+ * @var UrlGeneratorInterface|null
+ */
+ protected $generator;
+
+ /**
+ * @var RequestContext
+ */
+ protected $context;
+
+ /**
+ * @var LoaderInterface
+ */
+ protected $loader;
+
+ /**
+ * @var RouteCollection|null
+ */
+ protected $collection;
+
+ /**
+ * @var mixed
+ */
+ protected $resource;
+
+ /**
+ * @var array
+ */
+ protected $options = array();
+
+ /**
+ * @var LoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * Constructor.
+ *
+ * @param LoaderInterface $loader A LoaderInterface instance
+ * @param mixed $resource The main resource to load
+ * @param array $options An array of options
+ * @param RequestContext $context The context
+ * @param LoggerInterface $logger A logger instance
+ */
+ public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, LoggerInterface $logger = null)
+ {
+ $this->loader = $loader;
+ $this->resource = $resource;
+ $this->logger = $logger;
+ $this->context = null === $context ? new RequestContext() : $context;
+ $this->setOptions($options);
+ }
+
+ /**
+ * Sets options.
+ *
+ * Available options:
+ *
+ * * cache_dir: The cache directory (or null to disable caching)
+ * * debug: Whether to enable debugging or not (false by default)
+ * * resource_type: Type hint for the main resource (optional)
+ *
+ * @param array $options An array of options
+ *
+ * @throws \InvalidArgumentException When unsupported option is provided
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = array(
+ 'cache_dir' => null,
+ 'debug' => false,
+ 'generator_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ 'generator_base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
+ 'generator_dumper_class' => 'Symfony\\Component\\Routing\\Generator\\Dumper\\PhpGeneratorDumper',
+ 'generator_cache_class' => 'ProjectUrlGenerator',
+ 'matcher_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ 'matcher_base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
+ 'matcher_dumper_class' => 'Symfony\\Component\\Routing\\Matcher\\Dumper\\PhpMatcherDumper',
+ 'matcher_cache_class' => 'ProjectUrlMatcher',
+ 'resource_type' => null,
+ 'strict_requirements' => true,
+ );
+
+ // check option names and live merge, if errors are encountered Exception will be thrown
+ $invalid = array();
+ foreach ($options as $key => $value) {
+ if (array_key_exists($key, $this->options)) {
+ $this->options[$key] = $value;
+ } else {
+ $invalid[] = $key;
+ }
+ }
+
+ if ($invalid) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('", "', $invalid)));
+ }
+ }
+
+ /**
+ * Sets an option.
+ *
+ * @param string $key The key
+ * @param mixed $value The value
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setOption($key, $value)
+ {
+ if (!array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * Gets an option value.
+ *
+ * @param string $key The key
+ *
+ * @return mixed The value
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getOption($key)
+ {
+ if (!array_key_exists($key, $this->options)) {
+ throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key));
+ }
+
+ return $this->options[$key];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRouteCollection()
+ {
+ if (null === $this->collection) {
+ $this->collection = $this->loader->load($this->resource, $this->options['resource_type']);
+ }
+
+ return $this->collection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContext(RequestContext $context)
+ {
+ $this->context = $context;
+
+ if (null !== $this->matcher) {
+ $this->getMatcher()->setContext($context);
+ }
+ if (null !== $this->generator) {
+ $this->getGenerator()->setContext($context);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate($name, $parameters = array(), $referenceType = self::ABSOLUTE_PATH)
+ {
+ return $this->getGenerator()->generate($name, $parameters, $referenceType);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function match($pathinfo)
+ {
+ return $this->getMatcher()->match($pathinfo);
+ }
+
+ /**
+ * Gets the UrlMatcher instance associated with this Router.
+ *
+ * @return UrlMatcherInterface A UrlMatcherInterface instance
+ */
+ public function getMatcher()
+ {
+ if (null !== $this->matcher) {
+ return $this->matcher;
+ }
+
+ if (null === $this->options['cache_dir'] || null === $this->options['matcher_cache_class']) {
+ return $this->matcher = new $this->options['matcher_class']($this->getRouteCollection(), $this->context);
+ }
+
+ $class = $this->options['matcher_cache_class'];
+ $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']);
+ if (!$cache->isFresh($class)) {
+ $dumper = new $this->options['matcher_dumper_class']($this->getRouteCollection());
+
+ $options = array(
+ 'class' => $class,
+ 'base_class' => $this->options['matcher_base_class'],
+ );
+
+ $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
+ }
+
+ require_once $cache;
+
+ return $this->matcher = new $class($this->context);
+ }
+
+ /**
+ * Gets the UrlGenerator instance associated with this Router.
+ *
+ * @return UrlGeneratorInterface A UrlGeneratorInterface instance
+ */
+ public function getGenerator()
+ {
+ if (null !== $this->generator) {
+ return $this->generator;
+ }
+
+ if (null === $this->options['cache_dir'] || null === $this->options['generator_cache_class']) {
+ $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger);
+ } else {
+ $class = $this->options['generator_cache_class'];
+ $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']);
+ if (!$cache->isFresh($class)) {
+ $dumper = new $this->options['generator_dumper_class']($this->getRouteCollection());
+
+ $options = array(
+ 'class' => $class,
+ 'base_class' => $this->options['generator_base_class'],
+ );
+
+ $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
+ }
+
+ require_once $cache;
+
+ $this->generator = new $class($this->context, $this->logger);
+ }
+
+ if ($this->generator instanceof ConfigurableRequirementsInterface) {
+ $this->generator->setStrictRequirements($this->options['strict_requirements']);
+ }
+
+ return $this->generator;
+ }
+}
--- /dev/null
+<?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\Routing;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
+
+/**
+ * RouterInterface is the interface that all Router classes must implement.
+ *
+ * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface
+{
+ /**
+ * Gets the RouteCollection instance associated with this Router.
+ *
+ * @return RouteCollection A RouteCollection instance
+ */
+ public function getRouteCollection();
+}
--- /dev/null
+<?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\Routing\Tests\Annotation;
+
+use Symfony\Component\Routing\Annotation\Route;
+
+class RouteTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException \BadMethodCallException
+ */
+ public function testInvalidRouteParameter()
+ {
+ $route = new Route(array('foo' => 'bar'));
+ }
+
+ /**
+ * @dataProvider getValidParameters
+ */
+ public function testRouteParameters($parameter, $value, $getter)
+ {
+ $route = new Route(array($parameter => $value));
+ $this->assertEquals($route->$getter(), $value);
+ }
+
+ public function getValidParameters()
+ {
+ return array(
+ array('value', '/Blog', 'getPattern'),
+ array('value', '/Blog', 'getPath'),
+ array('requirements', array('_method' => 'GET'), 'getRequirements'),
+ array('options', array('compiler_class' => 'RouteCompiler'), 'getOptions'),
+ array('name', 'blog_index', 'getName'),
+ array('defaults', array('_controller' => 'MyBlogBundle:Blog:index'), 'getDefaults'),
+ array('schemes', array('https'), 'getSchemes'),
+ array('methods', array('GET', 'POST'), 'getMethods'),
+ array('host', array('{locale}.example.com'), 'getHost')
+ );
+ }
+}
--- /dev/null
+<?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\Routing\Tests;
+
+use Symfony\Component\Routing\CompiledRoute;
+
+class CompiledRouteTest extends \PHPUnit_Framework_TestCase
+{
+ public function testAccessors()
+ {
+ $compiled = new CompiledRoute('prefix', 'regex', array('tokens'), array(), array(), array(), array(), array('variables'));
+ $this->assertEquals('prefix', $compiled->getStaticPrefix(), '__construct() takes a static prefix as its second argument');
+ $this->assertEquals('regex', $compiled->getRegex(), '__construct() takes a regexp as its third argument');
+ $this->assertEquals(array('tokens'), $compiled->getTokens(), '__construct() takes an array of tokens as its fourth argument');
+ $this->assertEquals(array('variables'), $compiled->getVariables(), '__construct() takes an array of variables as its ninth argument');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Fixtures\AnnotatedClasses;
+
+abstract class AbstractClass
+{
+}
--- /dev/null
+<?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\Routing\Tests\Fixtures\AnnotatedClasses;
+
+class BarClass
+{
+ public function routeAction($arg1, $arg2 = 'defaultValue2', $arg3 = 'defaultValue3')
+ {
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Fixtures\AnnotatedClasses;
+
+class FooClass
+{
+}
--- /dev/null
+<?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\Routing\Tests\Fixtures;
+
+use Symfony\Component\Routing\Loader\XmlFileLoader;
+use Symfony\Component\Config\Util\XmlUtils;
+
+/**
+ * XmlFileLoader with schema validation turned off
+ */
+class CustomXmlFileLoader extends XmlFileLoader
+{
+ protected function loadFile($file)
+ {
+ return XmlUtils::loadFile($file, function() { return true; });
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Fixtures;
+
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface
+{
+ public function redirect($path, $route, $scheme = null)
+ {
+ return array(
+ '_controller' => 'Some controller reference...',
+ 'path' => $path,
+ 'scheme' => $scheme,
+ );
+ }
+}
--- /dev/null
+# skip "real" requests
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteRule .* - [QSA,L]
+
+# foo
+RewriteCond %{REQUEST_URI} ^/foo/(baz|symfony)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foo,E=_ROUTING_param_bar:%1,E=_ROUTING_default_def:test]
+
+# foobar
+RewriteCond %{REQUEST_URI} ^/foo(?:/([^/]++))?$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:foobar,E=_ROUTING_param_bar:%1,E=_ROUTING_default_bar:toto]
+
+# bar
+RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
+RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
+RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
+RewriteCond %{REQUEST_URI} ^/bar/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:bar,E=_ROUTING_param_foo:%1]
+
+# baragain
+RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
+RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC]
+RewriteRule .* - [S=1,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_POST:1,E=_ROUTING_allow_HEAD:1]
+RewriteCond %{REQUEST_URI} ^/baragain/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baragain,E=_ROUTING_param_foo:%1]
+
+# baz
+RewriteCond %{REQUEST_URI} ^/test/baz$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz]
+
+# baz2
+RewriteCond %{REQUEST_URI} ^/test/baz\.html$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz2]
+
+# baz3
+RewriteCond %{REQUEST_URI} ^/test/baz3$
+RewriteRule .* $0/ [QSA,L,R=301]
+RewriteCond %{REQUEST_URI} ^/test/baz3/$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz3]
+
+# baz4
+RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
+RewriteRule .* $0/ [QSA,L,R=301]
+RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz4,E=_ROUTING_param_foo:%1]
+
+# baz5
+RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
+RewriteCond %{REQUEST_METHOD} !^(GET|HEAD)$ [NC]
+RewriteRule .* - [S=2,E=_ROUTING_allow_GET:1,E=_ROUTING_allow_HEAD:1]
+RewriteCond %{REQUEST_URI} ^/test/([^/]++)$
+RewriteRule .* $0/ [QSA,L,R=301]
+RewriteCond %{REQUEST_URI} ^/test/([^/]++)/$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5,E=_ROUTING_param_foo:%1]
+
+# baz5unsafe
+RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
+RewriteCond %{REQUEST_METHOD} !^(POST)$ [NC]
+RewriteRule .* - [S=1,E=_ROUTING_allow_POST:1]
+RewriteCond %{REQUEST_URI} ^/testunsafe/([^/]++)/$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz5unsafe,E=_ROUTING_param_foo:%1]
+
+# baz6
+RewriteCond %{REQUEST_URI} ^/test/baz$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz6,E=_ROUTING_default_foo:bar\ baz]
+
+# baz7
+RewriteCond %{REQUEST_URI} ^/te\ st/baz$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz7]
+
+# baz8
+RewriteCond %{REQUEST_URI} ^/te\\\ st/baz$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz8]
+
+# baz9
+RewriteCond %{REQUEST_URI} ^/test/(te\\\ st)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:baz9,E=_ROUTING_param_baz:%1]
+
+RewriteCond %{HTTP:Host} ^a\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_1:1]
+
+# route1
+RewriteCond %{ENV:__ROUTING_host_1} =1
+RewriteCond %{REQUEST_URI} ^/route1$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route1]
+
+# route2
+RewriteCond %{ENV:__ROUTING_host_1} =1
+RewriteCond %{REQUEST_URI} ^/c2/route2$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route2]
+
+RewriteCond %{HTTP:Host} ^b\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_2:1]
+
+# route3
+RewriteCond %{ENV:__ROUTING_host_2} =1
+RewriteCond %{REQUEST_URI} ^/c2/route3$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route3]
+
+RewriteCond %{HTTP:Host} ^a\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_3:1]
+
+# route4
+RewriteCond %{ENV:__ROUTING_host_3} =1
+RewriteCond %{REQUEST_URI} ^/route4$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route4]
+
+RewriteCond %{HTTP:Host} ^c\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_4:1]
+
+# route5
+RewriteCond %{ENV:__ROUTING_host_4} =1
+RewriteCond %{REQUEST_URI} ^/route5$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route5]
+
+# route6
+RewriteCond %{REQUEST_URI} ^/route6$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route6]
+
+RewriteCond %{HTTP:Host} ^([^\.]++)\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_5:1,E=__ROUTING_host_5_var1:%1]
+
+# route11
+RewriteCond %{ENV:__ROUTING_host_5} =1
+RewriteCond %{REQUEST_URI} ^/route11$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route11,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1}]
+
+# route12
+RewriteCond %{ENV:__ROUTING_host_5} =1
+RewriteCond %{REQUEST_URI} ^/route12$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route12,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_default_var1:val]
+
+# route13
+RewriteCond %{ENV:__ROUTING_host_5} =1
+RewriteCond %{REQUEST_URI} ^/route13/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route13,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_param_name:%1]
+
+# route14
+RewriteCond %{ENV:__ROUTING_host_5} =1
+RewriteCond %{REQUEST_URI} ^/route14/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route14,E=_ROUTING_param_var1:%{ENV:__ROUTING_host_5_var1},E=_ROUTING_param_name:%1,E=_ROUTING_default_var1:val]
+
+RewriteCond %{HTTP:Host} ^c\.example\.com$
+RewriteRule .? - [E=__ROUTING_host_6:1]
+
+# route15
+RewriteCond %{ENV:__ROUTING_host_6} =1
+RewriteCond %{REQUEST_URI} ^/route15/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route15,E=_ROUTING_param_name:%1]
+
+# route16
+RewriteCond %{REQUEST_URI} ^/route16/([^/]++)$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route16,E=_ROUTING_param_name:%1,E=_ROUTING_default_var1:val]
+
+# route17
+RewriteCond %{REQUEST_URI} ^/route17$
+RewriteRule .* app.php [QSA,L,E=_ROUTING_route:route17]
+
+# 405 Method Not Allowed
+RewriteCond %{ENV:_ROUTING__allow_GET} =1 [OR]
+RewriteCond %{ENV:_ROUTING__allow_HEAD} =1 [OR]
+RewriteCond %{ENV:_ROUTING__allow_POST} =1
+RewriteRule .* app.php [QSA,L]
--- /dev/null
+<?php
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * ProjectUrlMatcher
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
+{
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ public function match($pathinfo)
+ {
+ $allow = array();
+ $pathinfo = rawurldecode($pathinfo);
+
+ // foo
+ if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
+ }
+
+ if (0 === strpos($pathinfo, '/bar')) {
+ // bar
+ if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
+ $allow = array_merge($allow, array('GET', 'HEAD'));
+ goto not_bar;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ());
+ }
+ not_bar:
+
+ // barhead
+ if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
+ $allow = array_merge($allow, array('GET', 'HEAD'));
+ goto not_barhead;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ());
+ }
+ not_barhead:
+
+ }
+
+ if (0 === strpos($pathinfo, '/test')) {
+ if (0 === strpos($pathinfo, '/test/baz')) {
+ // baz
+ if ($pathinfo === '/test/baz') {
+ return array('_route' => 'baz');
+ }
+
+ // baz2
+ if ($pathinfo === '/test/baz.html') {
+ return array('_route' => 'baz2');
+ }
+
+ // baz3
+ if ($pathinfo === '/test/baz3/') {
+ return array('_route' => 'baz3');
+ }
+
+ }
+
+ // baz4
+ if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ());
+ }
+
+ // baz5
+ if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
+ if ($this->context->getMethod() != 'POST') {
+ $allow[] = 'POST';
+ goto not_baz5;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ());
+ }
+ not_baz5:
+
+ // baz.baz6
+ if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
+ if ($this->context->getMethod() != 'PUT') {
+ $allow[] = 'PUT';
+ goto not_bazbaz6;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ());
+ }
+ not_bazbaz6:
+
+ }
+
+ // foofoo
+ if ($pathinfo === '/foofoo') {
+ return array ( 'def' => 'test', '_route' => 'foofoo',);
+ }
+
+ // quoter
+ if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
+ }
+
+ // space
+ if ($pathinfo === '/spa ce') {
+ return array('_route' => 'space');
+ }
+
+ if (0 === strpos($pathinfo, '/a')) {
+ if (0 === strpos($pathinfo, '/a/b\'b')) {
+ // foo1
+ if (preg_match('#^/a/b\'b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ());
+ }
+
+ // bar1
+ if (preg_match('#^/a/b\'b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ());
+ }
+
+ }
+
+ // overridden
+ if (preg_match('#^/a/(?P<var>.*)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ());
+ }
+
+ if (0 === strpos($pathinfo, '/a/b\'b')) {
+ // foo2
+ if (preg_match('#^/a/b\'b/(?P<foo1>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ());
+ }
+
+ // bar2
+ if (preg_match('#^/a/b\'b/(?P<bar1>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ());
+ }
+
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/multi')) {
+ // helloWorld
+ if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
+ }
+
+ // overridden2
+ if ($pathinfo === '/multi/new') {
+ return array('_route' => 'overridden2');
+ }
+
+ // hey
+ if ($pathinfo === '/multi/hey/') {
+ return array('_route' => 'hey');
+ }
+
+ }
+
+ // foo3
+ if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ());
+ }
+
+ // bar3
+ if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ());
+ }
+
+ if (0 === strpos($pathinfo, '/aba')) {
+ // ababa
+ if ($pathinfo === '/ababa') {
+ return array('_route' => 'ababa');
+ }
+
+ // foo4
+ if (preg_match('#^/aba/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ());
+ }
+
+ }
+
+ $host = $this->context->getHost();
+
+ if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
+ // route1
+ if ($pathinfo === '/route1') {
+ return array('_route' => 'route1');
+ }
+
+ // route2
+ if ($pathinfo === '/c2/route2') {
+ return array('_route' => 'route2');
+ }
+
+ }
+
+ if (preg_match('#^b\\.example\\.com$#s', $host, $hostMatches)) {
+ // route3
+ if ($pathinfo === '/c2/route3') {
+ return array('_route' => 'route3');
+ }
+
+ }
+
+ if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
+ // route4
+ if ($pathinfo === '/route4') {
+ return array('_route' => 'route4');
+ }
+
+ }
+
+ if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
+ // route5
+ if ($pathinfo === '/route5') {
+ return array('_route' => 'route5');
+ }
+
+ }
+
+ // route6
+ if ($pathinfo === '/route6') {
+ return array('_route' => 'route6');
+ }
+
+ if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#s', $host, $hostMatches)) {
+ if (0 === strpos($pathinfo, '/route1')) {
+ // route11
+ if ($pathinfo === '/route11') {
+ return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ());
+ }
+
+ // route12
+ if ($pathinfo === '/route12') {
+ return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',));
+ }
+
+ // route13
+ if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ());
+ }
+
+ // route14
+ if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',));
+ }
+
+ }
+
+ }
+
+ if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
+ // route15
+ if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/route1')) {
+ // route16
+ if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
+ }
+
+ // route17
+ if ($pathinfo === '/route17') {
+ return array('_route' => 'route17');
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/a')) {
+ // a
+ if ($pathinfo === '/a/a...') {
+ return array('_route' => 'a');
+ }
+
+ if (0 === strpos($pathinfo, '/a/b')) {
+ // b
+ if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
+ }
+
+ // c
+ if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
+ }
+
+ }
+
+ }
+
+ throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
+ }
+}
--- /dev/null
+# skip "real" requests
+RewriteCond %{REQUEST_FILENAME} -f
+RewriteRule .* - [QSA,L]
+
+# foo
+RewriteCond %{REQUEST_URI} ^/foo$
+RewriteRule .* ap\ p_d\ ev.php [QSA,L,E=_ROUTING_route:foo]
--- /dev/null
+<?php
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * ProjectUrlMatcher
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher
+{
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ public function match($pathinfo)
+ {
+ $allow = array();
+ $pathinfo = rawurldecode($pathinfo);
+
+ // foo
+ if (0 === strpos($pathinfo, '/foo') && preg_match('#^/foo/(?P<bar>baz|symfony)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo')), array ( 'def' => 'test',));
+ }
+
+ if (0 === strpos($pathinfo, '/bar')) {
+ // bar
+ if (preg_match('#^/bar/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
+ $allow = array_merge($allow, array('GET', 'HEAD'));
+ goto not_bar;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar')), array ());
+ }
+ not_bar:
+
+ // barhead
+ if (0 === strpos($pathinfo, '/barhead') && preg_match('#^/barhead/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ if (!in_array($this->context->getMethod(), array('GET', 'HEAD'))) {
+ $allow = array_merge($allow, array('GET', 'HEAD'));
+ goto not_barhead;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'barhead')), array ());
+ }
+ not_barhead:
+
+ }
+
+ if (0 === strpos($pathinfo, '/test')) {
+ if (0 === strpos($pathinfo, '/test/baz')) {
+ // baz
+ if ($pathinfo === '/test/baz') {
+ return array('_route' => 'baz');
+ }
+
+ // baz2
+ if ($pathinfo === '/test/baz.html') {
+ return array('_route' => 'baz2');
+ }
+
+ // baz3
+ if (rtrim($pathinfo, '/') === '/test/baz3') {
+ if (substr($pathinfo, -1) !== '/') {
+ return $this->redirect($pathinfo.'/', 'baz3');
+ }
+
+ return array('_route' => 'baz3');
+ }
+
+ }
+
+ // baz4
+ if (preg_match('#^/test/(?P<foo>[^/]++)/?$#s', $pathinfo, $matches)) {
+ if (substr($pathinfo, -1) !== '/') {
+ return $this->redirect($pathinfo.'/', 'baz4');
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz4')), array ());
+ }
+
+ // baz5
+ if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
+ if ($this->context->getMethod() != 'POST') {
+ $allow[] = 'POST';
+ goto not_baz5;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz5')), array ());
+ }
+ not_baz5:
+
+ // baz.baz6
+ if (preg_match('#^/test/(?P<foo>[^/]++)/$#s', $pathinfo, $matches)) {
+ if ($this->context->getMethod() != 'PUT') {
+ $allow[] = 'PUT';
+ goto not_bazbaz6;
+ }
+
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'baz.baz6')), array ());
+ }
+ not_bazbaz6:
+
+ }
+
+ // foofoo
+ if ($pathinfo === '/foofoo') {
+ return array ( 'def' => 'test', '_route' => 'foofoo',);
+ }
+
+ // quoter
+ if (preg_match('#^/(?P<quoter>[\']+)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'quoter')), array ());
+ }
+
+ // space
+ if ($pathinfo === '/spa ce') {
+ return array('_route' => 'space');
+ }
+
+ if (0 === strpos($pathinfo, '/a')) {
+ if (0 === strpos($pathinfo, '/a/b\'b')) {
+ // foo1
+ if (preg_match('#^/a/b\'b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo1')), array ());
+ }
+
+ // bar1
+ if (preg_match('#^/a/b\'b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar1')), array ());
+ }
+
+ }
+
+ // overridden
+ if (preg_match('#^/a/(?P<var>.*)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'overridden')), array ());
+ }
+
+ if (0 === strpos($pathinfo, '/a/b\'b')) {
+ // foo2
+ if (preg_match('#^/a/b\'b/(?P<foo1>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo2')), array ());
+ }
+
+ // bar2
+ if (preg_match('#^/a/b\'b/(?P<bar1>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar2')), array ());
+ }
+
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/multi')) {
+ // helloWorld
+ if (0 === strpos($pathinfo, '/multi/hello') && preg_match('#^/multi/hello(?:/(?P<who>[^/]++))?$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'helloWorld')), array ( 'who' => 'World!',));
+ }
+
+ // overridden2
+ if ($pathinfo === '/multi/new') {
+ return array('_route' => 'overridden2');
+ }
+
+ // hey
+ if (rtrim($pathinfo, '/') === '/multi/hey') {
+ if (substr($pathinfo, -1) !== '/') {
+ return $this->redirect($pathinfo.'/', 'hey');
+ }
+
+ return array('_route' => 'hey');
+ }
+
+ }
+
+ // foo3
+ if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo3')), array ());
+ }
+
+ // bar3
+ if (preg_match('#^/(?P<_locale>[^/]++)/b/(?P<bar>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'bar3')), array ());
+ }
+
+ if (0 === strpos($pathinfo, '/aba')) {
+ // ababa
+ if ($pathinfo === '/ababa') {
+ return array('_route' => 'ababa');
+ }
+
+ // foo4
+ if (preg_match('#^/aba/(?P<foo>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'foo4')), array ());
+ }
+
+ }
+
+ $host = $this->context->getHost();
+
+ if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
+ // route1
+ if ($pathinfo === '/route1') {
+ return array('_route' => 'route1');
+ }
+
+ // route2
+ if ($pathinfo === '/c2/route2') {
+ return array('_route' => 'route2');
+ }
+
+ }
+
+ if (preg_match('#^b\\.example\\.com$#s', $host, $hostMatches)) {
+ // route3
+ if ($pathinfo === '/c2/route3') {
+ return array('_route' => 'route3');
+ }
+
+ }
+
+ if (preg_match('#^a\\.example\\.com$#s', $host, $hostMatches)) {
+ // route4
+ if ($pathinfo === '/route4') {
+ return array('_route' => 'route4');
+ }
+
+ }
+
+ if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
+ // route5
+ if ($pathinfo === '/route5') {
+ return array('_route' => 'route5');
+ }
+
+ }
+
+ // route6
+ if ($pathinfo === '/route6') {
+ return array('_route' => 'route6');
+ }
+
+ if (preg_match('#^(?P<var1>[^\\.]++)\\.example\\.com$#s', $host, $hostMatches)) {
+ if (0 === strpos($pathinfo, '/route1')) {
+ // route11
+ if ($pathinfo === '/route11') {
+ return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route11')), array ());
+ }
+
+ // route12
+ if ($pathinfo === '/route12') {
+ return $this->mergeDefaults(array_replace($hostMatches, array('_route' => 'route12')), array ( 'var1' => 'val',));
+ }
+
+ // route13
+ if (0 === strpos($pathinfo, '/route13') && preg_match('#^/route13/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route13')), array ());
+ }
+
+ // route14
+ if (0 === strpos($pathinfo, '/route14') && preg_match('#^/route14/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($hostMatches, $matches, array('_route' => 'route14')), array ( 'var1' => 'val',));
+ }
+
+ }
+
+ }
+
+ if (preg_match('#^c\\.example\\.com$#s', $host, $hostMatches)) {
+ // route15
+ if (0 === strpos($pathinfo, '/route15') && preg_match('#^/route15/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'route15')), array ());
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/route1')) {
+ // route16
+ if (0 === strpos($pathinfo, '/route16') && preg_match('#^/route16/(?P<name>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'route16')), array ( 'var1' => 'val',));
+ }
+
+ // route17
+ if ($pathinfo === '/route17') {
+ return array('_route' => 'route17');
+ }
+
+ }
+
+ if (0 === strpos($pathinfo, '/a')) {
+ // a
+ if ($pathinfo === '/a/a...') {
+ return array('_route' => 'a');
+ }
+
+ if (0 === strpos($pathinfo, '/a/b')) {
+ // b
+ if (preg_match('#^/a/b/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'b')), array ());
+ }
+
+ // c
+ if (0 === strpos($pathinfo, '/a/b/c') && preg_match('#^/a/b/c/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'c')), array ());
+ }
+
+ }
+
+ }
+
+ // secure
+ if ($pathinfo === '/secure') {
+ if ($this->context->getScheme() !== 'https') {
+ return $this->redirect($pathinfo, 'secure', 'https');
+ }
+
+ return array('_route' => 'secure');
+ }
+
+ // nonsecure
+ if ($pathinfo === '/nonsecure') {
+ if ($this->context->getScheme() !== 'http') {
+ return $this->redirect($pathinfo, 'nonsecure', 'http');
+ }
+
+ return array('_route' => 'nonsecure');
+ }
+
+ throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
+ }
+}
--- /dev/null
+<?php
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\RequestContext;
+
+/**
+ * ProjectUrlMatcher
+ *
+ * This class has been auto-generated
+ * by the Symfony Routing Component.
+ */
+class ProjectUrlMatcher extends Symfony\Component\Routing\Matcher\UrlMatcher
+{
+ /**
+ * Constructor.
+ */
+ public function __construct(RequestContext $context)
+ {
+ $this->context = $context;
+ }
+
+ public function match($pathinfo)
+ {
+ $allow = array();
+ $pathinfo = rawurldecode($pathinfo);
+
+ if (0 === strpos($pathinfo, '/rootprefix')) {
+ // static
+ if ($pathinfo === '/rootprefix/test') {
+ return array('_route' => 'static');
+ }
+
+ // dynamic
+ if (preg_match('#^/rootprefix/(?P<var>[^/]++)$#s', $pathinfo, $matches)) {
+ return $this->mergeDefaults(array_replace($matches, array('_route' => 'dynamic')), array ());
+ }
+
+ }
+
+ throw 0 < count($allow) ? new MethodNotAllowedException(array_unique($allow)) : new ResourceNotFoundException();
+ }
+}
--- /dev/null
+blog_show:
+ defaults: { _controller: MyBlogBundle:Blog:show }
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <route path="/test"></route>
+</routes>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <route id="myroute"></route>
+</routes>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<r:routes xmlns:r="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <r:route id="blog_show" path="/blog/{slug}" host="{_locale}.example.com">
+ <r:default key="_controller">MyBundle:Blog:show</r:default>
+ <requirement xmlns="http://symfony.com/schema/routing" key="slug">\w+</requirement>
+ <r2:requirement xmlns:r2="http://symfony.com/schema/routing" key="_locale">en|fr|de</r2:requirement>
+ <r:option key="compiler_class">RouteCompiler</r:option>
+ </r:route>
+</r:routes>
--- /dev/null
+blog_show:
+ resource: validpattern.yml
+ path: /test
--- /dev/null
+blog_show:
+ path: /blog/{slug}
+ type: custom
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <route id="blog_show" path="/blog/{slug}">
+ <default key="_controller">MyBundle:Blog:show</default>
+ <requirement key="_method">GET</requirement>
+ <!-- </route> -->
+</routes>
--- /dev/null
+route: string
--- /dev/null
+someroute:
+ resource: path/to/some.yml
+ name_prefix: test_
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <foo>bar</foo>
+</routes>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <route id="blog_show" path="/blog/{slug}">
+ <default key="_controller">MyBundle:Blog:show</default>
+ <requirement key="_method">GET</requirement>
+ <option key="compiler_class">RouteCompiler</option>
+ <foo key="bar">baz</foo>
+ </route>
+</routes>
--- /dev/null
+"#$péß^a|":
+ path: "true"
--- /dev/null
+<?php
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+
+$collection = new RouteCollection();
+$collection->add('blog_show', new Route(
+ '/blog/{slug}',
+ array('_controller' => 'MyBlogBundle:Blog:show'),
+ array('locale' => '\w+'),
+ array('compiler_class' => 'RouteCompiler'),
+ '{locale}.example.com',
+ array('https'),
+ array('GET','POST','put','OpTiOnS')
+));
+$collection->add('blog_show_legacy', new Route(
+ '/blog/{slug}',
+ array('_controller' => 'MyBlogBundle:Blog:show'),
+ array('_method' => 'GET|POST|put|OpTiOnS', '_scheme' => 'https', 'locale' => '\w+',),
+ array('compiler_class' => 'RouteCompiler'),
+ '{locale}.example.com'
+));
+
+return $collection;
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <route id="blog_show" path="/blog/{slug}" host="{locale}.example.com" methods="GET|POST put,OpTiOnS" schemes="hTTps">
+ <default key="_controller">MyBundle:Blog:show</default>
+ <requirement key="locale">\w+</requirement>
+ <option key="compiler_class">RouteCompiler</option>
+ </route>
+
+ <route id="blog_show_legacy" pattern="/blog/{slug}" host="{locale}.example.com">
+ <default key="_controller">MyBundle:Blog:show</default>
+ <default key="slug" xsi:nil="true" />
+ <requirement key="_method">GET|POST|put|OpTiOnS</requirement>
+ <requirement key="_scheme">hTTps</requirement>
+ <requirement key="locale">\w+</requirement>
+ <option key="compiler_class">RouteCompiler</option>
+ </route>
+</routes>
--- /dev/null
+blog_show:
+ path: /blog/{slug}
+ defaults: { _controller: "MyBundle:Blog:show" }
+ host: "{locale}.example.com"
+ requirements: { 'locale': '\w+' }
+ methods: ['GET','POST','put','OpTiOnS']
+ schemes: ['https']
+ options:
+ compiler_class: RouteCompiler
+
+blog_show_legacy:
+ pattern: /blog/{slug}
+ defaults: { _controller: "MyBundle:Blog:show" }
+ host: "{locale}.example.com"
+ requirements: { '_method': 'GET|POST|put|OpTiOnS', _scheme: https, 'locale': '\w+' }
+ options:
+ compiler_class: RouteCompiler
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<routes xmlns="http://symfony.com/schema/routing"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
+
+ <import resource="validpattern.xml" prefix="/{foo}" host="">
+ <default key="foo">123</default>
+ <requirement key="foo">\d+</requirement>
+ <option key="foo">bar</option>
+ </import>
+</routes>
--- /dev/null
+_blog:
+ resource: validpattern.yml
+ prefix: /{foo}
+ defaults: { 'foo': '123' }
+ requirements: { 'foo': '\d+' }
+ options: { 'foo': 'bar' }
+ host: ""
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE foo>
+<foo></foo>
--- /dev/null
+<?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\Routing\Tests\Generator\Dumper;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\Generator\Dumper\PhpGeneratorDumper;
+use Symfony\Component\Routing\RequestContext;
+
+class PhpGeneratorDumperTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var RouteCollection
+ */
+ private $routeCollection;
+
+ /**
+ * @var PhpGeneratorDumper
+ */
+ private $generatorDumper;
+
+ /**
+ * @var string
+ */
+ private $testTmpFilepath;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->routeCollection = new RouteCollection();
+ $this->generatorDumper = new PhpGeneratorDumper($this->routeCollection);
+ $this->testTmpFilepath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'php_generator.php';
+ @unlink($this->testTmpFilepath);
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ @unlink($this->testTmpFilepath);
+
+ $this->routeCollection = null;
+ $this->generatorDumper = null;
+ $this->testTmpFilepath = null;
+ }
+
+ public function testDumpWithRoutes()
+ {
+ $this->routeCollection->add('Test', new Route('/testing/{foo}'));
+ $this->routeCollection->add('Test2', new Route('/testing2'));
+
+ file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump());
+ include ($this->testTmpFilepath);
+
+ $projectUrlGenerator = new \ProjectUrlGenerator(new RequestContext('/app.php'));
+
+ $absoluteUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), true);
+ $absoluteUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), true);
+ $relativeUrlWithParameter = $projectUrlGenerator->generate('Test', array('foo' => 'bar'), false);
+ $relativeUrlWithoutParameter = $projectUrlGenerator->generate('Test2', array(), false);
+
+ $this->assertEquals($absoluteUrlWithParameter, 'http://localhost/app.php/testing/bar');
+ $this->assertEquals($absoluteUrlWithoutParameter, 'http://localhost/app.php/testing2');
+ $this->assertEquals($relativeUrlWithParameter, '/app.php/testing/bar');
+ $this->assertEquals($relativeUrlWithoutParameter, '/app.php/testing2');
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testDumpWithoutRoutes()
+ {
+ file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump(array('class' => 'WithoutRoutesUrlGenerator')));
+ include ($this->testTmpFilepath);
+
+ $projectUrlGenerator = new \WithoutRoutesUrlGenerator(new RequestContext('/app.php'));
+
+ $projectUrlGenerator->generate('Test', array());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
+ */
+ public function testGenerateNonExistingRoute()
+ {
+ $this->routeCollection->add('Test', new Route('/test'));
+
+ file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump(array('class' => 'NonExistingRoutesUrlGenerator')));
+ include ($this->testTmpFilepath);
+
+ $projectUrlGenerator = new \NonExistingRoutesUrlGenerator(new RequestContext());
+ $url = $projectUrlGenerator->generate('NonExisting', array());
+ }
+
+ public function testDumpForRouteWithDefaults()
+ {
+ $this->routeCollection->add('Test', new Route('/testing/{foo}', array('foo' => 'bar')));
+
+ file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump(array('class' => 'DefaultRoutesUrlGenerator')));
+ include ($this->testTmpFilepath);
+
+ $projectUrlGenerator = new \DefaultRoutesUrlGenerator(new RequestContext());
+ $url = $projectUrlGenerator->generate('Test', array());
+
+ $this->assertEquals($url, '/testing');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Generator;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Symfony\Component\Routing\RequestContext;
+
+class UrlGeneratorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testAbsoluteUrlWithPort80()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes)->generate('test', array(), true);
+
+ $this->assertEquals('http://localhost/app.php/testing', $url);
+ }
+
+ public function testAbsoluteSecureUrlWithPort443()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes, array('scheme' => 'https'))->generate('test', array(), true);
+
+ $this->assertEquals('https://localhost/app.php/testing', $url);
+ }
+
+ public function testAbsoluteUrlWithNonStandardPort()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes, array('httpPort' => 8080))->generate('test', array(), true);
+
+ $this->assertEquals('http://localhost:8080/app.php/testing', $url);
+ }
+
+ public function testAbsoluteSecureUrlWithNonStandardPort()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes, array('httpsPort' => 8080, 'scheme' => 'https'))->generate('test', array(), true);
+
+ $this->assertEquals('https://localhost:8080/app.php/testing', $url);
+ }
+
+ public function testRelativeUrlWithoutParameters()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes)->generate('test', array(), false);
+
+ $this->assertEquals('/app.php/testing', $url);
+ }
+
+ public function testRelativeUrlWithParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}'));
+ $url = $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), false);
+
+ $this->assertEquals('/app.php/testing/bar', $url);
+ }
+
+ public function testRelativeUrlWithNullParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing.{format}', array('format' => null)));
+ $url = $this->getGenerator($routes)->generate('test', array(), false);
+
+ $this->assertEquals('/app.php/testing', $url);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testRelativeUrlWithNullParameterButNotOptional()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}/bar', array('foo' => null)));
+ // This must raise an exception because the default requirement for "foo" is "[^/]+" which is not met with these params.
+ // Generating path "/testing//bar" would be wrong as matching this route would fail.
+ $this->getGenerator($routes)->generate('test', array(), false);
+ }
+
+ public function testRelativeUrlWithOptionalZeroParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{page}'));
+ $url = $this->getGenerator($routes)->generate('test', array('page' => 0), false);
+
+ $this->assertEquals('/app.php/testing/0', $url);
+ }
+
+ public function testNotPassedOptionalParameterInBetween()
+ {
+ $routes = $this->getRoutes('test', new Route('/{slug}/{page}', array('slug' => 'index', 'page' => 0)));
+ $this->assertSame('/app.php/index/1', $this->getGenerator($routes)->generate('test', array('page' => 1)));
+ $this->assertSame('/app.php/', $this->getGenerator($routes)->generate('test'));
+ }
+
+ public function testRelativeUrlWithExtraParameters()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), false);
+
+ $this->assertEquals('/app.php/testing?foo=bar', $url);
+ }
+
+ public function testAbsoluteUrlWithExtraParameters()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), true);
+
+ $this->assertEquals('http://localhost/app.php/testing?foo=bar', $url);
+ }
+
+ public function testUrlWithNullExtraParameters()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $url = $this->getGenerator($routes)->generate('test', array('foo' => null), true);
+
+ $this->assertEquals('http://localhost/app.php/testing', $url);
+ }
+
+ public function testUrlWithExtraParametersFromGlobals()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing'));
+ $generator = $this->getGenerator($routes);
+ $context = new RequestContext('/app.php');
+ $context->setParameter('bar', 'bar');
+ $generator->setContext($context);
+ $url = $generator->generate('test', array('foo' => 'bar'));
+
+ $this->assertEquals('/app.php/testing?foo=bar', $url);
+ }
+
+ public function testUrlWithGlobalParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}'));
+ $generator = $this->getGenerator($routes);
+ $context = new RequestContext('/app.php');
+ $context->setParameter('foo', 'bar');
+ $generator->setContext($context);
+ $url = $generator->generate('test', array());
+
+ $this->assertEquals('/app.php/testing/bar', $url);
+ }
+
+ public function testGlobalParameterHasHigherPriorityThanDefault()
+ {
+ $routes = $this->getRoutes('test', new Route('/{_locale}', array('_locale' => 'en')));
+ $generator = $this->getGenerator($routes);
+ $context = new RequestContext('/app.php');
+ $context->setParameter('_locale', 'de');
+ $generator->setContext($context);
+ $url = $generator->generate('test', array());
+
+ $this->assertSame('/app.php/de', $url);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException
+ */
+ public function testGenerateWithoutRoutes()
+ {
+ $routes = $this->getRoutes('foo', new Route('/testing/{foo}'));
+ $this->getGenerator($routes)->generate('test', array(), true);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\MissingMandatoryParametersException
+ */
+ public function testGenerateForRouteWithoutMandatoryParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}'));
+ $this->getGenerator($routes)->generate('test', array(), true);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testGenerateForRouteWithInvalidOptionalParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array('foo' => '1'), array('foo' => 'd+')));
+ $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), true);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testGenerateForRouteWithInvalidParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array(), array('foo' => '1|2')));
+ $this->getGenerator($routes)->generate('test', array('foo' => '0'), true);
+ }
+
+ public function testGenerateForRouteWithInvalidOptionalParameterNonStrict()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array('foo' => '1'), array('foo' => 'd+')));
+ $generator = $this->getGenerator($routes);
+ $generator->setStrictRequirements(false);
+ $this->assertNull($generator->generate('test', array('foo' => 'bar'), true));
+ }
+
+ public function testGenerateForRouteWithInvalidOptionalParameterNonStrictWithLogger()
+ {
+ if (!interface_exists('Psr\Log\LoggerInterface')) {
+ $this->markTestSkipped('The "psr/log" package is not available');
+ }
+
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array('foo' => '1'), array('foo' => 'd+')));
+ $logger = $this->getMock('Psr\Log\LoggerInterface');
+ $logger->expects($this->once())
+ ->method('error');
+ $generator = $this->getGenerator($routes, array(), $logger);
+ $generator->setStrictRequirements(false);
+ $this->assertNull($generator->generate('test', array('foo' => 'bar'), true));
+ }
+
+ public function testGenerateForRouteWithInvalidParameterButDisabledRequirementsCheck()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array('foo' => '1'), array('foo' => 'd+')));
+ $generator = $this->getGenerator($routes);
+ $generator->setStrictRequirements(null);
+ $this->assertSame('/app.php/testing/bar', $generator->generate('test', array('foo' => 'bar')));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testGenerateForRouteWithInvalidMandatoryParameter()
+ {
+ $routes = $this->getRoutes('test', new Route('/testing/{foo}', array(), array('foo' => 'd+')));
+ $this->getGenerator($routes)->generate('test', array('foo' => 'bar'), true);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testRequiredParamAndEmptyPassed()
+ {
+ $routes = $this->getRoutes('test', new Route('/{slug}', array(), array('slug' => '.+')));
+ $this->getGenerator($routes)->generate('test', array('slug' => ''));
+ }
+
+ public function testSchemeRequirementDoesNothingIfSameCurrentScheme()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http')));
+ $this->assertEquals('/app.php/', $this->getGenerator($routes)->generate('test'));
+
+ $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https')));
+ $this->assertEquals('/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test'));
+ }
+
+ public function testSchemeRequirementForcesAbsoluteUrl()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'https')));
+ $this->assertEquals('https://localhost/app.php/', $this->getGenerator($routes)->generate('test'));
+
+ $routes = $this->getRoutes('test', new Route('/', array(), array('_scheme' => 'http')));
+ $this->assertEquals('http://localhost/app.php/', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test'));
+ }
+
+ public function testPathWithTwoStartingSlashes()
+ {
+ $routes = $this->getRoutes('test', new Route('//path-and-not-domain'));
+
+ // this must not generate '//path-and-not-domain' because that would be a network path
+ $this->assertSame('/path-and-not-domain', $this->getGenerator($routes, array('BaseUrl' => ''))->generate('test'));
+ }
+
+ public function testNoTrailingSlashForMultipleOptionalParameters()
+ {
+ $routes = $this->getRoutes('test', new Route('/category/{slug1}/{slug2}/{slug3}', array('slug2' => null, 'slug3' => null)));
+
+ $this->assertEquals('/app.php/category/foo', $this->getGenerator($routes)->generate('test', array('slug1' => 'foo')));
+ }
+
+ public function testWithAnIntegerAsADefaultValue()
+ {
+ $routes = $this->getRoutes('test', new Route('/{default}', array('default' => 0)));
+
+ $this->assertEquals('/app.php/foo', $this->getGenerator($routes)->generate('test', array('default' => 'foo')));
+ }
+
+ public function testNullForOptionalParameterIsIgnored()
+ {
+ $routes = $this->getRoutes('test', new Route('/test/{default}', array('default' => 0)));
+
+ $this->assertEquals('/app.php/test', $this->getGenerator($routes)->generate('test', array('default' => null)));
+ }
+
+ public function testQueryParamSameAsDefault()
+ {
+ $routes = $this->getRoutes('test', new Route('/test', array('default' => 'value')));
+
+ $this->assertSame('/app.php/test', $this->getGenerator($routes)->generate('test', array('default' => 'foo')));
+ $this->assertSame('/app.php/test', $this->getGenerator($routes)->generate('test', array('default' => 'value')));
+ $this->assertSame('/app.php/test', $this->getGenerator($routes)->generate('test'));
+ }
+
+ public function testGenerateWithSpecialRouteName()
+ {
+ $routes = $this->getRoutes('$péß^a|', new Route('/bar'));
+
+ $this->assertSame('/app.php/bar', $this->getGenerator($routes)->generate('$péß^a|'));
+ }
+
+ public function testUrlEncoding()
+ {
+ // This tests the encoding of reserved characters that are used for delimiting of URI components (defined in RFC 3986)
+ // and other special ASCII chars. These chars are tested as static text path, variable path and query param.
+ $chars = '@:[]/()*\'" +,;-._~&$<>|{}%\\^`!?foo=bar#id';
+ $routes = $this->getRoutes('test', new Route("/$chars/{varpath}", array(), array('varpath' => '.+')));
+ $this->assertSame('/app.php/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id'
+ .'/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id'
+ .'?query=%40%3A%5B%5D%2F%28%29%2A%27%22+%2B%2C%3B-._%7E%26%24%3C%3E%7C%7B%7D%25%5C%5E%60%21%3Ffoo%3Dbar%23id',
+ $this->getGenerator($routes)->generate('test', array(
+ 'varpath' => $chars,
+ 'query' => $chars
+ ))
+ );
+ }
+
+ public function testEncodingOfRelativePathSegments()
+ {
+ $routes = $this->getRoutes('test', new Route('/dir/../dir/..'));
+ $this->assertSame('/app.php/dir/%2E%2E/dir/%2E%2E', $this->getGenerator($routes)->generate('test'));
+ $routes = $this->getRoutes('test', new Route('/dir/./dir/.'));
+ $this->assertSame('/app.php/dir/%2E/dir/%2E', $this->getGenerator($routes)->generate('test'));
+ $routes = $this->getRoutes('test', new Route('/a./.a/a../..a/...'));
+ $this->assertSame('/app.php/a./.a/a../..a/...', $this->getGenerator($routes)->generate('test'));
+ }
+
+ public function testAdjacentVariables()
+ {
+ $routes = $this->getRoutes('test', new Route('/{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '\d+')));
+ $generator = $this->getGenerator($routes);
+ $this->assertSame('/app.php/foo123', $generator->generate('test', array('x' => 'foo', 'y' => '123')));
+ $this->assertSame('/app.php/foo123bar.xml', $generator->generate('test', array('x' => 'foo', 'y' => '123', 'z' => 'bar', '_format' => 'xml')));
+
+ // The default requirement for 'x' should not allow the separator '.' in this case because it would otherwise match everything
+ // and following optional variables like _format could never match.
+ $this->setExpectedException('Symfony\Component\Routing\Exception\InvalidParameterException');
+ $generator->generate('test', array('x' => 'do.t', 'y' => '123', 'z' => 'bar', '_format' => 'xml'));
+ }
+
+ public function testOptionalVariableWithNoRealSeparator()
+ {
+ $routes = $this->getRoutes('test', new Route('/get{what}', array('what' => 'All')));
+ $generator = $this->getGenerator($routes);
+
+ $this->assertSame('/app.php/get', $generator->generate('test'));
+ $this->assertSame('/app.php/getSites', $generator->generate('test', array('what' => 'Sites')));
+ }
+
+ public function testRequiredVariableWithNoRealSeparator()
+ {
+ $routes = $this->getRoutes('test', new Route('/get{what}Suffix'));
+ $generator = $this->getGenerator($routes);
+
+ $this->assertSame('/app.php/getSitesSuffix', $generator->generate('test', array('what' => 'Sites')));
+ }
+
+ public function testDefaultRequirementOfVariable()
+ {
+ $routes = $this->getRoutes('test', new Route('/{page}.{_format}'));
+ $generator = $this->getGenerator($routes);
+
+ $this->assertSame('/app.php/index.mobile.html', $generator->generate('test', array('page' => 'index', '_format' => 'mobile.html')));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testDefaultRequirementOfVariableDisallowsSlash()
+ {
+ $routes = $this->getRoutes('test', new Route('/{page}.{_format}'));
+ $this->getGenerator($routes)->generate('test', array('page' => 'index', '_format' => 'sl/ash'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testDefaultRequirementOfVariableDisallowsNextSeparator()
+ {
+ $routes = $this->getRoutes('test', new Route('/{page}.{_format}'));
+ $this->getGenerator($routes)->generate('test', array('page' => 'do.t', '_format' => 'html'));
+ }
+
+ public function testWithHostDifferentFromContext()
+ {
+ $routes = $this->getRoutes('test', new Route('/{name}', array(), array(), array(), '{locale}.example.com'));
+
+ $this->assertEquals('//fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test', array('name' =>'Fabien', 'locale' => 'fr')));
+ }
+
+ public function testWithHostSameAsContext()
+ {
+ $routes = $this->getRoutes('test', new Route('/{name}', array(), array(), array(), '{locale}.example.com'));
+
+ $this->assertEquals('/app.php/Fabien', $this->getGenerator($routes, array('host' => 'fr.example.com'))->generate('test', array('name' =>'Fabien', 'locale' => 'fr')));
+ }
+
+ public function testWithHostSameAsContextAndAbsolute()
+ {
+ $routes = $this->getRoutes('test', new Route('/{name}', array(), array(), array(), '{locale}.example.com'));
+
+ $this->assertEquals('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes, array('host' => 'fr.example.com'))->generate('test', array('name' =>'Fabien', 'locale' => 'fr'), true));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testUrlWithInvalidParameterInHost()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array(), array('foo' => 'bar'), array(), '{foo}.example.com'));
+ $this->getGenerator($routes)->generate('test', array('foo' => 'baz'), false);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testUrlWithInvalidParameterInHostWhenParamHasADefaultValue()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array('foo' => 'bar'), array('foo' => 'bar'), array(), '{foo}.example.com'));
+ $this->getGenerator($routes)->generate('test', array('foo' => 'baz'), false);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException
+ */
+ public function testUrlWithInvalidParameterEqualsDefaultValueInHost()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array('foo' => 'baz'), array('foo' => 'bar'), array(), '{foo}.example.com'));
+ $this->getGenerator($routes)->generate('test', array('foo' => 'baz'), false);
+ }
+
+ public function testUrlWithInvalidParameterInHostInNonStrictMode()
+ {
+ $routes = $this->getRoutes('test', new Route('/', array(), array('foo' => 'bar'), array(), '{foo}.example.com'));
+ $generator = $this->getGenerator($routes);
+ $generator->setStrictRequirements(false);
+ $this->assertNull($generator->generate('test', array('foo' => 'baz'), false));
+ }
+
+ public function testGenerateNetworkPath()
+ {
+ $routes = $this->getRoutes('test', new Route('/{name}', array(), array('_scheme' => 'http'), array(), '{locale}.example.com'));
+
+ $this->assertSame('//fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test',
+ array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'network path with different host'
+ );
+ $this->assertSame('//fr.example.com/app.php/Fabien?query=string', $this->getGenerator($routes, array('host' => 'fr.example.com'))->generate('test',
+ array('name' =>'Fabien', 'locale' => 'fr', 'query' => 'string'), UrlGeneratorInterface::NETWORK_PATH), 'network path although host same as context'
+ );
+ $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes, array('scheme' => 'https'))->generate('test',
+ array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::NETWORK_PATH), 'absolute URL because scheme requirement does not match context'
+ );
+ $this->assertSame('http://fr.example.com/app.php/Fabien', $this->getGenerator($routes)->generate('test',
+ array('name' =>'Fabien', 'locale' => 'fr'), UrlGeneratorInterface::ABSOLUTE_URL), 'absolute URL with same scheme because it is requested'
+ );
+ }
+
+ public function testGenerateRelativePath()
+ {
+ $routes = new RouteCollection();
+ $routes->add('article', new Route('/{author}/{article}/'));
+ $routes->add('comments', new Route('/{author}/{article}/comments'));
+ $routes->add('host', new Route('/{article}', array(), array(), array(), '{author}.example.com'));
+ $routes->add('scheme', new Route('/{author}', array(), array('_scheme' => 'https')));
+ $routes->add('unrelated', new Route('/about'));
+
+ $generator = $this->getGenerator($routes, array('host' => 'example.com', 'pathInfo' => '/fabien/symfony-is-great/'));
+
+ $this->assertSame('comments', $generator->generate('comments',
+ array('author' =>'fabien', 'article' => 'symfony-is-great'), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('comments?page=2', $generator->generate('comments',
+ array('author' =>'fabien', 'article' => 'symfony-is-great', 'page' => 2), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('../twig-is-great/', $generator->generate('article',
+ array('author' =>'fabien', 'article' => 'twig-is-great'), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('../../bernhard/forms-are-great/', $generator->generate('article',
+ array('author' =>'bernhard', 'article' => 'forms-are-great'), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('//bernhard.example.com/app.php/forms-are-great', $generator->generate('host',
+ array('author' =>'bernhard', 'article' => 'forms-are-great'), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('https://example.com/app.php/bernhard', $generator->generate('scheme',
+ array('author' =>'bernhard'), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ $this->assertSame('../../about', $generator->generate('unrelated',
+ array(), UrlGeneratorInterface::RELATIVE_PATH)
+ );
+ }
+
+ /**
+ * @dataProvider provideRelativePaths
+ */
+ public function testGetRelativePath($sourcePath, $targetPath, $expectedPath)
+ {
+ $this->assertSame($expectedPath, UrlGenerator::getRelativePath($sourcePath, $targetPath));
+ }
+
+ public function provideRelativePaths()
+ {
+ return array(
+ array(
+ '/same/dir/',
+ '/same/dir/',
+ ''
+ ),
+ array(
+ '/same/file',
+ '/same/file',
+ ''
+ ),
+ array(
+ '/',
+ '/file',
+ 'file'
+ ),
+ array(
+ '/',
+ '/dir/file',
+ 'dir/file'
+ ),
+ array(
+ '/dir/file.html',
+ '/dir/different-file.html',
+ 'different-file.html'
+ ),
+ array(
+ '/same/dir/extra-file',
+ '/same/dir/',
+ './'
+ ),
+ array(
+ '/parent/dir/',
+ '/parent/',
+ '../'
+ ),
+ array(
+ '/parent/dir/extra-file',
+ '/parent/',
+ '../'
+ ),
+ array(
+ '/a/b/',
+ '/x/y/z/',
+ '../../x/y/z/'
+ ),
+ array(
+ '/a/b/c/d/e',
+ '/a/c/d',
+ '../../../c/d'
+ ),
+ array(
+ '/a/b/c//',
+ '/a/b/c/',
+ '../'
+ ),
+ array(
+ '/a/b/c/',
+ '/a/b/c//',
+ './/'
+ ),
+ array(
+ '/root/a/b/c/',
+ '/root/x/b/c/',
+ '../../../x/b/c/'
+ ),
+ array(
+ '/a/b/c/d/',
+ '/a',
+ '../../../../a'
+ ),
+ array(
+ '/special-chars/sp%20ce/1€/mäh/e=mc²',
+ '/special-chars/sp%20ce/1€/<µ>/e=mc²',
+ '../<µ>/e=mc²'
+ ),
+ array(
+ 'not-rooted',
+ 'dir/file',
+ 'dir/file'
+ ),
+ array(
+ '//dir/',
+ '',
+ '../../'
+ ),
+ array(
+ '/dir/',
+ '/dir/file:with-colon',
+ './file:with-colon'
+ ),
+ array(
+ '/dir/',
+ '/dir/subdir/file:with-colon',
+ 'subdir/file:with-colon'
+ ),
+ array(
+ '/dir/',
+ '/dir/:subdir/',
+ './:subdir/'
+ ),
+ );
+ }
+
+ protected function getGenerator(RouteCollection $routes, array $parameters = array(), $logger = null)
+ {
+ $context = new RequestContext('/app.php');
+ foreach ($parameters as $key => $value) {
+ $method = 'set'.$key;
+ $context->$method($value);
+ }
+ $generator = new UrlGenerator($routes, $context, $logger);
+
+ return $generator;
+ }
+
+ protected function getRoutes($name, Route $route)
+ {
+ $routes = new RouteCollection();
+ $routes->add($name, $route);
+
+ return $routes;
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+abstract class AbstractAnnotationLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Doctrine\\Common\\Version')) {
+ $this->markTestSkipped('Doctrine is not available.');
+ }
+ }
+
+ public function getReader()
+ {
+ return $this->getMockBuilder('Doctrine\Common\Annotations\Reader')
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+ }
+
+ public function getClassLoader($reader)
+ {
+ return $this->getMockBuilder('Symfony\Component\Routing\Loader\AnnotationClassLoader')
+ ->setConstructorArgs(array($reader))
+ ->getMockForAbstractClass()
+ ;
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+class AnnotationClassLoaderTest extends AbstractAnnotationLoaderTest
+{
+ protected $loader;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->reader = $this->getReader();
+ $this->loader = $this->getClassLoader($this->reader);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testLoadMissingClass()
+ {
+ $this->loader->load('MissingClass');
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testLoadAbstractClass()
+ {
+ $this->loader->load('Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\AbstractClass');
+ }
+
+ /**
+ * @dataProvider provideTestSupportsChecksResource
+ */
+ public function testSupportsChecksResource($resource, $expectedSupports)
+ {
+ $this->assertSame($expectedSupports, $this->loader->supports($resource), '->supports() returns true if the resource is loadable');
+ }
+
+ public function provideTestSupportsChecksResource()
+ {
+ return array(
+ array('class', true),
+ array('\fully\qualified\class\name', true),
+ array('namespaced\class\without\leading\slash', true),
+ array('ÿClassWithLegalSpecialCharacters', true),
+ array('5', false),
+ array('foo.foo', false),
+ array(null, false),
+ );
+ }
+
+ public function testSupportsChecksTypeIfSpecified()
+ {
+ $this->assertTrue($this->loader->supports('class', 'annotation'), '->supports() checks the resource type if specified');
+ $this->assertFalse($this->loader->supports('class', 'foo'), '->supports() checks the resource type if specified');
+ }
+
+ public function getLoadTests()
+ {
+ return array(
+ array(
+ 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass',
+ array('name'=>'route1'),
+ array('arg2' => 'defaultValue2', 'arg3' =>'defaultValue3')
+ ),
+ array(
+ 'Symfony\Component\Routing\Tests\Fixtures\AnnotatedClasses\BarClass',
+ array('name'=>'route1', 'defaults' => array('arg2' => 'foo')),
+ array('arg2' => 'defaultValue2', 'arg3' =>'defaultValue3')
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider getLoadTests
+ */
+ public function testLoad($className, $routeDatas = array(), $methodArgs = array())
+ {
+ $routeDatas = array_replace(array(
+ 'name' => 'route',
+ 'path' => '/',
+ 'requirements' => array(),
+ 'options' => array(),
+ 'defaults' => array(),
+ 'schemes' => array(),
+ 'methods' => array(),
+ ), $routeDatas);
+
+ $this->reader
+ ->expects($this->once())
+ ->method('getMethodAnnotations')
+ ->will($this->returnValue(array($this->getAnnotatedRoute($routeDatas))))
+ ;
+ $routeCollection = $this->loader->load($className);
+ $route = $routeCollection->get($routeDatas['name']);
+
+ $this->assertSame($routeDatas['path'], $route->getPath(), '->load preserves path annotation');
+ $this->assertSame($routeDatas['requirements'],$route->getRequirements(), '->load preserves requirements annotation');
+ $this->assertCount(0, array_intersect($route->getOptions(), $routeDatas['options']), '->load preserves options annotation');
+ $this->assertSame(array_replace($routeDatas['defaults'], $methodArgs), $route->getDefaults(), '->load preserves defaults annotation');
+ }
+
+ private function getAnnotatedRoute($datas)
+ {
+ return new \Symfony\Component\Routing\Annotation\Route($datas);
+ }
+
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
+use Symfony\Component\Config\FileLocator;
+
+class AnnotationDirectoryLoaderTest extends AbstractAnnotationLoaderTest
+{
+ protected $loader;
+ protected $reader;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->reader = $this->getReader();
+ $this->loader = new AnnotationDirectoryLoader(new FileLocator(), $this->getClassLoader($this->reader));
+ }
+
+ public function testLoad()
+ {
+ $this->reader->expects($this->exactly(2))->method('getClassAnnotation');
+
+ $this->reader
+ ->expects($this->any())
+ ->method('getMethodAnnotations')
+ ->will($this->returnValue(array()))
+ ;
+
+ $this->loader->load(__DIR__.'/../Fixtures/AnnotatedClasses');
+ }
+
+ public function testSupports()
+ {
+ $fixturesDir = __DIR__.'/../Fixtures';
+
+ $this->assertTrue($this->loader->supports($fixturesDir), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($this->loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($this->loader->supports($fixturesDir, 'annotation'), '->supports() checks the resource type if specified');
+ $this->assertFalse($this->loader->supports($fixturesDir, 'foo'), '->supports() checks the resource type if specified');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Routing\Loader\AnnotationFileLoader;
+use Symfony\Component\Config\FileLocator;
+
+class AnnotationFileLoaderTest extends AbstractAnnotationLoaderTest
+{
+ protected $loader;
+ protected $reader;
+
+ protected function setUp()
+ {
+ parent::setUp();
+
+ $this->reader = $this->getReader();
+ $this->loader = new AnnotationFileLoader(new FileLocator(), $this->getClassLoader($this->reader));
+ }
+
+ public function testLoad()
+ {
+ $this->reader->expects($this->once())->method('getClassAnnotation');
+
+ $this->loader->load(__DIR__.'/../Fixtures/AnnotatedClasses/FooClass.php');
+ }
+
+ public function testSupports()
+ {
+ $fixture = __DIR__.'/../Fixtures/annotated.php';
+
+ $this->assertTrue($this->loader->supports($fixture), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($this->loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($this->loader->supports($fixture, 'annotation'), '->supports() checks the resource type if specified');
+ $this->assertFalse($this->loader->supports($fixture, 'foo'), '->supports() checks the resource type if specified');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Routing\Loader\ClosureLoader;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+class ClosureLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\FileLocator')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testSupports()
+ {
+ $loader = new ClosureLoader();
+
+ $closure = function () {};
+
+ $this->assertTrue($loader->supports($closure), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($loader->supports($closure, 'closure'), '->supports() checks the resource type if specified');
+ $this->assertFalse($loader->supports($closure, 'foo'), '->supports() checks the resource type if specified');
+ }
+
+ public function testLoad()
+ {
+ $loader = new ClosureLoader();
+
+ $route = new Route('/');
+ $routes = $loader->load(function () use ($route) {
+ $routes = new RouteCollection();
+
+ $routes->add('foo', $route);
+
+ return $routes;
+ });
+
+ $this->assertEquals($route, $routes->get('foo'), '->load() loads a \Closure resource');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Routing\Loader\PhpFileLoader;
+
+class PhpFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\FileLocator')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testSupports()
+ {
+ $loader = new PhpFileLoader($this->getMock('Symfony\Component\Config\FileLocator'));
+
+ $this->assertTrue($loader->supports('foo.php'), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($loader->supports('foo.php', 'php'), '->supports() checks the resource type if specified');
+ $this->assertFalse($loader->supports('foo.php', 'foo'), '->supports() checks the resource type if specified');
+ }
+
+ public function testLoadWithRoute()
+ {
+ $loader = new PhpFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('validpattern.php');
+ $routes = $routeCollection->all();
+
+ $this->assertCount(2, $routes, 'Two routes are loaded');
+ $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
+
+ foreach ($routes as $route) {
+ $this->assertSame('/blog/{slug}', $route->getPath());
+ $this->assertSame('MyBlogBundle:Blog:show', $route->getDefault('_controller'));
+ $this->assertSame('{locale}.example.com', $route->getHost());
+ $this->assertSame('RouteCompiler', $route->getOption('compiler_class'));
+ $this->assertEquals(array('GET', 'POST', 'PUT', 'OPTIONS'), $route->getMethods());
+ $this->assertEquals(array('https'), $route->getSchemes());
+ }
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Routing\Loader\XmlFileLoader;
+use Symfony\Component\Routing\Tests\Fixtures\CustomXmlFileLoader;
+
+class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\FileLocator')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testSupports()
+ {
+ $loader = new XmlFileLoader($this->getMock('Symfony\Component\Config\FileLocator'));
+
+ $this->assertTrue($loader->supports('foo.xml'), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($loader->supports('foo.xml', 'xml'), '->supports() checks the resource type if specified');
+ $this->assertFalse($loader->supports('foo.xml', 'foo'), '->supports() checks the resource type if specified');
+ }
+
+ public function testLoadWithRoute()
+ {
+ $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('validpattern.xml');
+ $routes = $routeCollection->all();
+
+ $this->assertCount(2, $routes, 'Two routes are loaded');
+ $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
+
+ foreach ($routes as $route) {
+ $this->assertSame('/blog/{slug}', $route->getPath());
+ $this->assertSame('{locale}.example.com', $route->getHost());
+ $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller'));
+ $this->assertSame('\w+', $route->getRequirement('locale'));
+ $this->assertSame('RouteCompiler', $route->getOption('compiler_class'));
+ $this->assertEquals(array('GET', 'POST', 'PUT', 'OPTIONS'), $route->getMethods());
+ $this->assertEquals(array('https'), $route->getSchemes());
+ }
+ }
+
+ public function testLoadWithNamespacePrefix()
+ {
+ $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('namespaceprefix.xml');
+
+ $this->assertCount(1, $routeCollection->all(), 'One route is loaded');
+
+ $route = $routeCollection->get('blog_show');
+ $this->assertSame('/blog/{slug}', $route->getPath());
+ $this->assertSame('{_locale}.example.com', $route->getHost());
+ $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller'));
+ $this->assertSame('\w+', $route->getRequirement('slug'));
+ $this->assertSame('en|fr|de', $route->getRequirement('_locale'));
+ $this->assertSame(null, $route->getDefault('slug'));
+ $this->assertSame('RouteCompiler', $route->getOption('compiler_class'));
+ }
+
+ public function testLoadWithImport()
+ {
+ $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('validresource.xml');
+ $routes = $routeCollection->all();
+
+ $this->assertCount(2, $routes, 'Two routes are loaded');
+ $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
+
+ foreach ($routes as $route) {
+ $this->assertSame('/{foo}/blog/{slug}', $route->getPath());
+ $this->assertSame('123', $route->getDefault('foo'));
+ $this->assertSame('\d+', $route->getRequirement('foo'));
+ $this->assertSame('bar', $route->getOption('foo'));
+ $this->assertSame('', $route->getHost());
+ }
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @dataProvider getPathsToInvalidFiles
+ */
+ public function testLoadThrowsExceptionWithInvalidFile($filePath)
+ {
+ $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $loader->load($filePath);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @dataProvider getPathsToInvalidFiles
+ */
+ public function testLoadThrowsExceptionWithInvalidFileEvenWithoutSchemaValidation($filePath)
+ {
+ $loader = new CustomXmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $loader->load($filePath);
+ }
+
+ public function getPathsToInvalidFiles()
+ {
+ return array(array('nonvalidnode.xml'), array('nonvalidroute.xml'), array('nonvalid.xml'), array('missing_id.xml'), array('missing_path.xml'));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage Document types are not allowed.
+ */
+ public function testDocTypeIsNotAllowed()
+ {
+ $loader = new XmlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $loader->load('withdoctype.xml');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Loader;
+
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Routing\Loader\YamlFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\FileLocator')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\Yaml\Yaml')) {
+ $this->markTestSkipped('The "Yaml" component is not available');
+ }
+ }
+
+ public function testSupports()
+ {
+ $loader = new YamlFileLoader($this->getMock('Symfony\Component\Config\FileLocator'));
+
+ $this->assertTrue($loader->supports('foo.yml'), '->supports() returns true if the resource is loadable');
+ $this->assertFalse($loader->supports('foo.foo'), '->supports() returns true if the resource is loadable');
+
+ $this->assertTrue($loader->supports('foo.yml', 'yaml'), '->supports() checks the resource type if specified');
+ $this->assertFalse($loader->supports('foo.yml', 'foo'), '->supports() checks the resource type if specified');
+ }
+
+ public function testLoadDoesNothingIfEmpty()
+ {
+ $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $collection = $loader->load('empty.yml');
+
+ $this->assertEquals(array(), $collection->all());
+ $this->assertEquals(array(new FileResource(realpath(__DIR__.'/../Fixtures/empty.yml'))), $collection->getResources());
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @dataProvider getPathsToInvalidFiles
+ */
+ public function testLoadThrowsExceptionWithInvalidFile($filePath)
+ {
+ $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $loader->load($filePath);
+ }
+
+ public function getPathsToInvalidFiles()
+ {
+ return array(array('nonvalid.yml'), array('nonvalid2.yml'), array('incomplete.yml'), array('nonvalidkeys.yml'), array('nonesense_resource_plus_path.yml'), array('nonesense_type_without_resource.yml'));
+ }
+
+ public function testLoadSpecialRouteName()
+ {
+ $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('special_route_name.yml');
+ $route = $routeCollection->get('#$péß^a|');
+
+ $this->assertInstanceOf('Symfony\Component\Routing\Route', $route);
+ $this->assertSame('/true', $route->getPath());
+ }
+
+ public function testLoadWithRoute()
+ {
+ $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('validpattern.yml');
+ $routes = $routeCollection->all();
+
+ $this->assertCount(2, $routes, 'Two routes are loaded');
+ $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
+
+ foreach ($routes as $route) {
+ $this->assertSame('/blog/{slug}', $route->getPath());
+ $this->assertSame('{locale}.example.com', $route->getHost());
+ $this->assertSame('MyBundle:Blog:show', $route->getDefault('_controller'));
+ $this->assertSame('\w+', $route->getRequirement('locale'));
+ $this->assertSame('RouteCompiler', $route->getOption('compiler_class'));
+ $this->assertEquals(array('GET', 'POST', 'PUT', 'OPTIONS'), $route->getMethods());
+ $this->assertEquals(array('https'), $route->getSchemes());
+ }
+ }
+
+ public function testLoadWithResource()
+ {
+ $loader = new YamlFileLoader(new FileLocator(array(__DIR__.'/../Fixtures')));
+ $routeCollection = $loader->load('validresource.yml');
+ $routes = $routeCollection->all();
+
+ $this->assertCount(2, $routes, 'Two routes are loaded');
+ $this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
+
+ foreach ($routes as $route) {
+ $this->assertSame('/{foo}/blog/{slug}', $route->getPath());
+ $this->assertSame('123', $route->getDefault('foo'));
+ $this->assertSame('\d+', $route->getRequirement('foo'));
+ $this->assertSame('bar', $route->getOption('foo'));
+ $this->assertSame('', $route->getHost());
+ }
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Matcher\ApacheUrlMatcher;
+
+class ApacheUrlMatcherTest extends \PHPUnit_Framework_TestCase
+{
+ protected $server;
+
+ protected function setUp()
+ {
+ $this->server = $_SERVER;
+ }
+
+ protected function tearDown()
+ {
+ $_SERVER = $this->server;
+ }
+
+ /**
+ * @dataProvider getMatchData
+ */
+ public function testMatch($name, $pathinfo, $server, $expect)
+ {
+ $collection = new RouteCollection();
+ $context = new RequestContext();
+ $matcher = new ApacheUrlMatcher($collection, $context);
+
+ $_SERVER = $server;
+
+ $result = $matcher->match($pathinfo, $server);
+ $this->assertSame(var_export($expect, true), var_export($result, true));
+ }
+
+ public function getMatchData()
+ {
+ return array(
+ array(
+ 'Simple route',
+ '/hello/world',
+ array(
+ '_ROUTING_route' => 'hello',
+ '_ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ '_ROUTING_param_name' => 'world',
+ ),
+ array(
+ '_controller' => 'AcmeBundle:Default:index',
+ 'name' => 'world',
+ '_route' => 'hello',
+ ),
+ ),
+ array(
+ 'Route with params and defaults',
+ '/hello/hugo',
+ array(
+ '_ROUTING_route' => 'hello',
+ '_ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ '_ROUTING_param_name' => 'hugo',
+ '_ROUTING_default_name' => 'world',
+ ),
+ array(
+ 'name' => 'hugo',
+ '_controller' => 'AcmeBundle:Default:index',
+ '_route' => 'hello',
+ ),
+ ),
+ array(
+ 'Route with defaults only',
+ '/hello',
+ array(
+ '_ROUTING_route' => 'hello',
+ '_ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ '_ROUTING_default_name' => 'world',
+ ),
+ array(
+ 'name' => 'world',
+ '_controller' => 'AcmeBundle:Default:index',
+ '_route' => 'hello',
+ ),
+ ),
+ array(
+ 'REDIRECT_ envs',
+ '/hello/world',
+ array(
+ 'REDIRECT__ROUTING_route' => 'hello',
+ 'REDIRECT__ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ 'REDIRECT__ROUTING_param_name' => 'world',
+ ),
+ array(
+ '_controller' => 'AcmeBundle:Default:index',
+ 'name' => 'world',
+ '_route' => 'hello',
+ ),
+ ),
+ array(
+ 'REDIRECT_REDIRECT_ envs',
+ '/hello/world',
+ array(
+ 'REDIRECT_REDIRECT__ROUTING_route' => 'hello',
+ 'REDIRECT_REDIRECT__ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ 'REDIRECT_REDIRECT__ROUTING_param_name' => 'world',
+ ),
+ array(
+ '_controller' => 'AcmeBundle:Default:index',
+ 'name' => 'world',
+ '_route' => 'hello',
+ ),
+ ),
+ array(
+ 'REDIRECT_REDIRECT_ envs',
+ '/hello/world',
+ array(
+ 'REDIRECT_REDIRECT__ROUTING_route' => 'hello',
+ 'REDIRECT_REDIRECT__ROUTING_param__controller' => 'AcmeBundle:Default:index',
+ 'REDIRECT_REDIRECT__ROUTING_param_name' => 'world',
+ ),
+ array(
+ '_controller' => 'AcmeBundle:Default:index',
+ 'name' => 'world',
+ '_route' => 'hello',
+ ),
+ ),
+ );
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Matcher\Dumper\ApacheMatcherDumper;
+
+class ApacheMatcherDumperTest extends \PHPUnit_Framework_TestCase
+{
+ protected static $fixturesPath;
+
+ public static function setUpBeforeClass()
+ {
+ self::$fixturesPath = realpath(__DIR__.'/../../Fixtures/');
+ }
+
+ public function testDump()
+ {
+ $dumper = new ApacheMatcherDumper($this->getRouteCollection());
+
+ $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher1.apache', $dumper->dump(), '->dump() dumps basic routes to the correct apache format.');
+ }
+
+ /**
+ * @dataProvider provideEscapeFixtures
+ */
+ public function testEscapePattern($src, $dest, $char, $with, $message)
+ {
+ $r = new \ReflectionMethod(new ApacheMatcherDumper($this->getRouteCollection()), 'escape');
+ $r->setAccessible(true);
+ $this->assertEquals($dest, $r->invoke(null, $src, $char, $with), $message);
+ }
+
+ public function provideEscapeFixtures()
+ {
+ return array(
+ array('foo', 'foo', ' ', '-', 'Preserve string that should not be escaped'),
+ array('fo-o', 'fo-o', ' ', '-', 'Preserve string that should not be escaped'),
+ array('fo o', 'fo- o', ' ', '-', 'Escape special characters'),
+ array('fo-- o', 'fo--- o', ' ', '-', 'Escape special characters'),
+ array('fo- o', 'fo- o', ' ', '-', 'Do not escape already escaped string'),
+ );
+ }
+
+ public function testEscapeScriptName()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+ $dumper = new ApacheMatcherDumper($collection);
+ $this->assertStringEqualsFile(self::$fixturesPath.'/dumper/url_matcher2.apache', $dumper->dump(array('script_name' => 'ap p_d\ ev.php')));
+ }
+
+ private function getRouteCollection()
+ {
+ $collection = new RouteCollection();
+
+ // defaults and requirements
+ $collection->add('foo', new Route(
+ '/foo/{bar}',
+ array('def' => 'test'),
+ array('bar' => 'baz|symfony')
+ ));
+ // defaults parameters in pattern
+ $collection->add('foobar', new Route(
+ '/foo/{bar}',
+ array('bar' => 'toto')
+ ));
+ // method requirement
+ $collection->add('bar', new Route(
+ '/bar/{foo}',
+ array(),
+ array('_method' => 'GET|head')
+ ));
+ // method requirement (again)
+ $collection->add('baragain', new Route(
+ '/baragain/{foo}',
+ array(),
+ array('_method' => 'get|post')
+ ));
+ // simple
+ $collection->add('baz', new Route(
+ '/test/baz'
+ ));
+ // simple with extension
+ $collection->add('baz2', new Route(
+ '/test/baz.html'
+ ));
+ // trailing slash
+ $collection->add('baz3', new Route(
+ '/test/baz3/'
+ ));
+ // trailing slash with variable
+ $collection->add('baz4', new Route(
+ '/test/{foo}/'
+ ));
+ // trailing slash and safe method
+ $collection->add('baz5', new Route(
+ '/test/{foo}/',
+ array(),
+ array('_method' => 'get')
+ ));
+ // trailing slash and unsafe method
+ $collection->add('baz5unsafe', new Route(
+ '/testunsafe/{foo}/',
+ array(),
+ array('_method' => 'post')
+ ));
+ // complex
+ $collection->add('baz6', new Route(
+ '/test/baz',
+ array('foo' => 'bar baz')
+ ));
+ // space in path
+ $collection->add('baz7', new Route(
+ '/te st/baz'
+ ));
+ // space preceded with \ in path
+ $collection->add('baz8', new Route(
+ '/te\\ st/baz'
+ ));
+ // space preceded with \ in requirement
+ $collection->add('baz9', new Route(
+ '/test/{baz}',
+ array(),
+ array(
+ 'baz' => 'te\\\\ st',
+ )
+ ));
+
+ $collection1 = new RouteCollection();
+
+ $route1 = new Route('/route1', array(), array(), array(), 'a.example.com');
+ $collection1->add('route1', $route1);
+
+ $collection2 = new RouteCollection();
+
+ $route2 = new Route('/route2', array(), array(), array(), 'a.example.com');
+ $collection2->add('route2', $route2);
+
+ $route3 = new Route('/route3', array(), array(), array(), 'b.example.com');
+ $collection2->add('route3', $route3);
+
+ $collection2->addPrefix('/c2');
+ $collection1->addCollection($collection2);
+
+ $route4 = new Route('/route4', array(), array(), array(), 'a.example.com');
+ $collection1->add('route4', $route4);
+
+ $route5 = new Route('/route5', array(), array(), array(), 'c.example.com');
+ $collection1->add('route5', $route5);
+
+ $route6 = new Route('/route6', array(), array(), array(), null);
+ $collection1->add('route6', $route6);
+
+ $collection->addCollection($collection1);
+
+ // host and variables
+
+ $collection1 = new RouteCollection();
+
+ $route11 = new Route('/route11', array(), array(), array(), '{var1}.example.com');
+ $collection1->add('route11', $route11);
+
+ $route12 = new Route('/route12', array('var1' => 'val'), array(), array(), '{var1}.example.com');
+ $collection1->add('route12', $route12);
+
+ $route13 = new Route('/route13/{name}', array(), array(), array(), '{var1}.example.com');
+ $collection1->add('route13', $route13);
+
+ $route14 = new Route('/route14/{name}', array('var1' => 'val'), array(), array(), '{var1}.example.com');
+ $collection1->add('route14', $route14);
+
+ $route15 = new Route('/route15/{name}', array(), array(), array(), 'c.example.com');
+ $collection1->add('route15', $route15);
+
+ $route16 = new Route('/route16/{name}', array('var1' => 'val'), array(), array(), null);
+ $collection1->add('route16', $route16);
+
+ $route17 = new Route('/route17', array(), array(), array(), null);
+ $collection1->add('route17', $route17);
+
+ $collection->addCollection($collection1);
+
+ return $collection;
+ }
+}
--- /dev/null
+<?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\Routing\Test\Matcher\Dumper;
+
+use Symfony\Component\Routing\Matcher\Dumper\DumperCollection;
+
+class DumperCollectionTest extends \PHPUnit_Framework_TestCase
+{
+ public function testGetRoot()
+ {
+ $a = new DumperCollection();
+
+ $b = new DumperCollection();
+ $a->add($b);
+
+ $c = new DumperCollection();
+ $b->add($c);
+
+ $d = new DumperCollection();
+ $c->add($d);
+
+ $this->assertSame($a, $c->getRoot());
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher\Dumper;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\Matcher\Dumper\DumperPrefixCollection;
+use Symfony\Component\Routing\Matcher\Dumper\DumperRoute;
+use Symfony\Component\Routing\Matcher\Dumper\DumperCollection;
+
+class DumperPrefixCollectionTest extends \PHPUnit_Framework_TestCase
+{
+ public function testAddPrefixRoute()
+ {
+ $coll = new DumperPrefixCollection;
+ $coll->setPrefix('');
+
+ $route = new DumperRoute('bar', new Route('/foo/bar'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('bar2', new Route('/foo/bar'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('qux', new Route('/foo/qux'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('bar3', new Route('/foo/bar'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('bar4', new Route(''));
+ $result = $coll->addPrefixRoute($route);
+
+ $expect = <<<'EOF'
+ |-coll /
+ | |-coll /f
+ | | |-coll /fo
+ | | | |-coll /foo
+ | | | | |-coll /foo/
+ | | | | | |-coll /foo/b
+ | | | | | | |-coll /foo/ba
+ | | | | | | | |-coll /foo/bar
+ | | | | | | | | |-route bar /foo/bar
+ | | | | | | | | |-route bar2 /foo/bar
+ | | | | | |-coll /foo/q
+ | | | | | | |-coll /foo/qu
+ | | | | | | | |-coll /foo/qux
+ | | | | | | | | |-route qux /foo/qux
+ | | | | | |-coll /foo/b
+ | | | | | | |-coll /foo/ba
+ | | | | | | | |-coll /foo/bar
+ | | | | | | | | |-route bar3 /foo/bar
+ | |-route bar4 /
+
+EOF;
+
+ $this->assertSame($expect, $this->collectionToString($result->getRoot(), ' '));
+ }
+
+ public function testMergeSlashNodes()
+ {
+ $coll = new DumperPrefixCollection;
+ $coll->setPrefix('');
+
+ $route = new DumperRoute('bar', new Route('/foo/bar'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('bar2', new Route('/foo/bar'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('qux', new Route('/foo/qux'));
+ $coll = $coll->addPrefixRoute($route);
+
+ $route = new DumperRoute('bar3', new Route('/foo/bar'));
+ $result = $coll->addPrefixRoute($route);
+
+ $result->getRoot()->mergeSlashNodes();
+
+ $expect = <<<'EOF'
+ |-coll /f
+ | |-coll /fo
+ | | |-coll /foo
+ | | | |-coll /foo/b
+ | | | | |-coll /foo/ba
+ | | | | | |-coll /foo/bar
+ | | | | | | |-route bar /foo/bar
+ | | | | | | |-route bar2 /foo/bar
+ | | | |-coll /foo/q
+ | | | | |-coll /foo/qu
+ | | | | | |-coll /foo/qux
+ | | | | | | |-route qux /foo/qux
+ | | | |-coll /foo/b
+ | | | | |-coll /foo/ba
+ | | | | | |-coll /foo/bar
+ | | | | | | |-route bar3 /foo/bar
+
+EOF;
+
+ $this->assertSame($expect, $this->collectionToString($result->getRoot(), ' '));
+ }
+
+ private function collectionToString(DumperCollection $collection, $prefix)
+ {
+ $string = '';
+ foreach ($collection as $route) {
+ if ($route instanceof DumperCollection) {
+ $string .= sprintf("%s|-coll %s\n", $prefix, $route->getPrefix());
+ $string .= $this->collectionToString($route, $prefix.'| ');
+ } else {
+ $string .= sprintf("%s|-route %s %s\n", $prefix, $route->getName(), $route->getRoute()->getPath());
+ }
+ }
+
+ return $string;
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher\Dumper;
+
+use Symfony\Component\Routing\Matcher\Dumper\PhpMatcherDumper;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+
+class PhpMatcherDumperTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException \LogicException
+ */
+ public function testDumpWhenSchemeIsUsedWithoutAProperDumper()
+ {
+ $collection = new RouteCollection();
+ $collection->add('secure', new Route(
+ '/secure',
+ array(),
+ array('_scheme' => 'https')
+ ));
+ $dumper = new PhpMatcherDumper($collection);
+ $dumper->dump();
+ }
+
+ /**
+ * @dataProvider getRouteCollections
+ */
+ public function testDump(RouteCollection $collection, $fixture, $options = array())
+ {
+ $basePath = __DIR__.'/../../Fixtures/dumper/';
+
+ $dumper = new PhpMatcherDumper($collection);
+ $this->assertStringEqualsFile($basePath.$fixture, $dumper->dump($options), '->dump() correctly dumps routes as optimized PHP code.');
+ }
+
+ public function getRouteCollections()
+ {
+ /* test case 1 */
+
+ $collection = new RouteCollection();
+
+ $collection->add('overridden', new Route('/overridden'));
+
+ // defaults and requirements
+ $collection->add('foo', new Route(
+ '/foo/{bar}',
+ array('def' => 'test'),
+ array('bar' => 'baz|symfony')
+ ));
+ // method requirement
+ $collection->add('bar', new Route(
+ '/bar/{foo}',
+ array(),
+ array('_method' => 'GET|head')
+ ));
+ // GET method requirement automatically adds HEAD as valid
+ $collection->add('barhead', new Route(
+ '/barhead/{foo}',
+ array(),
+ array('_method' => 'GET')
+ ));
+ // simple
+ $collection->add('baz', new Route(
+ '/test/baz'
+ ));
+ // simple with extension
+ $collection->add('baz2', new Route(
+ '/test/baz.html'
+ ));
+ // trailing slash
+ $collection->add('baz3', new Route(
+ '/test/baz3/'
+ ));
+ // trailing slash with variable
+ $collection->add('baz4', new Route(
+ '/test/{foo}/'
+ ));
+ // trailing slash and method
+ $collection->add('baz5', new Route(
+ '/test/{foo}/',
+ array(),
+ array('_method' => 'post')
+ ));
+ // complex name
+ $collection->add('baz.baz6', new Route(
+ '/test/{foo}/',
+ array(),
+ array('_method' => 'put')
+ ));
+ // defaults without variable
+ $collection->add('foofoo', new Route(
+ '/foofoo',
+ array('def' => 'test')
+ ));
+ // pattern with quotes
+ $collection->add('quoter', new Route(
+ '/{quoter}',
+ array(),
+ array('quoter' => '[\']+')
+ ));
+ // space in pattern
+ $collection->add('space', new Route(
+ '/spa ce'
+ ));
+
+ // prefixes
+ $collection1 = new RouteCollection();
+ $collection1->add('overridden', new Route('/overridden1'));
+ $collection1->add('foo1', new Route('/{foo}'));
+ $collection1->add('bar1', new Route('/{bar}'));
+ $collection1->addPrefix('/b\'b');
+ $collection2 = new RouteCollection();
+ $collection2->addCollection($collection1);
+ $collection2->add('overridden', new Route('/{var}', array(), array('var' => '.*')));
+ $collection1 = new RouteCollection();
+ $collection1->add('foo2', new Route('/{foo1}'));
+ $collection1->add('bar2', new Route('/{bar1}'));
+ $collection1->addPrefix('/b\'b');
+ $collection2->addCollection($collection1);
+ $collection2->addPrefix('/a');
+ $collection->addCollection($collection2);
+
+ // overridden through addCollection() and multiple sub-collections with no own prefix
+ $collection1 = new RouteCollection();
+ $collection1->add('overridden2', new Route('/old'));
+ $collection1->add('helloWorld', new Route('/hello/{who}', array('who' => 'World!')));
+ $collection2 = new RouteCollection();
+ $collection3 = new RouteCollection();
+ $collection3->add('overridden2', new Route('/new'));
+ $collection3->add('hey', new Route('/hey/'));
+ $collection2->addCollection($collection3);
+ $collection1->addCollection($collection2);
+ $collection1->addPrefix('/multi');
+ $collection->addCollection($collection1);
+
+ // "dynamic" prefix
+ $collection1 = new RouteCollection();
+ $collection1->add('foo3', new Route('/{foo}'));
+ $collection1->add('bar3', new Route('/{bar}'));
+ $collection1->addPrefix('/b');
+ $collection1->addPrefix('{_locale}');
+ $collection->addCollection($collection1);
+
+ // route between collections
+ $collection->add('ababa', new Route('/ababa'));
+
+ // collection with static prefix but only one route
+ $collection1 = new RouteCollection();
+ $collection1->add('foo4', new Route('/{foo}'));
+ $collection1->addPrefix('/aba');
+ $collection->addCollection($collection1);
+
+ // prefix and host
+
+ $collection1 = new RouteCollection();
+
+ $route1 = new Route('/route1', array(), array(), array(), 'a.example.com');
+ $collection1->add('route1', $route1);
+
+ $collection2 = new RouteCollection();
+
+ $route2 = new Route('/c2/route2', array(), array(), array(), 'a.example.com');
+ $collection1->add('route2', $route2);
+
+ $route3 = new Route('/c2/route3', array(), array(), array(), 'b.example.com');
+ $collection1->add('route3', $route3);
+
+ $route4 = new Route('/route4', array(), array(), array(), 'a.example.com');
+ $collection1->add('route4', $route4);
+
+ $route5 = new Route('/route5', array(), array(), array(), 'c.example.com');
+ $collection1->add('route5', $route5);
+
+ $route6 = new Route('/route6', array(), array(), array(), null);
+ $collection1->add('route6', $route6);
+
+ $collection->addCollection($collection1);
+
+ // host and variables
+
+ $collection1 = new RouteCollection();
+
+ $route11 = new Route('/route11', array(), array(), array(), '{var1}.example.com');
+ $collection1->add('route11', $route11);
+
+ $route12 = new Route('/route12', array('var1' => 'val'), array(), array(), '{var1}.example.com');
+ $collection1->add('route12', $route12);
+
+ $route13 = new Route('/route13/{name}', array(), array(), array(), '{var1}.example.com');
+ $collection1->add('route13', $route13);
+
+ $route14 = new Route('/route14/{name}', array('var1' => 'val'), array(), array(), '{var1}.example.com');
+ $collection1->add('route14', $route14);
+
+ $route15 = new Route('/route15/{name}', array(), array(), array(), 'c.example.com');
+ $collection1->add('route15', $route15);
+
+ $route16 = new Route('/route16/{name}', array('var1' => 'val'), array(), array(), null);
+ $collection1->add('route16', $route16);
+
+ $route17 = new Route('/route17', array(), array(), array(), null);
+ $collection1->add('route17', $route17);
+
+ $collection->addCollection($collection1);
+
+ // multiple sub-collections with a single route and a prefix each
+ $collection1 = new RouteCollection();
+ $collection1->add('a', new Route('/a...'));
+ $collection2 = new RouteCollection();
+ $collection2->add('b', new Route('/{var}'));
+ $collection3 = new RouteCollection();
+ $collection3->add('c', new Route('/{var}'));
+ $collection3->addPrefix('/c');
+ $collection2->addCollection($collection3);
+ $collection2->addPrefix('/b');
+ $collection1->addCollection($collection2);
+ $collection1->addPrefix('/a');
+ $collection->addCollection($collection1);
+
+ /* test case 2 */
+
+ $redirectCollection = clone $collection;
+
+ // force HTTPS redirection
+ $redirectCollection->add('secure', new Route(
+ '/secure',
+ array(),
+ array('_scheme' => 'https')
+ ));
+
+ // force HTTP redirection
+ $redirectCollection->add('nonsecure', new Route(
+ '/nonsecure',
+ array(),
+ array('_scheme' => 'http')
+ ));
+
+ /* test case 3 */
+
+ $rootprefixCollection = new RouteCollection();
+ $rootprefixCollection->add('static', new Route('/test'));
+ $rootprefixCollection->add('dynamic', new Route('/{var}'));
+ $rootprefixCollection->addPrefix('rootprefix');
+
+ return array(
+ array($collection, 'url_matcher1.php', array()),
+ array($redirectCollection, 'url_matcher2.php', array('base_class' => 'Symfony\Component\Routing\Tests\Fixtures\RedirectableUrlMatcher')),
+ array($rootprefixCollection, 'url_matcher3.php', array())
+ );
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+
+class RedirectableUrlMatcherTest extends \PHPUnit_Framework_TestCase
+{
+ public function testRedirectWhenNoSlash()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/'));
+
+ $matcher = $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($coll, new RequestContext()));
+ $matcher->expects($this->once())->method('redirect');
+ $matcher->match('/foo');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+ */
+ public function testRedirectWhenNoSlashForNonSafeMethod()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/'));
+
+ $context = new RequestContext();
+ $context->setMethod('POST');
+ $matcher = $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($coll, $context));
+ $matcher->match('/foo');
+ }
+
+ public function testSchemeRedirect()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https')));
+
+ $matcher = $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($coll, new RequestContext()));
+ $matcher
+ ->expects($this->once())
+ ->method('redirect')
+ ->with('/foo', 'foo', 'https')
+ ->will($this->returnValue(array('_route' => 'foo')))
+ ;
+ $matcher->match('/foo');
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher;
+
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\Matcher\TraceableUrlMatcher;
+
+class TraceableUrlMatcherTest extends \PHPUnit_Framework_TestCase
+{
+ public function test()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo', array(), array('_method' => 'POST')));
+ $coll->add('bar', new Route('/bar/{id}', array(), array('id' => '\d+')));
+ $coll->add('bar1', new Route('/bar/{name}', array(), array('id' => '\w+', '_method' => 'POST')));
+ $coll->add('bar2', new Route('/foo', array(), array(), array(), 'baz'));
+ $coll->add('bar3', new Route('/foo1', array(), array(), array(), 'baz'));
+
+ $context = new RequestContext();
+ $context->setHost('baz');
+
+ $matcher = new TraceableUrlMatcher($coll, $context);
+ $traces = $matcher->getTraces('/babar');
+ $this->assertEquals(array(0, 0, 0, 0, 0), $this->getLevels($traces));
+
+ $traces = $matcher->getTraces('/foo');
+ $this->assertEquals(array(1, 0, 0, 2), $this->getLevels($traces));
+
+ $traces = $matcher->getTraces('/bar/12');
+ $this->assertEquals(array(0, 2), $this->getLevels($traces));
+
+ $traces = $matcher->getTraces('/bar/dd');
+ $this->assertEquals(array(0, 1, 1, 0, 0), $this->getLevels($traces));
+
+ $traces = $matcher->getTraces('/foo1');
+ $this->assertEquals(array(0, 0, 0, 0, 2), $this->getLevels($traces));
+
+ $context->setMethod('POST');
+ $traces = $matcher->getTraces('/foo');
+ $this->assertEquals(array(2), $this->getLevels($traces));
+
+ $traces = $matcher->getTraces('/bar/dd');
+ $this->assertEquals(array(0, 1, 2), $this->getLevels($traces));
+ }
+
+ public function getLevels($traces)
+ {
+ $levels = array();
+ foreach ($traces as $trace) {
+ $levels[] = $trace['level'];
+ }
+
+ return $levels;
+ }
+}
--- /dev/null
+<?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\Routing\Tests\Matcher;
+
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Matcher\UrlMatcher;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\RequestContext;
+
+class UrlMatcherTest extends \PHPUnit_Framework_TestCase
+{
+ public function testNoMethodSoAllowed()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $matcher->match('/foo');
+ }
+
+ public function testMethodNotAllowed()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo', array(), array('_method' => 'post')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ try {
+ $matcher->match('/foo');
+ $this->fail();
+ } catch (MethodNotAllowedException $e) {
+ $this->assertEquals(array('POST'), $e->getAllowedMethods());
+ }
+ }
+
+ public function testHeadAllowedWhenRequirementContainsGet()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo', array(), array('_method' => 'get')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext('', 'head'));
+ $matcher->match('/foo');
+ }
+
+ public function testMethodNotAllowedAggregatesAllowedMethods()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo1', new Route('/foo', array(), array('_method' => 'post')));
+ $coll->add('foo2', new Route('/foo', array(), array('_method' => 'put|delete')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ try {
+ $matcher->match('/foo');
+ $this->fail();
+ } catch (MethodNotAllowedException $e) {
+ $this->assertEquals(array('POST', 'PUT', 'DELETE'), $e->getAllowedMethods());
+ }
+ }
+
+ public function testMatch()
+ {
+ // test the patterns are matched and parameters are returned
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo/{bar}'));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ try {
+ $matcher->match('/no-match');
+ $this->fail();
+ } catch (ResourceNotFoundException $e) {}
+ $this->assertEquals(array('_route' => 'foo', 'bar' => 'baz'), $matcher->match('/foo/baz'));
+
+ // test that defaults are merged
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo/{bar}', array('def' => 'test')));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'foo', 'bar' => 'baz', 'def' => 'test'), $matcher->match('/foo/baz'));
+
+ // test that route "method" is ignored if no method is given in the context
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo', array(), array('_method' => 'GET|head')));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertInternalType('array', $matcher->match('/foo'));
+
+ // route does not match with POST method context
+ $matcher = new UrlMatcher($collection, new RequestContext('', 'post'));
+ try {
+ $matcher->match('/foo');
+ $this->fail();
+ } catch (MethodNotAllowedException $e) {}
+
+ // route does match with GET or HEAD method context
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertInternalType('array', $matcher->match('/foo'));
+ $matcher = new UrlMatcher($collection, new RequestContext('', 'head'));
+ $this->assertInternalType('array', $matcher->match('/foo'));
+
+ // route with an optional variable as the first segment
+ $collection = new RouteCollection();
+ $collection->add('bar', new Route('/{bar}/foo', array('bar' => 'bar'), array('bar' => 'foo|bar')));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'bar', 'bar' => 'bar'), $matcher->match('/bar/foo'));
+ $this->assertEquals(array('_route' => 'bar', 'bar' => 'foo'), $matcher->match('/foo/foo'));
+
+ $collection = new RouteCollection();
+ $collection->add('bar', new Route('/{bar}', array('bar' => 'bar'), array('bar' => 'foo|bar')));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'bar', 'bar' => 'foo'), $matcher->match('/foo'));
+ $this->assertEquals(array('_route' => 'bar', 'bar' => 'bar'), $matcher->match('/'));
+
+ // route with only optional variables
+ $collection = new RouteCollection();
+ $collection->add('bar', new Route('/{foo}/{bar}', array('foo' => 'foo', 'bar' => 'bar'), array()));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'bar', 'foo' => 'foo', 'bar' => 'bar'), $matcher->match('/'));
+ $this->assertEquals(array('_route' => 'bar', 'foo' => 'a', 'bar' => 'bar'), $matcher->match('/a'));
+ $this->assertEquals(array('_route' => 'bar', 'foo' => 'a', 'bar' => 'b'), $matcher->match('/a/b'));
+ }
+
+ public function testMatchWithPrefixes()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/{foo}'));
+ $collection->addPrefix('/b');
+ $collection->addPrefix('/a');
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'foo', 'foo' => 'foo'), $matcher->match('/a/b/foo'));
+ }
+
+ public function testMatchWithDynamicPrefix()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/{foo}'));
+ $collection->addPrefix('/b');
+ $collection->addPrefix('/{_locale}');
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_locale' => 'fr', '_route' => 'foo', 'foo' => 'foo'), $matcher->match('/fr/b/foo'));
+ }
+
+ public function testMatchSpecialRouteName()
+ {
+ $collection = new RouteCollection();
+ $collection->add('$péß^a|', new Route('/bar'));
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => '$péß^a|'), $matcher->match('/bar'));
+ }
+
+ public function testMatchNonAlpha()
+ {
+ $collection = new RouteCollection();
+ $chars = '!"$%éà &\'()*+,./:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\[]^_`abcdefghijklmnopqrstuvwxyz{|}~-';
+ $collection->add('foo', new Route('/{foo}/bar', array(), array('foo' => '['.preg_quote($chars).']+')));
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'foo', 'foo' => $chars), $matcher->match('/'.rawurlencode($chars).'/bar'));
+ $this->assertEquals(array('_route' => 'foo', 'foo' => $chars), $matcher->match('/'.strtr($chars, array('%' => '%25')).'/bar'));
+ }
+
+ public function testMatchWithDotMetacharacterInRequirements()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/{foo}/bar', array(), array('foo' => '.+')));
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ $this->assertEquals(array('_route' => 'foo', 'foo' => "\n"), $matcher->match('/'.urlencode("\n").'/bar'), 'linefeed character is matched');
+ }
+
+ public function testMatchOverriddenRoute()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('foo', new Route('/foo1'));
+
+ $collection->addCollection($collection1);
+
+ $matcher = new UrlMatcher($collection, new RequestContext());
+
+ $this->assertEquals(array('_route' => 'foo'), $matcher->match('/foo1'));
+ $this->setExpectedException('Symfony\Component\Routing\Exception\ResourceNotFoundException');
+ $this->assertEquals(array(), $matcher->match('/foo'));
+ }
+
+ public function testMatchRegression()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/{foo}'));
+ $coll->add('bar', new Route('/foo/bar/{foo}'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $this->assertEquals(array('foo' => 'bar', '_route' => 'bar'), $matcher->match('/foo/bar/bar'));
+
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/{bar}'));
+ $matcher = new UrlMatcher($collection, new RequestContext());
+ try {
+ $matcher->match('/');
+ $this->fail();
+ } catch (ResourceNotFoundException $e) {
+ }
+ }
+
+ public function testDefaultRequirementForOptionalVariables()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{page}.{_format}', array('page' => 'index', '_format' => 'html')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $this->assertEquals(array('page' => 'my-page', '_format' => 'xml', '_route' => 'test'), $matcher->match('/my-page.xml'));
+ }
+
+ public function testMatchingIsEager()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{foo}-{bar}-', array(), array('foo' => '.+', 'bar' => '.+')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $this->assertEquals(array('foo' => 'text1-text2-text3', 'bar' => 'text4', '_route' => 'test'), $matcher->match('/text1-text2-text3-text4-'));
+ }
+
+ public function testAdjacentVariables()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => 'y|Y')));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ // 'w' eagerly matches as much as possible and the other variables match the remaining chars.
+ // This also shows that the variables w-z must all exclude the separating char (the dot '.' in this case) by default requirement.
+ // Otherwise they would also consume '.xml' and _format would never match as it's an optional variable.
+ $this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'Y', 'z' => 'Z','_format' => 'xml', '_route' => 'test'), $matcher->match('/wwwwwxYZ.xml'));
+ // As 'y' has custom requirement and can only be of value 'y|Y', it will leave 'ZZZ' to variable z.
+ // So with carefully chosen requirements adjacent variables, can be useful.
+ $this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'y', 'z' => 'ZZZ','_format' => 'html', '_route' => 'test'), $matcher->match('/wwwwwxyZZZ'));
+ // z and _format are optional.
+ $this->assertEquals(array('w' => 'wwwww', 'x' => 'x', 'y' => 'y', 'z' => 'default-z','_format' => 'html', '_route' => 'test'), $matcher->match('/wwwwwxy'));
+
+ $this->setExpectedException('Symfony\Component\Routing\Exception\ResourceNotFoundException');
+ $matcher->match('/wxy.html');
+ }
+
+ public function testOptionalVariableWithNoRealSeparator()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/get{what}', array('what' => 'All')));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ $this->assertEquals(array('what' => 'All', '_route' => 'test'), $matcher->match('/get'));
+ $this->assertEquals(array('what' => 'Sites', '_route' => 'test'), $matcher->match('/getSites'));
+
+ // Usually the character in front of an optional parameter can be left out, e.g. with pattern '/get/{what}' just '/get' would match.
+ // But here the 't' in 'get' is not a separating character, so it makes no sense to match without it.
+ $this->setExpectedException('Symfony\Component\Routing\Exception\ResourceNotFoundException');
+ $matcher->match('/ge');
+ }
+
+ public function testRequiredVariableWithNoRealSeparator()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/get{what}Suffix'));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ $this->assertEquals(array('what' => 'Sites', '_route' => 'test'), $matcher->match('/getSitesSuffix'));
+ }
+
+ public function testDefaultRequirementOfVariable()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{page}.{_format}'));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ $this->assertEquals(array('page' => 'index', '_format' => 'mobile.html', '_route' => 'test'), $matcher->match('/index.mobile.html'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+ */
+ public function testDefaultRequirementOfVariableDisallowsSlash()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{page}.{_format}'));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ $matcher->match('/index.sl/ash');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+ */
+ public function testDefaultRequirementOfVariableDisallowsNextSeparator()
+ {
+ $coll = new RouteCollection();
+ $coll->add('test', new Route('/{page}.{_format}', array(), array('_format' => 'html|xml')));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+
+ $matcher->match('/do.t.html');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+ */
+ public function testSchemeRequirement()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo', array(), array('_scheme' => 'https')));
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $matcher->match('/foo');
+ }
+
+ public function testDecodeOnce()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/{foo}'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $this->assertEquals(array('foo' => 'bar%23', '_route' => 'foo'), $matcher->match('/foo/bar%2523'));
+ }
+
+ public function testCannotRelyOnPrefix()
+ {
+ $coll = new RouteCollection();
+
+ $subColl = new RouteCollection();
+ $subColl->add('bar', new Route('/bar'));
+ $subColl->addPrefix('/prefix');
+ // overwrite the pattern, so the prefix is not valid anymore for this route in the collection
+ $subColl->get('bar')->setPattern('/new');
+
+ $coll->addCollection($subColl);
+
+ $matcher = new UrlMatcher($coll, new RequestContext());
+ $this->assertEquals(array('_route' => 'bar'), $matcher->match('/new'));
+ }
+
+ public function testWithHost()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/{foo}', array(), array(), array(), '{locale}.example.com'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com'));
+ $this->assertEquals(array('foo' => 'bar', '_route' => 'foo', 'locale' => 'en'), $matcher->match('/foo/bar'));
+ }
+
+ public function testWithHostOnRouteCollection()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/{foo}'));
+ $coll->add('bar', new Route('/bar/{foo}', array(), array(), array(), '{locale}.example.net'));
+ $coll->setHost('{locale}.example.com');
+
+ $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com'));
+ $this->assertEquals(array('foo' => 'bar', '_route' => 'foo', 'locale' => 'en'), $matcher->match('/foo/bar'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'en.example.com'));
+ $this->assertEquals(array('foo' => 'bar', '_route' => 'bar', 'locale' => 'en'), $matcher->match('/bar/bar'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
+ */
+ public function testWithOutHostHostDoesNotMatch()
+ {
+ $coll = new RouteCollection();
+ $coll->add('foo', new Route('/foo/{foo}', array(), array(), array(), '{locale}.example.com'));
+
+ $matcher = new UrlMatcher($coll, new RequestContext('', 'GET', 'example.com'));
+ $matcher->match('/foo/bar');
+ }
+}
--- /dev/null
+<?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\Routing\Tests;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+
+class RouteCollectionTest extends \PHPUnit_Framework_TestCase
+{
+ public function testRoute()
+ {
+ $collection = new RouteCollection();
+ $route = new Route('/foo');
+ $collection->add('foo', $route);
+ $this->assertEquals(array('foo' => $route), $collection->all(), '->add() adds a route');
+ $this->assertEquals($route, $collection->get('foo'), '->get() returns a route by name');
+ $this->assertNull($collection->get('bar'), '->get() returns null if a route does not exist');
+ }
+
+ public function testOverriddenRoute()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+ $collection->add('foo', new Route('/foo1'));
+
+ $this->assertEquals('/foo1', $collection->get('foo')->getPath());
+ }
+
+ public function testDeepOverriddenRoute()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('foo', new Route('/foo1'));
+
+ $collection2 = new RouteCollection();
+ $collection2->add('foo', new Route('/foo2'));
+
+ $collection1->addCollection($collection2);
+ $collection->addCollection($collection1);
+
+ $this->assertEquals('/foo2', $collection1->get('foo')->getPath());
+ $this->assertEquals('/foo2', $collection->get('foo')->getPath());
+ }
+
+ public function testIterator()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('bar', $bar = new Route('/bar'));
+ $collection1->add('foo', $foo = new Route('/foo-new'));
+ $collection->addCollection($collection1);
+ $collection->add('last', $last = new Route('/last'));
+
+ $this->assertInstanceOf('\ArrayIterator', $collection->getIterator());
+ $this->assertSame(array('bar' => $bar, 'foo' => $foo, 'last' => $last), $collection->getIterator()->getArrayCopy());
+ }
+
+ public function testCount()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('bar', new Route('/bar'));
+ $collection->addCollection($collection1);
+
+ $this->assertCount(2, $collection);
+ }
+
+ public function testAddCollection()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('bar', $bar = new Route('/bar'));
+ $collection1->add('foo', $foo = new Route('/foo-new'));
+
+ $collection2 = new RouteCollection();
+ $collection2->add('grandchild', $grandchild = new Route('/grandchild'));
+
+ $collection1->addCollection($collection2);
+ $collection->addCollection($collection1);
+ $collection->add('last', $last = new Route('/last'));
+
+ $this->assertSame(array('bar' => $bar, 'foo' => $foo, 'grandchild' => $grandchild, 'last' => $last), $collection->all(),
+ '->addCollection() imports routes of another collection, overrides if necessary and adds them at the end');
+ }
+
+ public function testAddCollectionWithResources()
+ {
+ if (!class_exists('Symfony\Component\Config\Resource\FileResource')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml'));
+ $collection1 = new RouteCollection();
+ $collection1->addResource($foo1 = new FileResource(__DIR__.'/Fixtures/foo1.xml'));
+ $collection->addCollection($collection1);
+ $this->assertEquals(array($foo, $foo1), $collection->getResources(), '->addCollection() merges resources');
+ }
+
+ public function testAddDefaultsAndRequirementsAndOptions()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', new Route('/{placeholder}'));
+ $collection1 = new RouteCollection();
+ $collection1->add('bar', new Route('/{placeholder}',
+ array('_controller' => 'fixed', 'placeholder' => 'default'), array('placeholder' => '.+'), array('option' => 'value'))
+ );
+ $collection->addCollection($collection1);
+
+ $collection->addDefaults(array('placeholder' => 'new-default'));
+ $this->assertEquals(array('placeholder' => 'new-default'), $collection->get('foo')->getDefaults(), '->addDefaults() adds defaults to all routes');
+ $this->assertEquals(array('_controller' => 'fixed', 'placeholder' => 'new-default'), $collection->get('bar')->getDefaults(),
+ '->addDefaults() adds defaults to all routes and overwrites existing ones');
+
+ $collection->addRequirements(array('placeholder' => '\d+'));
+ $this->assertEquals(array('placeholder' => '\d+'), $collection->get('foo')->getRequirements(), '->addRequirements() adds requirements to all routes');
+ $this->assertEquals(array('placeholder' => '\d+'), $collection->get('bar')->getRequirements(),
+ '->addRequirements() adds requirements to all routes and overwrites existing ones');
+
+ $collection->addOptions(array('option' => 'new-value'));
+ $this->assertEquals(
+ array('option' => 'new-value', 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler'),
+ $collection->get('bar')->getOptions(), '->addOptions() adds options to all routes and overwrites existing ones'
+ );
+ }
+
+ public function testAddPrefix()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', $foo = new Route('/foo'));
+ $collection2 = new RouteCollection();
+ $collection2->add('bar', $bar = new Route('/bar'));
+ $collection->addCollection($collection2);
+ $collection->addPrefix(' / ');
+ $this->assertSame('/foo', $collection->get('foo')->getPattern(), '->addPrefix() trims the prefix and a single slash has no effect');
+ $collection->addPrefix('/{admin}', array('admin' => 'admin'), array('admin' => '\d+'));
+ $this->assertEquals('/{admin}/foo', $collection->get('foo')->getPath(), '->addPrefix() adds a prefix to all routes');
+ $this->assertEquals('/{admin}/bar', $collection->get('bar')->getPath(), '->addPrefix() adds a prefix to all routes');
+ $this->assertEquals(array('admin' => 'admin'), $collection->get('foo')->getDefaults(), '->addPrefix() adds defaults to all routes');
+ $this->assertEquals(array('admin' => 'admin'), $collection->get('bar')->getDefaults(), '->addPrefix() adds defaults to all routes');
+ $this->assertEquals(array('admin' => '\d+'), $collection->get('foo')->getRequirements(), '->addPrefix() adds requirements to all routes');
+ $this->assertEquals(array('admin' => '\d+'), $collection->get('bar')->getRequirements(), '->addPrefix() adds requirements to all routes');
+ $collection->addPrefix('0');
+ $this->assertEquals('/0/{admin}/foo', $collection->get('foo')->getPattern(), '->addPrefix() ensures a prefix must start with a slash and must not end with a slash');
+ $collection->addPrefix('/ /');
+ $this->assertSame('/ /0/{admin}/foo', $collection->get('foo')->getPath(), '->addPrefix() can handle spaces if desired');
+ $this->assertSame('/ /0/{admin}/bar', $collection->get('bar')->getPath(), 'the route pattern of an added collection is in synch with the added prefix');
+ }
+
+ public function testAddPrefixOverridesDefaultsAndRequirements()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', $foo = new Route('/foo'));
+ $collection->add('bar', $bar = new Route('/bar', array(), array('_scheme' => 'http')));
+ $collection->addPrefix('/admin', array(), array('_scheme' => 'https'));
+
+ $this->assertEquals('https', $collection->get('foo')->getRequirement('_scheme'), '->addPrefix() overrides existing requirements');
+ $this->assertEquals('https', $collection->get('bar')->getRequirement('_scheme'), '->addPrefix() overrides existing requirements');
+ }
+
+ public function testResource()
+ {
+ if (!class_exists('Symfony\Component\Config\Resource\FileResource')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ $collection = new RouteCollection();
+ $collection->addResource($foo = new FileResource(__DIR__.'/Fixtures/foo.xml'));
+ $collection->addResource($bar = new FileResource(__DIR__.'/Fixtures/bar.xml'));
+ $collection->addResource(new FileResource(__DIR__.'/Fixtures/foo.xml'));
+
+ $this->assertEquals(array($foo, $bar), $collection->getResources(),
+ '->addResource() adds a resource and getResources() only returns unique ones by comparing the string representation');
+ }
+
+ public function testUniqueRouteWithGivenName()
+ {
+ $collection1 = new RouteCollection();
+ $collection1->add('foo', new Route('/old'));
+ $collection2 = new RouteCollection();
+ $collection3 = new RouteCollection();
+ $collection3->add('foo', $new = new Route('/new'));
+
+ $collection2->addCollection($collection3);
+ $collection1->addCollection($collection2);
+
+ $this->assertSame($new, $collection1->get('foo'), '->get() returns new route that overrode previous one');
+ // size of 1 because collection1 contains /new but not /old anymore
+ $this->assertCount(1, $collection1->getIterator(), '->addCollection() removes previous routes when adding new routes with the same name');
+ }
+
+ public function testGet()
+ {
+ $collection1 = new RouteCollection();
+ $collection1->add('a', $a = new Route('/a'));
+ $collection2 = new RouteCollection();
+ $collection2->add('b', $b = new Route('/b'));
+ $collection1->addCollection($collection2);
+ $collection1->add('$péß^a|', $c = new Route('/special'));
+
+ $this->assertSame($b, $collection1->get('b'), '->get() returns correct route in child collection');
+ $this->assertSame($c, $collection1->get('$péß^a|'), '->get() can handle special characters');
+ $this->assertNull($collection2->get('a'), '->get() does not return the route defined in parent collection');
+ $this->assertNull($collection1->get('non-existent'), '->get() returns null when route does not exist');
+ $this->assertNull($collection1->get(0), '->get() does not disclose internal child RouteCollection');
+ }
+
+ public function testRemove()
+ {
+ $collection = new RouteCollection();
+ $collection->add('foo', $foo = new Route('/foo'));
+
+ $collection1 = new RouteCollection();
+ $collection1->add('bar', $bar = new Route('/bar'));
+ $collection->addCollection($collection1);
+ $collection->add('last', $last = new Route('/last'));
+
+ $collection->remove('foo');
+ $this->assertSame(array('bar' => $bar, 'last' => $last), $collection->all(), '->remove() can remove a single route');
+ $collection->remove(array('bar', 'last'));
+ $this->assertSame(array(), $collection->all(), '->remove() accepts an array and can remove multiple routes at once');
+ }
+
+ public function testSetHost()
+ {
+ $collection = new RouteCollection();
+ $routea = new Route('/a');
+ $routeb = new Route('/b', array(), array(), array(), '{locale}.example.net');
+ $collection->add('a', $routea);
+ $collection->add('b', $routeb);
+
+ $collection->setHost('{locale}.example.com');
+
+ $this->assertEquals('{locale}.example.com', $routea->getHost());
+ $this->assertEquals('{locale}.example.com', $routeb->getHost());
+ }
+}
--- /dev/null
+<?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\Routing\Tests;
+
+use Symfony\Component\Routing\Route;
+
+class RouteCompilerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider provideCompileData
+ */
+ public function testCompile($name, $arguments, $prefix, $regex, $variables, $tokens)
+ {
+ $r = new \ReflectionClass('Symfony\\Component\\Routing\\Route');
+ $route = $r->newInstanceArgs($arguments);
+
+ $compiled = $route->compile();
+ $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)');
+ $this->assertEquals($regex, $compiled->getRegex(), $name.' (regex)');
+ $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)');
+ $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)');
+ }
+
+ public function provideCompileData()
+ {
+ return array(
+ array(
+ 'Static route',
+ array('/foo'),
+ '/foo', '#^/foo$#s', array(), array(
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with a variable',
+ array('/foo/{bar}'),
+ '/foo', '#^/foo/(?P<bar>[^/]++)$#s', array('bar'), array(
+ array('variable', '/', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with a variable that has a default value',
+ array('/foo/{bar}', array('bar' => 'bar')),
+ '/foo', '#^/foo(?:/(?P<bar>[^/]++))?$#s', array('bar'), array(
+ array('variable', '/', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with several variables',
+ array('/foo/{bar}/{foobar}'),
+ '/foo', '#^/foo/(?P<bar>[^/]++)/(?P<foobar>[^/]++)$#s', array('bar', 'foobar'), array(
+ array('variable', '/', '[^/]++', 'foobar'),
+ array('variable', '/', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with several variables that have default values',
+ array('/foo/{bar}/{foobar}', array('bar' => 'bar', 'foobar' => '')),
+ '/foo', '#^/foo(?:/(?P<bar>[^/]++)(?:/(?P<foobar>[^/]++))?)?$#s', array('bar', 'foobar'), array(
+ array('variable', '/', '[^/]++', 'foobar'),
+ array('variable', '/', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with several variables but some of them have no default values',
+ array('/foo/{bar}/{foobar}', array('bar' => 'bar')),
+ '/foo', '#^/foo/(?P<bar>[^/]++)/(?P<foobar>[^/]++)$#s', array('bar', 'foobar'), array(
+ array('variable', '/', '[^/]++', 'foobar'),
+ array('variable', '/', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with an optional variable as the first segment',
+ array('/{bar}', array('bar' => 'bar')),
+ '', '#^/(?P<bar>[^/]++)?$#s', array('bar'), array(
+ array('variable', '/', '[^/]++', 'bar'),
+ )),
+
+ array(
+ 'Route with a requirement of 0',
+ array('/{bar}', array('bar' => null), array('bar' => '0')),
+ '', '#^/(?P<bar>0)?$#s', array('bar'), array(
+ array('variable', '/', '0', 'bar'),
+ )),
+
+ array(
+ 'Route with an optional variable as the first segment with requirements',
+ array('/{bar}', array('bar' => 'bar'), array('bar' => '(foo|bar)')),
+ '', '#^/(?P<bar>(foo|bar))?$#s', array('bar'), array(
+ array('variable', '/', '(foo|bar)', 'bar'),
+ )),
+
+ array(
+ 'Route with only optional variables',
+ array('/{foo}/{bar}', array('foo' => 'foo', 'bar' => 'bar')),
+ '', '#^/(?P<foo>[^/]++)?(?:/(?P<bar>[^/]++))?$#s', array('foo', 'bar'), array(
+ array('variable', '/', '[^/]++', 'bar'),
+ array('variable', '/', '[^/]++', 'foo'),
+ )),
+
+ array(
+ 'Route with a variable in last position',
+ array('/foo-{bar}'),
+ '/foo', '#^/foo\-(?P<bar>[^/]++)$#s', array('bar'), array(
+ array('variable', '-', '[^/]++', 'bar'),
+ array('text', '/foo'),
+ )),
+
+ array(
+ 'Route with nested placeholders',
+ array('/{static{var}static}'),
+ '/{static', '#^/\{static(?P<var>[^/]+)static\}$#s', array('var'), array(
+ array('text', 'static}'),
+ array('variable', '', '[^/]+', 'var'),
+ array('text', '/{static'),
+ )),
+
+ array(
+ 'Route without separator between variables',
+ array('/{w}{x}{y}{z}.{_format}', array('z' => 'default-z', '_format' => 'html'), array('y' => '(y|Y)')),
+ '', '#^/(?P<w>[^/\.]+)(?P<x>[^/\.]+)(?P<y>(y|Y))(?:(?P<z>[^/\.]++)(?:\.(?P<_format>[^/]++))?)?$#s', array('w', 'x', 'y', 'z', '_format'), array(
+ array('variable', '.', '[^/]++', '_format'),
+ array('variable', '', '[^/\.]++', 'z'),
+ array('variable', '', '(y|Y)', 'y'),
+ array('variable', '', '[^/\.]+', 'x'),
+ array('variable', '/', '[^/\.]+', 'w'),
+ )),
+
+ array(
+ 'Route with a format',
+ array('/foo/{bar}.{_format}'),
+ '/foo', '#^/foo/(?P<bar>[^/\.]++)\.(?P<_format>[^/]++)$#s', array('bar', '_format'), array(
+ array('variable', '.', '[^/]++', '_format'),
+ array('variable', '/', '[^/\.]++', 'bar'),
+ array('text', '/foo'),
+ )),
+ );
+ }
+
+ /**
+ * @expectedException \LogicException
+ */
+ public function testRouteWithSameVariableTwice()
+ {
+ $route = new Route('/{name}/{name}');
+
+ $compiled = $route->compile();
+ }
+
+ /**
+ * @dataProvider getNumericVariableNames
+ * @expectedException \DomainException
+ */
+ public function testRouteWithNumericVariableName($name)
+ {
+ $route = new Route('/{'. $name.'}');
+ $route->compile();
+ }
+
+ public function getNumericVariableNames()
+ {
+ return array(
+ array('09'),
+ array('123'),
+ array('1e2')
+ );
+ }
+
+ /**
+ * @dataProvider provideCompileWithHostData
+ */
+ public function testCompileWithHost($name, $arguments, $prefix, $regex, $variables, $pathVariables, $tokens, $hostRegex, $hostVariables, $hostTokens)
+ {
+ $r = new \ReflectionClass('Symfony\\Component\\Routing\\Route');
+ $route = $r->newInstanceArgs($arguments);
+
+ $compiled = $route->compile();
+ $this->assertEquals($prefix, $compiled->getStaticPrefix(), $name.' (static prefix)');
+ $this->assertEquals($regex, str_replace(array("\n", ' '), '', $compiled->getRegex()), $name.' (regex)');
+ $this->assertEquals($variables, $compiled->getVariables(), $name.' (variables)');
+ $this->assertEquals($pathVariables, $compiled->getPathVariables(), $name.' (path variables)');
+ $this->assertEquals($tokens, $compiled->getTokens(), $name.' (tokens)');
+ $this->assertEquals($hostRegex, str_replace(array("\n", ' '), '', $compiled->getHostRegex()), $name.' (host regex)');
+ $this->assertEquals($hostVariables, $compiled->getHostVariables(), $name.' (host variables)');
+ $this->assertEquals($hostTokens, $compiled->getHostTokens(), $name.' (host tokens)');
+ }
+
+ public function provideCompileWithHostData()
+ {
+ return array(
+ array(
+ 'Route with host pattern',
+ array('/hello', array(), array(), array(), 'www.example.com'),
+ '/hello', '#^/hello$#s', array(), array(), array(
+ array('text', '/hello'),
+ ),
+ '#^www\.example\.com$#s', array(), array(
+ array('text', 'www.example.com'),
+ ),
+ ),
+ array(
+ 'Route with host pattern and some variables',
+ array('/hello/{name}', array(), array(), array(), 'www.example.{tld}'),
+ '/hello', '#^/hello/(?P<name>[^/]++)$#s', array('tld', 'name'), array('name'), array(
+ array('variable', '/', '[^/]++', 'name'),
+ array('text', '/hello'),
+ ),
+ '#^www\.example\.(?P<tld>[^\.]++)$#s', array('tld'), array(
+ array('variable', '.', '[^\.]++', 'tld'),
+ array('text', 'www.example'),
+ ),
+ ),
+ array(
+ 'Route with variable at beginning of host',
+ array('/hello', array(), array(), array(), '{locale}.example.{tld}'),
+ '/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
+ array('text', '/hello'),
+ ),
+ '#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#s', array('locale', 'tld'), array(
+ array('variable', '.', '[^\.]++', 'tld'),
+ array('text', '.example'),
+ array('variable', '', '[^\.]++', 'locale'),
+ ),
+ ),
+ array(
+ 'Route with host variables that has a default value',
+ array('/hello', array('locale' => 'a', 'tld' => 'b'), array(), array(), '{locale}.example.{tld}'),
+ '/hello', '#^/hello$#s', array('locale', 'tld'), array(), array(
+ array('text', '/hello'),
+ ),
+ '#^(?P<locale>[^\.]++)\.example\.(?P<tld>[^\.]++)$#s', array('locale', 'tld'), array(
+ array('variable', '.', '[^\.]++', 'tld'),
+ array('text', '.example'),
+ array('variable', '', '[^\.]++', 'locale'),
+ ),
+ ),
+ );
+ }
+}
--- /dev/null
+<?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\Routing\Tests;
+
+use Symfony\Component\Routing\Route;
+
+class RouteTest extends \PHPUnit_Framework_TestCase
+{
+ public function testConstructor()
+ {
+ $route = new Route('/{foo}', array('foo' => 'bar'), array('foo' => '\d+'), array('foo' => 'bar'), '{locale}.example.com');
+ $this->assertEquals('/{foo}', $route->getPath(), '__construct() takes a path as its first argument');
+ $this->assertEquals(array('foo' => 'bar'), $route->getDefaults(), '__construct() takes defaults as its second argument');
+ $this->assertEquals(array('foo' => '\d+'), $route->getRequirements(), '__construct() takes requirements as its third argument');
+ $this->assertEquals('bar', $route->getOption('foo'), '__construct() takes options as its fourth argument');
+ $this->assertEquals('{locale}.example.com', $route->getHost(), '__construct() takes a host pattern as its fifth argument');
+
+ $route = new Route('/', array(), array(), array(), '', array('Https'), array('POST', 'put'));
+ $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes schemes as its sixth argument and lowercases it');
+ $this->assertEquals(array('POST', 'PUT'), $route->getMethods(), '__construct() takes methods as its seventh argument and uppercases it');
+
+ $route = new Route('/', array(), array(), array(), '', 'Https', 'Post');
+ $this->assertEquals(array('https'), $route->getSchemes(), '__construct() takes a single scheme as its sixth argument');
+ $this->assertEquals(array('POST'), $route->getMethods(), '__construct() takes a single method as its seventh argument');
+ }
+
+ public function testPath()
+ {
+ $route = new Route('/{foo}');
+ $route->setPath('/{bar}');
+ $this->assertEquals('/{bar}', $route->getPath(), '->setPath() sets the path');
+ $route->setPath('');
+ $this->assertEquals('/', $route->getPath(), '->setPath() adds a / at the beginning of the path if needed');
+ $route->setPath('bar');
+ $this->assertEquals('/bar', $route->getPath(), '->setPath() adds a / at the beginning of the path if needed');
+ $this->assertEquals($route, $route->setPath(''), '->setPath() implements a fluent interface');
+ $route->setPath('//path');
+ $this->assertEquals('/path', $route->getPath(), '->setPath() does not allow two slahes "//" at the beginning of the path as it would be confused with a network path when generating the path from the route');
+ }
+
+ public function testOptions()
+ {
+ $route = new Route('/{foo}');
+ $route->setOptions(array('foo' => 'bar'));
+ $this->assertEquals(array_merge(array(
+ 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler',
+ ), array('foo' => 'bar')), $route->getOptions(), '->setOptions() sets the options');
+ $this->assertEquals($route, $route->setOptions(array()), '->setOptions() implements a fluent interface');
+
+ $route->setOptions(array('foo' => 'foo'));
+ $route->addOptions(array('bar' => 'bar'));
+ $this->assertEquals($route, $route->addOptions(array()), '->addOptions() implements a fluent interface');
+ $this->assertEquals(array('foo' => 'foo', 'bar' => 'bar', 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler'), $route->getOptions(), '->addDefaults() keep previous defaults');
+ }
+
+ public function testDefaults()
+ {
+ $route = new Route('/{foo}');
+ $route->setDefaults(array('foo' => 'bar'));
+ $this->assertEquals(array('foo' => 'bar'), $route->getDefaults(), '->setDefaults() sets the defaults');
+ $this->assertEquals($route, $route->setDefaults(array()), '->setDefaults() implements a fluent interface');
+
+ $route->setDefault('foo', 'bar');
+ $this->assertEquals('bar', $route->getDefault('foo'), '->setDefault() sets a default value');
+
+ $route->setDefault('foo2', 'bar2');
+ $this->assertEquals('bar2', $route->getDefault('foo2'), '->getDefault() return the default value');
+ $this->assertNull($route->getDefault('not_defined'), '->getDefault() return null if default value is not setted');
+
+ $route->setDefault('_controller', $closure = function () { return 'Hello'; });
+ $this->assertEquals($closure, $route->getDefault('_controller'), '->setDefault() sets a default value');
+
+ $route->setDefaults(array('foo' => 'foo'));
+ $route->addDefaults(array('bar' => 'bar'));
+ $this->assertEquals($route, $route->addDefaults(array()), '->addDefaults() implements a fluent interface');
+ $this->assertEquals(array('foo' => 'foo', 'bar' => 'bar'), $route->getDefaults(), '->addDefaults() keep previous defaults');
+ }
+
+ public function testRequirements()
+ {
+ $route = new Route('/{foo}');
+ $route->setRequirements(array('foo' => '\d+'));
+ $this->assertEquals(array('foo' => '\d+'), $route->getRequirements(), '->setRequirements() sets the requirements');
+ $this->assertEquals('\d+', $route->getRequirement('foo'), '->getRequirement() returns a requirement');
+ $this->assertNull($route->getRequirement('bar'), '->getRequirement() returns null if a requirement is not defined');
+ $route->setRequirements(array('foo' => '^\d+$'));
+ $this->assertEquals('\d+', $route->getRequirement('foo'), '->getRequirement() removes ^ and $ from the path');
+ $this->assertEquals($route, $route->setRequirements(array()), '->setRequirements() implements a fluent interface');
+
+ $route->setRequirements(array('foo' => '\d+'));
+ $route->addRequirements(array('bar' => '\d+'));
+ $this->assertEquals($route, $route->addRequirements(array()), '->addRequirements() implements a fluent interface');
+ $this->assertEquals(array('foo' => '\d+', 'bar' => '\d+'), $route->getRequirements(), '->addRequirement() keep previous requirements');
+ }
+
+ public function testRequirement()
+ {
+ $route = new Route('/{foo}');
+ $route->setRequirement('foo', '^\d+$');
+ $this->assertEquals('\d+', $route->getRequirement('foo'), '->setRequirement() removes ^ and $ from the path');
+ }
+
+ /**
+ * @dataProvider getInvalidRequirements
+ * @expectedException \InvalidArgumentException
+ */
+ public function testSetInvalidRequirement($req)
+ {
+ $route = new Route('/{foo}');
+ $route->setRequirement('foo', $req);
+ }
+
+ public function getInvalidRequirements()
+ {
+ return array(
+ array(''),
+ array(array()),
+ array('^$'),
+ array('^'),
+ array('$')
+ );
+ }
+
+ public function testHost()
+ {
+ $route = new Route('/');
+ $route->setHost('{locale}.example.net');
+ $this->assertEquals('{locale}.example.net', $route->getHost(), '->setHost() sets the host pattern');
+ }
+
+ public function testScheme()
+ {
+ $route = new Route('/');
+ $this->assertEquals(array(), $route->getSchemes(), 'schemes is initialized with array()');
+ $route->setSchemes('hTTp');
+ $this->assertEquals(array('http'), $route->getSchemes(), '->setSchemes() accepts a single scheme string and lowercases it');
+ $route->setSchemes(array('HttpS', 'hTTp'));
+ $this->assertEquals(array('https', 'http'), $route->getSchemes(), '->setSchemes() accepts an array of schemes and lowercases them');
+ }
+
+ public function testSchemeIsBC()
+ {
+ $route = new Route('/');
+ $route->setRequirement('_scheme', 'http|https');
+ $this->assertEquals('http|https', $route->getRequirement('_scheme'));
+ $this->assertEquals(array('http', 'https'), $route->getSchemes());
+ $route->setSchemes(array('hTTp'));
+ $this->assertEquals('http', $route->getRequirement('_scheme'));
+ $route->setSchemes(array());
+ $this->assertNull($route->getRequirement('_scheme'));
+ }
+
+ public function testMethod()
+ {
+ $route = new Route('/');
+ $this->assertEquals(array(), $route->getMethods(), 'methods is initialized with array()');
+ $route->setMethods('gEt');
+ $this->assertEquals(array('GET'), $route->getMethods(), '->setMethods() accepts a single method string and uppercases it');
+ $route->setMethods(array('gEt', 'PosT'));
+ $this->assertEquals(array('GET', 'POST'), $route->getMethods(), '->setMethods() accepts an array of methods and uppercases them');
+ }
+
+ public function testMethodIsBC()
+ {
+ $route = new Route('/');
+ $route->setRequirement('_method', 'GET|POST');
+ $this->assertEquals('GET|POST', $route->getRequirement('_method'));
+ $this->assertEquals(array('GET', 'POST'), $route->getMethods());
+ $route->setMethods(array('gEt'));
+ $this->assertEquals('GET', $route->getRequirement('_method'));
+ $route->setMethods(array());
+ $this->assertNull($route->getRequirement('_method'));
+ }
+
+ public function testCompile()
+ {
+ $route = new Route('/{foo}');
+ $this->assertInstanceOf('Symfony\Component\Routing\CompiledRoute', $compiled = $route->compile(), '->compile() returns a compiled route');
+ $this->assertSame($compiled, $route->compile(), '->compile() only compiled the route once if unchanged');
+ $route->setRequirement('foo', '.*');
+ $this->assertNotSame($compiled, $route->compile(), '->compile() recompiles if the route was modified');
+ }
+}
--- /dev/null
+<?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\Routing\Tests;
+
+use Symfony\Component\Routing\Router;
+
+class RouterTest extends \PHPUnit_Framework_TestCase
+{
+ private $router = null;
+
+ private $loader = null;
+
+ protected function setUp()
+ {
+ $this->loader = $this->getMock('Symfony\Component\Config\Loader\LoaderInterface');
+ $this->router = new Router($this->loader, 'routing.yml');
+ }
+
+ public function testSetOptionsWithSupportedOptions()
+ {
+ $this->router->setOptions(array(
+ 'cache_dir' => './cache',
+ 'debug' => true,
+ 'resource_type' => 'ResourceType'
+ ));
+
+ $this->assertSame('./cache', $this->router->getOption('cache_dir'));
+ $this->assertTrue($this->router->getOption('debug'));
+ $this->assertSame('ResourceType', $this->router->getOption('resource_type'));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The Router does not support the following options: "option_foo", "option_bar"
+ */
+ public function testSetOptionsWithUnsupportedOptions()
+ {
+ $this->router->setOptions(array(
+ 'cache_dir' => './cache',
+ 'option_foo' => true,
+ 'option_bar' => 'baz',
+ 'resource_type' => 'ResourceType'
+ ));
+ }
+
+ public function testSetOptionWithSupportedOption()
+ {
+ $this->router->setOption('cache_dir', './cache');
+
+ $this->assertSame('./cache', $this->router->getOption('cache_dir'));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The Router does not support the "option_foo" option
+ */
+ public function testSetOptionWithUnsupportedOption()
+ {
+ $this->router->setOption('option_foo', true);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The Router does not support the "option_foo" option
+ */
+ public function testGetOptionWithUnsupportedOption()
+ {
+ $this->router->getOption('option_foo', true);
+ }
+
+ public function testThatRouteCollectionIsLoaded()
+ {
+ $this->router->setOption('resource_type', 'ResourceType');
+
+ $routeCollection = $this->getMock('Symfony\Component\Routing\RouteCollection');
+
+ $this->loader->expects($this->once())
+ ->method('load')->with('routing.yml', 'ResourceType')
+ ->will($this->returnValue($routeCollection));
+
+ $this->assertSame($routeCollection, $this->router->getRouteCollection());
+ }
+
+ /**
+ * @dataProvider provideMatcherOptionsPreventingCaching
+ */
+ public function testMatcherIsCreatedIfCacheIsNotConfigured($option)
+ {
+ $this->router->setOption($option, null);
+
+ $this->loader->expects($this->once())
+ ->method('load')->with('routing.yml', null)
+ ->will($this->returnValue($this->getMock('Symfony\Component\Routing\RouteCollection')));
+
+ $this->assertInstanceOf('Symfony\\Component\\Routing\\Matcher\\UrlMatcher', $this->router->getMatcher());
+
+ }
+
+ public function provideMatcherOptionsPreventingCaching()
+ {
+ return array(
+ array('cache_dir'),
+ array('matcher_cache_class')
+ );
+ }
+
+ /**
+ * @dataProvider provideGeneratorOptionsPreventingCaching
+ */
+ public function testGeneratorIsCreatedIfCacheIsNotConfigured($option)
+ {
+ $this->router->setOption($option, null);
+
+ $this->loader->expects($this->once())
+ ->method('load')->with('routing.yml', null)
+ ->will($this->returnValue($this->getMock('Symfony\Component\Routing\RouteCollection')));
+
+ $this->assertInstanceOf('Symfony\\Component\\Routing\\Generator\\UrlGenerator', $this->router->getGenerator());
+
+ }
+
+ public function provideGeneratorOptionsPreventingCaching()
+ {
+ return array(
+ array('cache_dir'),
+ array('generator_cache_class')
+ );
+ }
+}
--- /dev/null
+{
+ "name": "symfony/routing",
+ "type": "library",
+ "description": "Symfony Routing 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"
+ },
+ "require-dev": {
+ "symfony/config": "~2.2",
+ "symfony/yaml": "~2.0",
+ "doctrine/common": "~2.2",
+ "psr/log": "~1.0"
+ },
+ "suggest": {
+ "symfony/config": "",
+ "symfony/yaml": "",
+ "doctrine/common": ""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Routing\\": "" }
+ },
+ "target-dir": "Symfony/Component/Routing",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Routing Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./vendor</directory>
+ <directory>./Tests</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+CHANGELOG
+=========
+
+2.3.0
+-----
+
+ * added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues)
+ * added Translator::getFallbackLocales()
+ * deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method
+
+2.2.0
+-----
+
+ * QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3.
+ * [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now
+ throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found
+ and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid.
+ * changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException
+ (IcuDatFileLoader, IcuResFileLoader and QtFileLoader)
+
+2.1.0
+-----
+
+ * added support for more than one fallback locale
+ * added support for extracting translation messages from templates (Twig and PHP)
+ * added dumpers for translation catalogs
+ * added support for QT, gettext, and ResourceBundles
--- /dev/null
+<?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\Translation\Catalogue;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\MessageCatalogueInterface;
+
+/**
+ * Base catalogues binary operation class.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+abstract class AbstractOperation implements OperationInterface
+{
+ /**
+ * @var MessageCatalogueInterface
+ */
+ protected $source;
+
+ /**
+ * @var MessageCatalogueInterface
+ */
+ protected $target;
+
+ /**
+ * @var MessageCatalogue
+ */
+ protected $result;
+
+ /**
+ * @var null|array
+ */
+ private $domains;
+
+ /**
+ * @var array
+ */
+ protected $messages;
+
+ /**
+ * @param MessageCatalogueInterface $source
+ * @param MessageCatalogueInterface $target
+ *
+ * @throws \LogicException
+ */
+ public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
+ {
+ if ($source->getLocale() !== $target->getLocale()) {
+ throw new \LogicException('Operated catalogues must belong to the same locale.');
+ }
+
+ $this->source = $source;
+ $this->target = $target;
+ $this->result = new MessageCatalogue($source->getLocale());
+ $this->domains = null;
+ $this->messages = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDomains()
+ {
+ if (null === $this->domains) {
+ $this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains())));
+ }
+
+ return $this->domains;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMessages($domain)
+ {
+ if (!in_array($domain, $this->getDomains())) {
+ throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
+ }
+
+ if (!isset($this->messages[$domain]['all'])) {
+ $this->processDomain($domain);
+ }
+
+ return $this->messages[$domain]['all'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNewMessages($domain)
+ {
+ if (!in_array($domain, $this->getDomains())) {
+ throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
+ }
+
+ if (!isset($this->messages[$domain]['new'])) {
+ $this->processDomain($domain);
+ }
+
+ return $this->messages[$domain]['new'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getObsoleteMessages($domain)
+ {
+ if (!in_array($domain, $this->getDomains())) {
+ throw new \InvalidArgumentException(sprintf('Invalid domain: %s.', $domain));
+ }
+
+ if (!isset($this->messages[$domain]['obsolete'])) {
+ $this->processDomain($domain);
+ }
+
+ return $this->messages[$domain]['obsolete'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResult()
+ {
+ foreach ($this->getDomains() as $domain) {
+ if (!isset($this->messages[$domain])) {
+ $this->processDomain($domain);
+ }
+ }
+
+ return $this->result;
+ }
+
+ /**
+ * @param string $domain
+ */
+ abstract protected function processDomain($domain);
+}
--- /dev/null
+<?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\Translation\Catalogue;
+
+/**
+ * Diff operation between two catalogues.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class DiffOperation extends AbstractOperation
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function processDomain($domain)
+ {
+ $this->messages[$domain] = array(
+ 'all' => array(),
+ 'new' => array(),
+ 'obsolete' => array(),
+ );
+
+ foreach ($this->source->all($domain) as $id => $message) {
+ if ($this->target->has($id, $domain)) {
+ $this->messages[$domain]['all'][$id] = $message;
+ $this->result->add(array($id => $message), $domain);
+ } else {
+ $this->messages[$domain]['obsolete'][$id] = $message;
+ }
+ }
+
+ foreach ($this->target->all($domain) as $id => $message) {
+ if (!$this->source->has($id, $domain)) {
+ $this->messages[$domain]['all'][$id] = $message;
+ $this->messages[$domain]['new'][$id] = $message;
+ $this->result->add(array($id => $message), $domain);
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Catalogue;
+
+/**
+ * Merge operation between two catalogues.
+ *
+ * @author Jean-François Simon <contact@jfsimon.fr>
+ */
+class MergeOperation extends AbstractOperation
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function processDomain($domain)
+ {
+ $this->messages[$domain] = array(
+ 'all' => array(),
+ 'new' => array(),
+ 'obsolete' => array(),
+ );
+
+ foreach ($this->source->all($domain) as $id => $message) {
+ $this->messages[$domain]['all'][$id] = $message;
+ $this->result->add(array($id => $message), $domain);
+ }
+
+ foreach ($this->target->all($domain) as $id => $message) {
+ if (!$this->source->has($id, $domain)) {
+ $this->messages[$domain]['all'][$id] = $message;
+ $this->messages[$domain]['new'][$id] = $message;
+ $this->result->add(array($id => $message), $domain);
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Catalogue;
+
+use Symfony\Component\Translation\MessageCatalogueInterface;
+
+/**
+ * Represents an operation on catalogue(s).
+ *
+ * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
+ */
+interface OperationInterface
+{
+ /**
+ * Returns domains affected by operation.
+ *
+ * @return array
+ */
+ public function getDomains();
+
+ /**
+ * Returns all valid messages after operation.
+ *
+ * @param string $domain
+ *
+ * @return array
+ */
+ public function getMessages($domain);
+
+ /**
+ * Returns new messages after operation.
+ *
+ * @param string $domain
+ *
+ * @return array
+ */
+ public function getNewMessages($domain);
+
+ /**
+ * Returns obsolete messages after operation.
+ *
+ * @param string $domain
+ *
+ * @return array
+ */
+ public function getObsoleteMessages($domain);
+
+ /**
+ * Returns resulting catalogue.
+ *
+ * @return MessageCatalogueInterface
+ */
+ public function getResult();
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * CsvFileDumper generates a csv formatted string representation of a message catalogue.
+ *
+ * @author Stealth35
+ */
+class CsvFileDumper extends FileDumper
+{
+ private $delimiter = ';';
+ private $enclosure = '"';
+
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain = 'messages')
+ {
+ $handle = fopen('php://memory', 'rb+');
+
+ foreach ($messages->all($domain) as $source => $target) {
+ fputcsv($handle, array($source, $target), $this->delimiter, $this->enclosure);
+ }
+
+ rewind($handle);
+ $output = stream_get_contents($handle);
+ fclose($handle);
+
+ return $output;
+ }
+
+ /**
+ * Sets the delimiter and escape character for CSV.
+ *
+ * @param string $delimiter delimiter character
+ * @param string $enclosure enclosure character
+ */
+ public function setCsvControl($delimiter = ';', $enclosure = '"')
+ {
+ $this->delimiter = $delimiter;
+ $this->enclosure = $enclosure;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'csv';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * DumperInterface is the interface implemented by all translation dumpers.
+ * There is no common option.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+interface DumperInterface
+{
+ /**
+ * Dumps the message catalogue.
+ *
+ * @param MessageCatalogue $messages The message catalogue
+ * @param array $options Options that are used by the dumper
+ */
+ public function dump(MessageCatalogue $messages, $options = array());
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s).
+ * Performs backup of already existing files.
+ *
+ * Options:
+ * - path (mandatory): the directory where the files should be saved
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+abstract class FileDumper implements DumperInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function dump(MessageCatalogue $messages, $options = array())
+ {
+ if (!array_key_exists('path', $options)) {
+ throw new \InvalidArgumentException('The file dumper need a path options.');
+ }
+
+ // save a file for each domain
+ foreach ($messages->getDomains() as $domain) {
+ $file = $domain.'.'.$messages->getLocale().'.'.$this->getExtension();
+ // backup
+ $fullpath = $options['path'].'/'.$file;
+ if (file_exists($fullpath)) {
+ copy($fullpath, $fullpath.'~');
+ }
+ // save file
+ file_put_contents($fullpath, $this->format($messages, $domain));
+ }
+ }
+
+ /**
+ * Transforms a domain of a message catalogue to its string representation.
+ *
+ * @param MessageCatalogue $messages
+ * @param string $domain
+ *
+ * @return string representation
+ */
+ abstract protected function format(MessageCatalogue $messages, $domain);
+
+ /**
+ * Gets the file extension of the dumper.
+ *
+ * @return string file extension
+ */
+ abstract protected function getExtension();
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue.
+ *
+ * @author Stealth35
+ */
+class IcuResFileDumper implements DumperInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function dump(MessageCatalogue $messages, $options = array())
+ {
+ if (!array_key_exists('path', $options)) {
+ throw new \InvalidArgumentException('The file dumper need a path options.');
+ }
+
+ // save a file for each domain
+ foreach ($messages->getDomains() as $domain) {
+ $file = $messages->getLocale().'.'.$this->getExtension();
+ $path = $options['path'].'/'.$domain.'/';
+
+ if (!file_exists($path)) {
+ mkdir($path);
+ }
+
+ // backup
+ if (file_exists($path.$file)) {
+ copy($path.$file, $path.$file.'~');
+ }
+
+ // save file
+ file_put_contents($path.$file, $this->format($messages, $domain));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain = 'messages')
+ {
+ $data = $indexes = $resources = '';
+
+ foreach ($messages->all($domain) as $source => $target) {
+ $indexes .= pack('v', strlen($data) + 28);
+ $data .= $source."\0";
+ }
+
+ $data .= $this->writePadding($data);
+
+ $keyTop = $this->getPosition($data);
+
+ foreach ($messages->all($domain) as $source => $target) {
+ $resources .= pack('V', $this->getPosition($data));
+
+ $data .= pack('V', strlen($target))
+ .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8')
+ .$this->writePadding($data)
+ ;
+ }
+
+ $resOffset = $this->getPosition($data);
+
+ $data .= pack('v', count($messages))
+ .$indexes
+ .$this->writePadding($data)
+ .$resources
+ ;
+
+ $bundleTop = $this->getPosition($data);
+
+ $root = pack('V7',
+ $resOffset + (2 << 28), // Resource Offset + Resource Type
+ 6, // Index length
+ $keyTop, // Index keys top
+ $bundleTop, // Index resources top
+ $bundleTop, // Index bundle top
+ count($messages), // Index max table length
+ 0 // Index attributes
+ );
+
+ $header = pack('vC2v4C12@32',
+ 32, // Header size
+ 0xDA, 0x27, // Magic number 1 and 2
+ 20, 0, 0, 2, // Rest of the header, ..., Size of a char
+ 0x52, 0x65, 0x73, 0x42, // Data format identifier
+ 1, 2, 0, 0, // Data version
+ 1, 4, 0, 0 // Unicode version
+ );
+
+ $output = $header
+ .$root
+ .$data;
+
+ return $output;
+ }
+
+ private function writePadding($data)
+ {
+ $padding = strlen($data) % 4;
+
+ if ($padding) {
+ return str_repeat("\xAA", 4 - $padding);
+ }
+ }
+
+ private function getPosition($data)
+ {
+ $position = (strlen($data) + 28) / 4;
+
+ return $position;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'res';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * IniFileDumper generates an ini formatted string representation of a message catalogue.
+ *
+ * @author Stealth35
+ */
+class IniFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain = 'messages')
+ {
+ $output = '';
+
+ foreach ($messages->all($domain) as $source => $target) {
+ $escapeTarget = str_replace('"', '\"', $target);
+ $output .= $source.'="'.$escapeTarget."\"\n";
+ }
+
+ return $output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'ini';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Loader\MoFileLoader;
+
+/**
+ * MoFileDumper generates a gettext formatted string representation of a message catalogue.
+ *
+ * @author Stealth35
+ */
+class MoFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain = 'messages')
+ {
+ $output = $sources = $targets = $sourceOffsets = $targetOffsets = '';
+ $offsets = array();
+ $size = 0;
+
+ foreach ($messages->all($domain) as $source => $target) {
+ $offsets[] = array_map('strlen', array($sources, $source, $targets, $target));
+ $sources .= "\0".$source;
+ $targets .= "\0".$target;
+ ++$size;
+ }
+
+ $header = array(
+ 'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC,
+ 'formatRevision' => 0,
+ 'count' => $size,
+ 'offsetId' => MoFileLoader::MO_HEADER_SIZE,
+ 'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size),
+ 'sizeHashes' => 0,
+ 'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size),
+ );
+
+ $sourcesSize = strlen($sources);
+ $sourcesStart = $header['offsetHashes'] + 1;
+
+ foreach ($offsets as $offset) {
+ $sourceOffsets .= $this->writeLong($offset[1])
+ .$this->writeLong($offset[0] + $sourcesStart);
+ $targetOffsets .= $this->writeLong($offset[3])
+ .$this->writeLong($offset[2] + $sourcesStart + $sourcesSize);
+ }
+
+ $output = implode(array_map(array($this, 'writeLong'), $header))
+ .$sourceOffsets
+ .$targetOffsets
+ .$sources
+ .$targets
+ ;
+
+ return $output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'mo';
+ }
+
+ private function writeLong($str)
+ {
+ return pack('V*', $str);
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * PhpFileDumper generates php files from a message catalogue.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+class PhpFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected function format(MessageCatalogue $messages, $domain)
+ {
+ $output = "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n";
+
+ return $output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'php';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * PoFileDumper generates a gettext formatted string representation of a message catalogue.
+ *
+ * @author Stealth35
+ */
+class PoFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain = 'messages')
+ {
+ $output = '';
+ $newLine = false;
+ foreach ($messages->all($domain) as $source => $target) {
+ if ($newLine) {
+ $output .= "\n";
+ } else {
+ $newLine = true;
+ }
+ $output .= sprintf('msgid "%s"'."\n", $this->escape($source));
+ $output .= sprintf('msgstr "%s"', $this->escape($target));
+ }
+
+ return $output;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'po';
+ }
+
+ private function escape($str)
+ {
+ return addcslashes($str, "\0..\37\42\134");
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * QtFileDumper generates ts files from a message catalogue.
+ *
+ * @author Benjamin Eberlei <kontakt@beberlei.de>
+ */
+class QtFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function format(MessageCatalogue $messages, $domain)
+ {
+ $dom = new \DOMDocument('1.0', 'utf-8');
+ $dom->formatOutput = true;
+ $ts = $dom->appendChild($dom->createElement('TS'));
+ $context = $ts->appendChild($dom->createElement('context'));
+ $context->appendChild($dom->createElement('name', $domain));
+
+ foreach ($messages->all($domain) as $source => $target) {
+ $message = $context->appendChild($dom->createElement('message'));
+ $message->appendChild($dom->createElement('source', $source));
+ $message->appendChild($dom->createElement('translation', $target));
+ }
+
+ return $dom->saveXML();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'ts';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * XliffFileDumper generates xliff files from a message catalogue.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+class XliffFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected function format(MessageCatalogue $messages, $domain)
+ {
+ $dom = new \DOMDocument('1.0', 'utf-8');
+ $dom->formatOutput = true;
+
+ $xliff = $dom->appendChild($dom->createElement('xliff'));
+ $xliff->setAttribute('version', '1.2');
+ $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
+
+ $xliffFile = $xliff->appendChild($dom->createElement('file'));
+ $xliffFile->setAttribute('source-language', $messages->getLocale());
+ $xliffFile->setAttribute('datatype', 'plaintext');
+ $xliffFile->setAttribute('original', 'file.ext');
+
+ $xliffBody = $xliffFile->appendChild($dom->createElement('body'));
+ foreach ($messages->all($domain) as $source => $target) {
+ $translation = $dom->createElement('trans-unit');
+
+ $translation->setAttribute('id', md5($source));
+ $translation->setAttribute('resname', $source);
+
+ $s = $translation->appendChild($dom->createElement('source'));
+ $s->appendChild($dom->createTextNode($source));
+
+ $t = $translation->appendChild($dom->createElement('target'));
+ $t->appendChild($dom->createTextNode($target));
+
+ $xliffBody->appendChild($translation);
+ }
+
+ return $dom->saveXML();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'xlf';
+ }
+}
--- /dev/null
+<?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\Translation\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Yaml\Yaml;
+
+/**
+ * YamlFileDumper generates yaml files from a message catalogue.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+class YamlFileDumper extends FileDumper
+{
+ /**
+ * {@inheritDoc}
+ */
+ protected function format(MessageCatalogue $messages, $domain)
+ {
+ return Yaml::dump($messages->all($domain));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getExtension()
+ {
+ return 'yml';
+ }
+}
--- /dev/null
+<?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\Translation\Exception;
+
+/**
+ * Exception interface for all exceptions thrown by the component.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface ExceptionInterface
+{
+}
--- /dev/null
+<?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\Translation\Exception;
+
+/**
+ * Thrown when a resource cannot be loaded.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Translation\Exception;
+
+/**
+ * Thrown when a resource does not exist.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
--- /dev/null
+<?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\Translation\Extractor;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * ChainExtractor extracts translation messages from template files.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+class ChainExtractor implements ExtractorInterface
+{
+ /**
+ * The extractors.
+ *
+ * @var ExtractorInterface[]
+ */
+ private $extractors = array();
+
+ /**
+ * Adds a loader to the translation extractor.
+ *
+ * @param string $format The format of the loader
+ * @param ExtractorInterface $extractor The loader
+ */
+ public function addExtractor($format, ExtractorInterface $extractor)
+ {
+ $this->extractors[$format] = $extractor;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setPrefix($prefix)
+ {
+ foreach ($this->extractors as $extractor) {
+ $extractor->setPrefix($prefix);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extract($directory, MessageCatalogue $catalogue)
+ {
+ foreach ($this->extractors as $extractor) {
+ $extractor->extract($directory, $catalogue);
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Extractor;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * Extracts translation messages from a template directory to the catalogue.
+ * New found messages are injected to the catalogue using the prefix.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+interface ExtractorInterface
+{
+ /**
+ * Extracts translation messages from a template directory to the catalogue.
+ *
+ * @param string $directory The path to look into
+ * @param MessageCatalogue $catalogue The catalogue
+ */
+ public function extract($directory, MessageCatalogue $catalogue);
+
+ /**
+ * Sets the prefix that should be used for new found messages.
+ *
+ * @param string $prefix The prefix
+ */
+ public function setPrefix($prefix);
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * IdentityTranslator does not translate anything.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class IdentityTranslator implements TranslatorInterface
+{
+ private $selector;
+
+ /**
+ * Constructor.
+ *
+ * @param MessageSelector $selector The message selector for pluralization
+ *
+ * @api
+ */
+ public function __construct(MessageSelector $selector)
+ {
+ $this->selector = $selector;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function setLocale($locale)
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getLocale()
+ {
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null)
+ {
+ return strtr((string) $id, $parameters);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null)
+ {
+ return strtr($this->selector->choose((string) $id, (int) $number, $locale), $parameters);
+ }
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * Tests if a given number belongs to a given math interval.
+ *
+ * An interval can represent a finite set of numbers:
+ *
+ * {1,2,3,4}
+ *
+ * An interval can represent numbers between two numbers:
+ *
+ * [1, +Inf]
+ * ]-1,2[
+ *
+ * The left delimiter can be [ (inclusive) or ] (exclusive).
+ * The right delimiter can be [ (exclusive) or ] (inclusive).
+ * Beside numbers, you can use -Inf and +Inf for the infinite.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
+ */
+class Interval
+{
+ /**
+ * Tests if the given number is in the math interval.
+ *
+ * @param integer $number A number
+ * @param string $interval An interval
+ *
+ * @return Boolean
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function test($number, $interval)
+ {
+ $interval = trim($interval);
+
+ if (!preg_match('/^'.self::getIntervalRegexp().'$/x', $interval, $matches)) {
+ throw new \InvalidArgumentException(sprintf('"%s" is not a valid interval.', $interval));
+ }
+
+ if ($matches[1]) {
+ foreach (explode(',', $matches[2]) as $n) {
+ if ($number == $n) {
+ return true;
+ }
+ }
+ } else {
+ $leftNumber = self::convertNumber($matches['left']);
+ $rightNumber = self::convertNumber($matches['right']);
+
+ return
+ ('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber)
+ && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber)
+ ;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a Regexp that matches valid intervals.
+ *
+ * @return string A Regexp (without the delimiters)
+ */
+ public static function getIntervalRegexp()
+ {
+ return <<<EOF
+ ({\s*
+ (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
+ \s*})
+
+ |
+
+ (?P<left_delimiter>[\[\]])
+ \s*
+ (?P<left>-Inf|\-?\d+(\.\d+)?)
+ \s*,\s*
+ (?P<right>\+?Inf|\-?\d+(\.\d+)?)
+ \s*
+ (?P<right_delimiter>[\[\]])
+EOF;
+ }
+
+ private static function convertNumber($number)
+ {
+ if ('-Inf' === $number) {
+ return log(0);
+ } elseif ('+Inf' === $number || 'Inf' === $number) {
+ return -log(0);
+ }
+
+ return (float) $number;
+ }
+}
--- /dev/null
+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.
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * ArrayLoader loads translations from a PHP array.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class ArrayLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ $this->flatten($resource);
+ $catalogue = new MessageCatalogue($locale);
+ $catalogue->add($resource, $domain);
+
+ return $catalogue;
+ }
+
+ /**
+ * Flattens an nested array of translations
+ *
+ * The scheme used is:
+ * 'key' => array('key2' => array('key3' => 'value'))
+ * Becomes:
+ * 'key.key2.key3' => 'value'
+ *
+ * This function takes an array by reference and will modify it
+ *
+ * @param array &$messages The array that will be flattened
+ * @param array $subnode Current subnode being parsed, used internally for recursive calls
+ * @param string $path Current path being parsed, used internally for recursive calls
+ */
+ private function flatten(array &$messages, array $subnode = null, $path = null)
+ {
+ if (null === $subnode) {
+ $subnode =& $messages;
+ }
+ foreach ($subnode as $key => $value) {
+ if (is_array($value)) {
+ $nodePath = $path ? $path.'.'.$key : $key;
+ $this->flatten($messages, $value, $nodePath);
+ if (null === $path) {
+ unset($messages[$key]);
+ }
+ } elseif (null !== $path) {
+ $messages[$path.'.'.$key] = $value;
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * CsvFileLoader loads translations from CSV files.
+ *
+ * @author Saša Stamenković <umpirsky@gmail.com>
+ *
+ * @api
+ */
+class CsvFileLoader extends ArrayLoader implements LoaderInterface
+{
+ private $delimiter = ';';
+ private $enclosure = '"';
+ private $escape = '\\';
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $messages = array();
+
+ try {
+ $file = new \SplFileObject($resource, 'rb');
+ } catch (\RuntimeException $e) {
+ throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e);
+ }
+
+ $file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY);
+ $file->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
+
+ foreach ($file as $data) {
+ if (substr($data[0], 0, 1) === '#') {
+ continue;
+ }
+
+ if (!isset($data[1])) {
+ continue;
+ }
+
+ if (count($data) == 2) {
+ $messages[$data[0]] = $data[1];
+ } else {
+ continue;
+ }
+ }
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+
+ /**
+ * Sets the delimiter, enclosure, and escape character for CSV.
+ *
+ * @param string $delimiter delimiter character
+ * @param string $enclosure enclosure character
+ * @param string $escape escape character
+ */
+ public function setCsvControl($delimiter = ';', $enclosure = '"', $escape = '\\')
+ {
+ $this->delimiter = $delimiter;
+ $this->enclosure = $enclosure;
+ $this->escape = $escape;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * IcuResFileLoader loads translations from a resource bundle.
+ *
+ * @author stealth35
+ */
+class IcuDatFileLoader extends IcuResFileLoader
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource.'.dat')) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource.'.dat')) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $rb = new \ResourceBundle($locale, $resource);
+
+ if (!$rb) {
+ throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
+ } elseif (intl_is_failure($rb->getErrorCode())) {
+ throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
+ }
+
+ $messages = $this->flatten($rb);
+ $catalogue = new MessageCatalogue($locale);
+ $catalogue->add($messages, $domain);
+ $catalogue->addResource(new FileResource($resource.'.dat'));
+
+ return $catalogue;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\DirectoryResource;
+
+/**
+ * IcuResFileLoader loads translations from a resource bundle.
+ *
+ * @author stealth35
+ */
+class IcuResFileLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!is_dir($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $rb = new \ResourceBundle($locale, $resource);
+
+ if (!$rb) {
+ throw new InvalidResourceException(sprintf('Cannot load resource "%s"', $resource));
+ } elseif (intl_is_failure($rb->getErrorCode())) {
+ throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode());
+ }
+
+ $messages = $this->flatten($rb);
+ $catalogue = new MessageCatalogue($locale);
+ $catalogue->add($messages, $domain);
+ $catalogue->addResource(new DirectoryResource($resource));
+
+ return $catalogue;
+ }
+
+ /**
+ * Flattens an ResourceBundle
+ *
+ * The scheme used is:
+ * key { key2 { key3 { "value" } } }
+ * Becomes:
+ * 'key.key2.key3' => 'value'
+ *
+ * This function takes an array by reference and will modify it
+ *
+ * @param \ResourceBundle $rb the ResourceBundle that will be flattened
+ * @param array $messages used internally for recursive calls
+ * @param string $path current path being parsed, used internally for recursive calls
+ *
+ * @return array the flattened ResourceBundle
+ */
+ protected function flatten(\ResourceBundle $rb, array &$messages = array(), $path = null)
+ {
+ foreach ($rb as $key => $value) {
+ $nodePath = $path ? $path.'.'.$key : $key;
+ if ($value instanceof \ResourceBundle) {
+ $this->flatten($value, $messages, $nodePath);
+ } else {
+ $messages[$nodePath] = $value;
+ }
+ }
+
+ return $messages;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * IniFileLoader loads translations from an ini file.
+ *
+ * @author stealth35
+ */
+class IniFileLoader extends ArrayLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $messages = parse_ini_file($resource, true);
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+
+/**
+ * LoaderInterface is the interface implemented by all translation loaders.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface LoaderInterface
+{
+ /**
+ * Loads a locale.
+ *
+ * @param mixed $resource A resource
+ * @param string $locale A locale
+ * @param string $domain The domain
+ *
+ * @return MessageCatalogue A MessageCatalogue instance
+ *
+ * @api
+ *
+ * @throws NotFoundResourceException when the resource cannot be found
+ * @throws InvalidResourceException when the resource cannot be loaded
+ */
+ public function load($resource, $locale, $domain = 'messages');
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
+ */
+class MoFileLoader extends ArrayLoader implements LoaderInterface
+{
+ /**
+ * Magic used for validating the format of a MO file as well as
+ * detecting if the machine used to create that file was little endian.
+ *
+ * @var float
+ */
+ const MO_LITTLE_ENDIAN_MAGIC = 0x950412de;
+
+ /**
+ * Magic used for validating the format of a MO file as well as
+ * detecting if the machine used to create that file was big endian.
+ *
+ * @var float
+ */
+ const MO_BIG_ENDIAN_MAGIC = 0xde120495;
+
+ /**
+ * The size of the header of a MO file in bytes.
+ *
+ * @var integer Number of bytes.
+ */
+ const MO_HEADER_SIZE = 28;
+
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $messages = $this->parse($resource);
+
+ // empty file
+ if (null === $messages) {
+ $messages = array();
+ }
+
+ // not an array
+ if (!is_array($messages)) {
+ throw new InvalidResourceException(sprintf('The file "%s" must contain a valid mo file.', $resource));
+ }
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+
+ /**
+ * Parses machine object (MO) format, independent of the machine's endian it
+ * was created on. Both 32bit and 64bit systems are supported.
+ *
+ * @param resource $resource
+ *
+ * @return array
+ * @throws InvalidResourceException If stream content has an invalid format.
+ */
+ private function parse($resource)
+ {
+ $stream = fopen($resource, 'r');
+
+ $stat = fstat($stream);
+
+ if ($stat['size'] < self::MO_HEADER_SIZE) {
+ throw new InvalidResourceException("MO stream content has an invalid format.");
+ }
+ $magic = unpack('V1', fread($stream, 4));
+ $magic = hexdec(substr(dechex(current($magic)), -8));
+
+ if ($magic == self::MO_LITTLE_ENDIAN_MAGIC) {
+ $isBigEndian = false;
+ } elseif ($magic == self::MO_BIG_ENDIAN_MAGIC) {
+ $isBigEndian = true;
+ } else {
+ throw new InvalidResourceException("MO stream content has an invalid format.");
+ }
+
+ $formatRevision = $this->readLong($stream, $isBigEndian);
+ $count = $this->readLong($stream, $isBigEndian);
+ $offsetId = $this->readLong($stream, $isBigEndian);
+ $offsetTranslated = $this->readLong($stream, $isBigEndian);
+ $sizeHashes = $this->readLong($stream, $isBigEndian);
+ $offsetHashes = $this->readLong($stream, $isBigEndian);
+
+ $messages = array();
+
+ for ($i = 0; $i < $count; $i++) {
+ $singularId = $pluralId = null;
+ $translated = null;
+
+ fseek($stream, $offsetId + $i * 8);
+
+ $length = $this->readLong($stream, $isBigEndian);
+ $offset = $this->readLong($stream, $isBigEndian);
+
+ if ($length < 1) {
+ continue;
+ }
+
+ fseek($stream, $offset);
+ $singularId = fread($stream, $length);
+
+ if (strpos($singularId, "\000") !== false) {
+ list($singularId, $pluralId) = explode("\000", $singularId);
+ }
+
+ fseek($stream, $offsetTranslated + $i * 8);
+ $length = $this->readLong($stream, $isBigEndian);
+ $offset = $this->readLong($stream, $isBigEndian);
+
+ fseek($stream, $offset);
+ $translated = fread($stream, $length);
+
+ if (strpos($translated, "\000") !== false) {
+ $translated = explode("\000", $translated);
+ }
+
+ $ids = array('singular' => $singularId, 'plural' => $pluralId);
+ $item = compact('ids', 'translated');
+
+ if (is_array($item['translated'])) {
+ $messages[$item['ids']['singular']] = stripcslashes($item['translated'][0]);
+ if (isset($item['ids']['plural'])) {
+ $plurals = array();
+ foreach ($item['translated'] as $plural => $translated) {
+ $plurals[] = sprintf('{%d} %s', $plural, $translated);
+ }
+ $messages[$item['ids']['plural']] = stripcslashes(implode('|', $plurals));
+ }
+ } elseif (!empty($item['ids']['singular'])) {
+ $messages[$item['ids']['singular']] = stripcslashes($item['translated']);
+ }
+ }
+
+ fclose($stream);
+
+ return array_filter($messages);
+ }
+
+ /**
+ * Reads an unsigned long from stream respecting endianess.
+ *
+ * @param resource $stream
+ * @param boolean $isBigEndian
+ * @return integer
+ */
+ private function readLong($stream, $isBigEndian)
+ {
+ $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
+ $result = current($result);
+
+ return (integer) substr($result, -8);
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * PhpFileLoader loads translations from PHP files returning an array of translations.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class PhpFileLoader extends ArrayLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $messages = require($resource);
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/)
+ * @copyright Copyright (c) 2012, Clemens Tolboom
+ */
+class PoFileLoader extends ArrayLoader implements LoaderInterface
+{
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $messages = $this->parse($resource);
+
+ // empty file
+ if (null === $messages) {
+ $messages = array();
+ }
+
+ // not an array
+ if (!is_array($messages)) {
+ throw new InvalidResourceException(sprintf('The file "%s" must contain a valid po file.', $resource));
+ }
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+
+ /**
+ * Parses portable object (PO) format.
+ *
+ * From http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files
+ * we should be able to parse files having:
+ *
+ * white-space
+ * # translator-comments
+ * #. extracted-comments
+ * #: reference...
+ * #, flag...
+ * #| msgid previous-untranslated-string
+ * msgid untranslated-string
+ * msgstr translated-string
+ *
+ * extra or different lines are:
+ *
+ * #| msgctxt previous-context
+ * #| msgid previous-untranslated-string
+ * msgctxt context
+ *
+ * #| msgid previous-untranslated-string-singular
+ * #| msgid_plural previous-untranslated-string-plural
+ * msgid untranslated-string-singular
+ * msgid_plural untranslated-string-plural
+ * msgstr[0] translated-string-case-0
+ * ...
+ * msgstr[N] translated-string-case-n
+ *
+ * The definition states:
+ * - white-space and comments are optional.
+ * - msgid "" that an empty singleline defines a header.
+ *
+ * This parser sacrifices some features of the reference implementation the
+ * differences to that implementation are as follows.
+ * - No support for comments spanning multiple lines.
+ * - Translator and extracted comments are treated as being the same type.
+ * - Message IDs are allowed to have other encodings as just US-ASCII.
+ *
+ * Items with an empty id are ignored.
+ *
+ * @param resource $resource
+ *
+ * @return array
+ */
+ private function parse($resource)
+ {
+ $stream = fopen($resource, 'r');
+
+ $defaults = array(
+ 'ids' => array(),
+ 'translated' => null,
+ );
+
+ $messages = array();
+ $item = $defaults;
+
+ while ($line = fgets($stream)) {
+ $line = trim($line);
+
+ if ($line === '') {
+ // Whitespace indicated current item is done
+ $this->addMessage($messages, $item);
+ $item = $defaults;
+ } elseif (substr($line, 0, 7) === 'msgid "') {
+ // We start a new msg so save previous
+ // TODO: this fails when comments or contexts are added
+ $this->addMessage($messages, $item);
+ $item = $defaults;
+ $item['ids']['singular'] = substr($line, 7, -1);
+ } elseif (substr($line, 0, 8) === 'msgstr "') {
+ $item['translated'] = substr($line, 8, -1);
+ } elseif ($line[0] === '"') {
+ $continues = isset($item['translated']) ? 'translated' : 'ids';
+
+ if (is_array($item[$continues])) {
+ end($item[$continues]);
+ $item[$continues][key($item[$continues])] .= substr($line, 1, -1);
+ } else {
+ $item[$continues] .= substr($line, 1, -1);
+ }
+ } elseif (substr($line, 0, 14) === 'msgid_plural "') {
+ $item['ids']['plural'] = substr($line, 14, -1);
+ } elseif (substr($line, 0, 7) === 'msgstr[') {
+ $size = strpos($line, ']');
+ $item['translated'][(integer) substr($line, 7, 1)] = substr($line, $size + 3, -1);
+ }
+
+ }
+ // save last item
+ $this->addMessage($messages, $item);
+ fclose($stream);
+
+ return $messages;
+ }
+
+ /**
+ * Save a translation item to the messeages.
+ *
+ * A .po file could contain by error missing plural indexes. We need to
+ * fix these before saving them.
+ *
+ * @param array $messages
+ * @param array $item
+ */
+ private function addMessage(array &$messages, array $item)
+ {
+ if (is_array($item['translated'])) {
+ $messages[$item['ids']['singular']] = stripslashes($item['translated'][0]);
+ if (isset($item['ids']['plural'])) {
+ $plurals = $item['translated'];
+ // PO are by definition indexed so sort by index.
+ ksort($plurals);
+ // Make sure every index is filled.
+ end($plurals);
+ $count = key($plurals);
+ // Fill missing spots with '-'.
+ $empties = array_fill(0, $count+1, '-');
+ $plurals += $empties;
+ ksort($plurals);
+ $messages[$item['ids']['plural']] = stripcslashes(implode('|', $plurals));
+ }
+ } elseif (!empty($item['ids']['singular'])) {
+ $messages[$item['ids']['singular']] = stripslashes($item['translated']);
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * QtFileLoader loads translations from QT Translations XML files.
+ *
+ * @author Benjamin Eberlei <kontakt@beberlei.de>
+ *
+ * @api
+ */
+class QtFileLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ $dom = new \DOMDocument();
+ $current = libxml_use_internal_errors(true);
+ if (!@$dom->load($resource, defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0)) {
+ throw new InvalidResourceException(implode("\n", $this->getXmlErrors()));
+ }
+
+ $xpath = new \DOMXPath($dom);
+ $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]');
+
+ $catalogue = new MessageCatalogue($locale);
+ if ($nodes->length == 1) {
+ $translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message');
+ foreach ($translations as $translation) {
+ $catalogue->set(
+ (string) $translation->getElementsByTagName('source')->item(0)->nodeValue,
+ (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue,
+ $domain
+ );
+ $translation = $translation->nextSibling;
+ }
+ $catalogue->addResource(new FileResource($resource));
+ }
+
+ libxml_use_internal_errors($current);
+
+ return $catalogue;
+ }
+
+ /**
+ * Returns the XML errors of the internal XML parser
+ *
+ * @return array An array of errors
+ */
+ private function getXmlErrors()
+ {
+ $errors = array();
+ foreach (libxml_get_errors() as $error) {
+ $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
+ LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
+ $error->code,
+ trim($error->message),
+ $error->file ? $error->file : 'n/a',
+ $error->line,
+ $error->column
+ );
+ }
+
+ libxml_clear_errors();
+ libxml_use_internal_errors(false);
+
+ return $errors;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+
+/**
+ * XliffFileLoader loads translations from XLIFF files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class XliffFileLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ list($xml, $encoding) = $this->parseFile($resource);
+ $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
+
+ $catalogue = new MessageCatalogue($locale);
+ foreach ($xml->xpath('//xliff:trans-unit') as $translation) {
+ $attributes = $translation->attributes();
+
+ if (!(isset($attributes['resname']) || isset($translation->source)) || !isset($translation->target)) {
+ continue;
+ }
+
+ $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source;
+ $target = (string) $translation->target;
+
+ // If the xlf file has another encoding specified, try to convert it because
+ // simple_xml will always return utf-8 encoded values
+ if ('UTF-8' !== $encoding && !empty($encoding)) {
+ if (function_exists('mb_convert_encoding')) {
+ $target = mb_convert_encoding($target, $encoding, 'UTF-8');
+ } elseif (function_exists('iconv')) {
+ $target = iconv('UTF-8', $encoding, $target);
+ } else {
+ throw new \RuntimeException('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
+ }
+ }
+
+ $catalogue->set((string) $source, $target, $domain);
+ }
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+
+ /**
+ * Validates and parses the given file into a SimpleXMLElement
+ *
+ * @param string $file
+ *
+ * @throws \RuntimeException
+ *
+ * @return \SimpleXMLElement
+ *
+ * @throws InvalidResourceException
+ */
+ private function parseFile($file)
+ {
+ $internalErrors = libxml_use_internal_errors(true);
+ $disableEntities = libxml_disable_entity_loader(true);
+ libxml_clear_errors();
+
+ $dom = new \DOMDocument();
+ $dom->validateOnParse = true;
+ if (!@$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
+ libxml_disable_entity_loader($disableEntities);
+
+ throw new InvalidResourceException(implode("\n", $this->getXmlErrors($internalErrors)));
+ }
+
+ libxml_disable_entity_loader($disableEntities);
+
+ foreach ($dom->childNodes as $child) {
+ if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
+ libxml_use_internal_errors($internalErrors);
+
+ throw new InvalidResourceException('Document types are not allowed.');
+ }
+ }
+
+ $location = str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd';
+ $parts = explode('/', $location);
+ if (0 === stripos($location, 'phar://')) {
+ $tmpfile = tempnam(sys_get_temp_dir(), 'sf2');
+ if ($tmpfile) {
+ copy($location, $tmpfile);
+ $parts = explode('/', str_replace('\\', '/', $tmpfile));
+ }
+ }
+ $drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
+ $location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
+
+ $source = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
+ $source = str_replace('http://www.w3.org/2001/xml.xsd', $location, $source);
+
+ if (!@$dom->schemaValidateSource($source)) {
+ throw new InvalidResourceException(implode("\n", $this->getXmlErrors($internalErrors)));
+ }
+
+ $dom->normalizeDocument();
+
+ libxml_use_internal_errors($internalErrors);
+
+ return array(simplexml_import_dom($dom), strtoupper($dom->encoding));
+ }
+
+ /**
+ * Returns the XML errors of the internal XML parser
+ *
+ * @param Boolean $internalErrors
+ *
+ * @return array An array of errors
+ */
+ private function getXmlErrors($internalErrors)
+ {
+ $errors = array();
+ foreach (libxml_get_errors() as $error) {
+ $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
+ LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
+ $error->code,
+ trim($error->message),
+ $error->file ? $error->file : 'n/a',
+ $error->line,
+ $error->column
+ );
+ }
+
+ libxml_clear_errors();
+ libxml_use_internal_errors($internalErrors);
+
+ return $errors;
+ }
+}
--- /dev/null
+<?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\Translation\Loader;
+
+use Symfony\Component\Translation\Exception\InvalidResourceException;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+use Symfony\Component\Config\Resource\FileResource;
+use Symfony\Component\Yaml\Parser as YamlParser;
+use Symfony\Component\Yaml\Exception\ParseException;
+
+/**
+ * YamlFileLoader loads translations from Yaml files.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class YamlFileLoader extends ArrayLoader implements LoaderInterface
+{
+ private $yamlParser;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function load($resource, $locale, $domain = 'messages')
+ {
+ if (!stream_is_local($resource)) {
+ throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource));
+ }
+
+ if (!file_exists($resource)) {
+ throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource));
+ }
+
+ if (null === $this->yamlParser) {
+ $this->yamlParser = new YamlParser();
+ }
+
+ try {
+ $messages = $this->yamlParser->parse(file_get_contents($resource));
+ } catch (ParseException $e) {
+ throw new InvalidResourceException('Error parsing YAML.', 0, $e);
+ }
+
+ // empty file
+ if (null === $messages) {
+ $messages = array();
+ }
+
+ // not an array
+ if (!is_array($messages)) {
+ throw new InvalidResourceException(sprintf('The file "%s" must contain a YAML array.', $resource));
+ }
+
+ $catalogue = parent::load($messages, $locale, $domain);
+ $catalogue->addResource(new FileResource($resource));
+
+ return $catalogue;
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+
+May-19-2004:
+- Changed the <choice> for ElemType_header, moving minOccurs="0" maxOccurs="unbounded" from its elements
+to <choice> itself.
+- Added <choice> for ElemType_trans-unit to allow "any order" for <context-group>, <count-group>, <prop-group>, <note>, and
+<alt-trans>.
+
+Oct-2005
+- updated version info to 1.2
+- equiv-trans attribute to <trans-unit> element
+- merged-trans attribute for <group> element
+- Add the <seg-source> element as optional in the <trans-unit> and <alt-trans> content models, at the same level as <source>
+- Create a new value "seg" for the mtype attribute of the <mrk> element
+- Add mid as an optional attribute for the <alt-trans> element
+
+Nov-14-2005
+- Changed name attribute for <context-group> from required to optional
+- Added extension point at <xliff>
+
+Jan-9-2006
+- Added alttranstype type attribute to <alt-trans>, and values
+
+Jan-10-2006
+- Corrected error with overwritten purposeValueList
+- Corrected name="AttrType_Version", attribute should have been "name"
+
+-->
+<xsd:schema xmlns:xlf="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="urn:oasis:names:tc:xliff:document:1.2" xml:lang="en">
+ <!-- Import for xml:lang and xml:space -->
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+ <!-- Attributes Lists -->
+ <xsd:simpleType name="XTend">
+ <xsd:restriction base="xsd:string">
+ <xsd:pattern value="x-[^\s]+"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="context-typeValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'context-type'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="database">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a database content.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="element">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the content of an element within an XML document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="elementtitle">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the name of an element within an XML document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="linenumber">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="numparams">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a the number of parameters contained within the <source>.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="paramnotes">
+ <xsd:annotation>
+ <xsd:documentation>Indicates notes pertaining to the parameters in the <source>.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="record">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the content of a record within a database.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="recordtitle">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the name of a record within a database.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="sourcefile">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="count-typeValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'count-type'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="num-usages">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="repetition">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the count units are translation units existing already in the same document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="total">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a total count.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="InlineDelimitersValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'ctype' when used other elements than <ph> or <x>.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="bold">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a run of bolded text.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="italic">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a run of text in italics.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="underlined">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a run of underlined text.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="link">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a run of hyper-text.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="InlinePlaceholdersValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'ctype' when used with <ph> or <x>.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="image">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a inline image.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pb">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a page break.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="lb">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a line break.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="mime-typeValueList">
+ <xsd:restriction base="xsd:string">
+ <xsd:pattern value="(text|multipart|message|application|image|audio|video|model)(/.+)*"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="datatypeValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'datatype'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="asp">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Active Server Page data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="c">
+ <xsd:annotation>
+ <xsd:documentation>Indicates C source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cdf">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Channel Definition Format (CDF) data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cfm">
+ <xsd:annotation>
+ <xsd:documentation>Indicates ColdFusion data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cpp">
+ <xsd:annotation>
+ <xsd:documentation>Indicates C++ source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="csharp">
+ <xsd:annotation>
+ <xsd:documentation>Indicates C-Sharp data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cstring">
+ <xsd:annotation>
+ <xsd:documentation>Indicates strings from C, ASM, and driver files data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="csv">
+ <xsd:annotation>
+ <xsd:documentation>Indicates comma-separated values data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="database">
+ <xsd:annotation>
+ <xsd:documentation>Indicates database data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="documentfooter">
+ <xsd:annotation>
+ <xsd:documentation>Indicates portions of document that follows data and contains metadata.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="documentheader">
+ <xsd:annotation>
+ <xsd:documentation>Indicates portions of document that precedes data and contains metadata.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="filedialog">
+ <xsd:annotation>
+ <xsd:documentation>Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="form">
+ <xsd:annotation>
+ <xsd:documentation>Indicates standard user input screen data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="html">
+ <xsd:annotation>
+ <xsd:documentation>Indicates HyperText Markup Language (HTML) data - document instance.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="htmlbody">
+ <xsd:annotation>
+ <xsd:documentation>Indicates content within an HTML document’s <body> element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="ini">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Windows INI file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="interleaf">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Interleaf data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="javaclass">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Java source file data (extension '.java').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="javapropertyresourcebundle">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Java property resource bundle data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="javalistresourcebundle">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Java list resource bundle data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="javascript">
+ <xsd:annotation>
+ <xsd:documentation>Indicates JavaScript source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="jscript">
+ <xsd:annotation>
+ <xsd:documentation>Indicates JScript source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="layout">
+ <xsd:annotation>
+ <xsd:documentation>Indicates information relating to formatting.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="lisp">
+ <xsd:annotation>
+ <xsd:documentation>Indicates LISP source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="margin">
+ <xsd:annotation>
+ <xsd:documentation>Indicates information relating to margin formats.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="menufile">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a file containing menu.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="messagefile">
+ <xsd:annotation>
+ <xsd:documentation>Indicates numerically identified string table.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="mif">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Maker Interchange Format (MIF) data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="mimetype">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="mo">
+ <xsd:annotation>
+ <xsd:documentation>Indicates GNU Machine Object data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="msglib">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Message Librarian strings created by Novell's Message Librarian Tool.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pagefooter">
+ <xsd:annotation>
+ <xsd:documentation>Indicates information to be displayed at the bottom of each page of a document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pageheader">
+ <xsd:annotation>
+ <xsd:documentation>Indicates information to be displayed at the top of each page of a document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="parameters">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a list of property values (e.g., settings within INI files or preferences dialog).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pascal">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Pascal source file data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="php">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Hypertext Preprocessor data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="plaintext">
+ <xsd:annotation>
+ <xsd:documentation>Indicates plain text file (no formatting other than, possibly, wrapping).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="po">
+ <xsd:annotation>
+ <xsd:documentation>Indicates GNU Portable Object file.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="report">
+ <xsd:annotation>
+ <xsd:documentation>Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="resources">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Windows .NET binary resources.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="resx">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Windows .NET Resources.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rtf">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Rich Text Format (RTF) data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="sgml">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Standard Generalized Markup Language (SGML) data - document instance.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="sgmldtd">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="svg">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Scalable Vector Graphic (SVG) data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="vbscript">
+ <xsd:annotation>
+ <xsd:documentation>Indicates VisualBasic Script source file.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="warning">
+ <xsd:annotation>
+ <xsd:documentation>Indicates warning message.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="winres">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="xhtml">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Extensible HyperText Markup Language (XHTML) data - document instance.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="xml">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Extensible Markup Language (XML) data - document instance.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="xmldtd">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="xsl">
+ <xsd:annotation>
+ <xsd:documentation>Indicates Extensible Stylesheet Language (XSL) data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="xul">
+ <xsd:annotation>
+ <xsd:documentation>Indicates XUL elements.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="mtypeValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'mtype'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="abbrev">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text is an abbreviation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="abbreviated-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="abbreviation">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="acronym">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="appellation">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620: A proper-name term, such as the name of an agency or other proper entity.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="collocation">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="common-name">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="datetime">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text is a date and/or time.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="equation">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="expanded-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="formula">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="head-term">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="initialism">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="international-scientific-term">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="internationalism">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="logical-expression">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="materials-management-unit">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.17: A unit to track object.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="name">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text is a name.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="near-synonym">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="part-number">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="phrase">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text is a phrase.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="phraseological-unit">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="protected">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text should not be translated.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="romanized-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="seg">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the marked text represents a segment.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="set-phrase">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.18.2: A fixed, lexicalized phrase.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="short-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="sku">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="standard-text">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.19: A fixed chunk of recurring text.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="symbol">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="synonym">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="synonymous-phrase">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="term">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the marked text is a term.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="transcribed-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="transliterated-form">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="truncated-term">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza').</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="variant">
+ <xsd:annotation>
+ <xsd:documentation>ISO-12620 2.1.9: One of the alternate forms of a term.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="restypeValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'restype'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="auto3state">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC AUTO3STATE control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="autocheckbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC AUTOCHECKBOX control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="autoradiobutton">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC AUTORADIOBUTTON control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="bedit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC BEDIT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="bitmap">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a bitmap, for example a BITMAP resource in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="button">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a button object, for example a BUTTON control Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="caption">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a caption, such as the caption of a dialog box.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cell">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the cell in a table, for example the content of the <td> element in HTML.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="checkbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates check box object, for example a CHECKBOX control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="checkboxmenuitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a menu item with an associated checkbox.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="checkedlistbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a list box, but with a check-box for each item.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="colorchooser">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a color selection dialog.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="combobox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="comboboxexitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="comboboxitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="component">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a UI base class element that cannot be represented by any other element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="contextmenu">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a context menu.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="ctext">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC CTEXT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cursor">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a cursor, for example a CURSOR resource in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="datetimepicker">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a date/time picker.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="defpushbutton">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC DEFPUSHBUTTON control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="dialog">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a dialog box.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="dlginit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC DLGINIT resource block.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="edit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an edit box object, for example an EDIT control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="file">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a filename.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="filechooser">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a file dialog.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="fn">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a footnote.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="font">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a font name.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="footer">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a footer.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="frame">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a frame object.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="grid">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a XUL grid element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="groupbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a groupbox object, for example a GROUPBOX control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="header">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a header item.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="heading">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="hedit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC HEDIT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="hscrollbar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a horizontal scrollbar.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="icon">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an icon, for example an ICON resource in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="iedit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC IEDIT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="keywords">
+ <xsd:annotation>
+ <xsd:documentation>Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="label">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a label object.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="linklabel">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a label that is also a HTML link (not necessarily a URL).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="list">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="listbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a listbox object, for example an LISTBOX control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="listitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an list item (an entry in a list).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="ltext">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC LTEXT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="menu">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a menu (a group of menu-items).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="menubar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a toolbar containing one or more tope level menus.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="menuitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a menu item (an entry in a menu).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="menuseparator">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a XUL menuseparator element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="message">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a message, for example an entry in a MESSAGETABLE resource in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="monthcalendar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a calendar control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="numericupdown">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an edit box beside a spin control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="panel">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a catch all for rectangular areas.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="popupmenu">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a standalone menu not necessarily associated with a menubar.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pushbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a pushbox object, for example a PUSHBOX control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pushbutton">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC PUSHBUTTON control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="radio">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a radio button object.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="radiobuttonmenuitem">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a menuitem with associated radio button.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rcdata">
+ <xsd:annotation>
+ <xsd:documentation>Indicates raw data resources for an application.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="row">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a row in a table.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rtext">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC RTEXT control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="scrollpane">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a user navigable container used to show a portion of a document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="separator">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a generic divider object (e.g. menu group separator).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="shortcut">
+ <xsd:annotation>
+ <xsd:documentation>Windows accelerators, shortcuts in resource or property files.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="spinner">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a UI control to indicate process activity but not progress.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="splitter">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a splitter bar.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="state3">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC STATE3 control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="statusbar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a window for providing feedback to the users, like 'read-only', etc.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="string">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a string, for example an entry in a STRINGTABLE resource in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="tabcontrol">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a layers of controls with a tab to select layers.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="table">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a display and edits regular two-dimensional tables of cells.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="textbox">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a XUL textbox element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="togglebutton">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a UI button that can be toggled to on or off state.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="toolbar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an array of controls, usually buttons.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="tooltip">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a pop up tool tip text.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="trackbar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a bar with a pointer indicating a position within a certain range.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="tree">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a control that displays a set of hierarchical data.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="uri">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a URI (URN or URL).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="userbutton">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a Windows RC USERBUTTON control.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="usercontrol">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a user-defined control like CONTROL control in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="var">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the text of a variable.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="versioninfo">
+ <xsd:annotation>
+ <xsd:documentation>Indicates version information about a resource like VERSIONINFO in Windows.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="vscrollbar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a vertical scrollbar.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="window">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a graphical window.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="size-unitValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'size-unit'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="byte">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in 8-bit bytes.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="char">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in Unicode characters.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="col">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in columns. Used for HTML text area.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="cm">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in centimeters.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="dlgunit">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in dialog units, as defined in Windows resources.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="em">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in 'font-size' units (as defined in CSS).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="ex">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in 'x-height' units (as defined in CSS).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="glyph">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster'</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="in">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in inches.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="mm">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in millimeters.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="percent">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in percentage.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="pixel">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in pixels.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="point">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in point.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="row">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a size in rows. Used for HTML text area.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="stateValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'state'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="final">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the terminating state.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-adaptation">
+ <xsd:annotation>
+ <xsd:documentation>Indicates only non-textual information needs adaptation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-l10n">
+ <xsd:annotation>
+ <xsd:documentation>Indicates both text and non-textual information needs adaptation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-review-adaptation">
+ <xsd:annotation>
+ <xsd:documentation>Indicates only non-textual information needs review.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-review-l10n">
+ <xsd:annotation>
+ <xsd:documentation>Indicates both text and non-textual information needs review.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-review-translation">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that only the text of the item needs to be reviewed.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="needs-translation">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item needs to be translated.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="new">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item is new. For example, translation units that were not in a previous version of the document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="signed-off">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that changes are reviewed and approved.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="translated">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item has been translated.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="state-qualifierValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'state-qualifier'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="exact-match">
+ <xsd:annotation>
+ <xsd:documentation>Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="fuzzy-match">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="id-match">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a match based on matching IDs (in addition to matching text).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="leveraged-glossary">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a translation derived from a glossary.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="leveraged-inherited">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a translation derived from existing translation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="leveraged-mt">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a translation derived from machine translation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="leveraged-repository">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a translation derived from a translation repository.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="leveraged-tm">
+ <xsd:annotation>
+ <xsd:documentation>Indicates a translation derived from a translation memory.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="mt-suggestion">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the translation is suggested by machine translation.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rejected-grammar">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item has been rejected because of incorrect grammar.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rejected-inaccurate">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item has been rejected because it is incorrect.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rejected-length">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item has been rejected because it is too long or too short.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rejected-spelling">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the item has been rejected because of incorrect spelling.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="tm-suggestion">
+ <xsd:annotation>
+ <xsd:documentation>Indicates the translation is suggested by translation memory.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="unitValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'unit'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="word">
+ <xsd:annotation>
+ <xsd:documentation>Refers to words.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="page">
+ <xsd:annotation>
+ <xsd:documentation>Refers to pages.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="trans-unit">
+ <xsd:annotation>
+ <xsd:documentation>Refers to <trans-unit> elements.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="bin-unit">
+ <xsd:annotation>
+ <xsd:documentation>Refers to <bin-unit> elements.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="glyph">
+ <xsd:annotation>
+ <xsd:documentation>Refers to glyphs.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="item">
+ <xsd:annotation>
+ <xsd:documentation>Refers to <trans-unit> and/or <bin-unit> elements.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="instance">
+ <xsd:annotation>
+ <xsd:documentation>Refers to the occurrences of instances defined by the count-type value.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="character">
+ <xsd:annotation>
+ <xsd:documentation>Refers to characters.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="line">
+ <xsd:annotation>
+ <xsd:documentation>Refers to lines.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="sentence">
+ <xsd:annotation>
+ <xsd:documentation>Refers to sentences.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="paragraph">
+ <xsd:annotation>
+ <xsd:documentation>Refers to paragraphs.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="segment">
+ <xsd:annotation>
+ <xsd:documentation>Refers to segments.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="placeable">
+ <xsd:annotation>
+ <xsd:documentation>Refers to placeables (inline elements).</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="priorityValueList">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'priority'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:positiveInteger">
+ <xsd:enumeration value="1">
+ <xsd:annotation>
+ <xsd:documentation>Highest priority.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="2">
+ <xsd:annotation>
+ <xsd:documentation>High priority.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="3">
+ <xsd:annotation>
+ <xsd:documentation>High priority, but not as important as 2.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="4">
+ <xsd:annotation>
+ <xsd:documentation>High priority, but not as important as 3.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="5">
+ <xsd:annotation>
+ <xsd:documentation>Medium priority, but more important than 6.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="6">
+ <xsd:annotation>
+ <xsd:documentation>Medium priority, but less important than 5.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="7">
+ <xsd:annotation>
+ <xsd:documentation>Low priority, but more important than 8.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="8">
+ <xsd:annotation>
+ <xsd:documentation>Low priority, but more important than 9.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="9">
+ <xsd:annotation>
+ <xsd:documentation>Low priority.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="10">
+ <xsd:annotation>
+ <xsd:documentation>Lowest priority.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="reformatValueYesNo">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="yes">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that all properties can be reformatted. This value must be used alone.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="no">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that no properties should be reformatted. This value must be used alone.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="reformatValueList">
+ <xsd:list>
+ <xsd:simpleType>
+ <xsd:union memberTypes="xlf:XTend">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="coord">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that all information in the coord attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="coord-x">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the x information in the coord attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="coord-y">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the y information in the coord attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="coord-cx">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the cx information in the coord attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="coord-cy">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the cy information in the coord attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="font">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that all the information in the font attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="font-name">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the name information in the font attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="font-size">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the size information in the font attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="font-weight">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the weight information in the font attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="css-style">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the information in the css-style attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="style">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the information in the style attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="ex-style">
+ <xsd:annotation>
+ <xsd:documentation>This value indicates that the information in the exstyle attribute can be modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:union>
+ </xsd:simpleType>
+ </xsd:list>
+ </xsd:simpleType>
+ <xsd:simpleType name="purposeValueList">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="information">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="location">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="match">
+ <xsd:annotation>
+ <xsd:documentation>Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="alttranstypeValueList">
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="proposal">
+ <xsd:annotation>
+ <xsd:documentation>Represents a translation proposal from a translation memory or other resource.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="previous-version">
+ <xsd:annotation>
+ <xsd:documentation>Represents a previous version of the target element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="rejected">
+ <xsd:annotation>
+ <xsd:documentation>Represents a rejected version of the target element.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="reference">
+ <xsd:annotation>
+ <xsd:documentation>Represents a translation to be used for reference purposes only, for example from a related product or a different language.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ <xsd:enumeration value="accepted">
+ <xsd:annotation>
+ <xsd:documentation>Represents a proposed translation that was used for the translation of the trans-unit, possibly modified.</xsd:documentation>
+ </xsd:annotation>
+ </xsd:enumeration>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <!-- Other Types -->
+ <xsd:complexType name="ElemType_ExternalReference">
+ <xsd:choice>
+ <xsd:element ref="xlf:internal-file"/>
+ <xsd:element ref="xlf:external-file"/>
+ </xsd:choice>
+ </xsd:complexType>
+ <xsd:simpleType name="AttrType_purpose">
+ <xsd:list>
+ <xsd:simpleType>
+ <xsd:union memberTypes="xlf:purposeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ </xsd:list>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_datatype">
+ <xsd:union memberTypes="xlf:datatypeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_restype">
+ <xsd:union memberTypes="xlf:restypeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_alttranstype">
+ <xsd:union memberTypes="xlf:alttranstypeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_context-type">
+ <xsd:union memberTypes="xlf:context-typeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_state">
+ <xsd:union memberTypes="xlf:stateValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_state-qualifier">
+ <xsd:union memberTypes="xlf:state-qualifierValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_count-type">
+ <xsd:union memberTypes="xlf:restypeValueList xlf:count-typeValueList xlf:datatypeValueList xlf:stateValueList xlf:state-qualifierValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_InlineDelimiters">
+ <xsd:union memberTypes="xlf:InlineDelimitersValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_InlinePlaceholders">
+ <xsd:union memberTypes="xlf:InlinePlaceholdersValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_size-unit">
+ <xsd:union memberTypes="xlf:size-unitValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_mtype">
+ <xsd:union memberTypes="xlf:mtypeValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_unit">
+ <xsd:union memberTypes="xlf:unitValueList xlf:XTend"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_priority">
+ <xsd:union memberTypes="xlf:priorityValueList"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_reformat">
+ <xsd:union memberTypes="xlf:reformatValueYesNo xlf:reformatValueList"/>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_YesNo">
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="yes"/>
+ <xsd:enumeration value="no"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_Position">
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="open"/>
+ <xsd:enumeration value="close"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_assoc">
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="preceding"/>
+ <xsd:enumeration value="following"/>
+ <xsd:enumeration value="both"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_annotates">
+ <xsd:restriction base="xsd:NMTOKEN">
+ <xsd:enumeration value="source"/>
+ <xsd:enumeration value="target"/>
+ <xsd:enumeration value="general"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_Coordinates">
+ <xsd:annotation>
+ <xsd:documentation>Values for the attribute 'coord'.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:string">
+ <xsd:pattern value="(-?\d+|#);(-?\d+|#);(-?\d+|#);(-?\d+|#)"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <xsd:simpleType name="AttrType_Version">
+ <xsd:annotation>
+ <xsd:documentation>Version values: 1.0 and 1.1 are allowed for backward compatibility.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="1.2"/>
+ <xsd:enumeration value="1.1"/>
+ <xsd:enumeration value="1.0"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ <!-- Groups -->
+ <xsd:group name="ElemGroup_TextContent">
+ <xsd:choice>
+ <xsd:element ref="xlf:g"/>
+ <xsd:element ref="xlf:bpt"/>
+ <xsd:element ref="xlf:ept"/>
+ <xsd:element ref="xlf:ph"/>
+ <xsd:element ref="xlf:it"/>
+ <xsd:element ref="xlf:mrk"/>
+ <xsd:element ref="xlf:x"/>
+ <xsd:element ref="xlf:bx"/>
+ <xsd:element ref="xlf:ex"/>
+ </xsd:choice>
+ </xsd:group>
+ <xsd:attributeGroup name="AttrGroup_TextContent">
+ <xsd:attribute name="id" type="xsd:string" use="required"/>
+ <xsd:attribute name="xid" type="xsd:string" use="optional"/>
+ <xsd:attribute name="equiv-text" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:attributeGroup>
+ <!-- XLIFF Structure -->
+ <xsd:element name="xliff">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded">
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ <xsd:element ref="xlf:file"/>
+ </xsd:sequence>
+ <xsd:attribute name="version" type="xlf:AttrType_Version" use="required"/>
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="file">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element minOccurs="0" ref="xlf:header"/>
+ <xsd:element ref="xlf:body"/>
+ </xsd:sequence>
+ <xsd:attribute name="original" type="xsd:string" use="required"/>
+ <xsd:attribute name="source-language" type="xsd:language" use="required"/>
+ <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="required"/>
+ <xsd:attribute name="tool-id" type="xsd:string" use="optional"/>
+ <xsd:attribute name="date" type="xsd:dateTime" use="optional"/>
+ <xsd:attribute ref="xml:space" use="optional"/>
+ <xsd:attribute name="category" type="xsd:string" use="optional"/>
+ <xsd:attribute name="target-language" type="xsd:language" use="optional"/>
+ <xsd:attribute name="product-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="product-version" type="xsd:string" use="optional"/>
+ <xsd:attribute name="build-num" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_group_id">
+ <xsd:selector xpath=".//xlf:group"/>
+ <xsd:field xpath="@id"/>
+ </xsd:unique>
+ <xsd:key name="K_unit_id">
+ <xsd:selector xpath=".//xlf:trans-unit|.//xlf:bin-unit"/>
+ <xsd:field xpath="@id"/>
+ </xsd:key>
+ <xsd:keyref name="KR_unit_id" refer="xlf:K_unit_id">
+ <xsd:selector xpath=".//bpt|.//ept|.//it|.//ph|.//g|.//x|.//bx|.//ex|.//sub"/>
+ <xsd:field xpath="@xid"/>
+ </xsd:keyref>
+ <xsd:key name="K_tool-id">
+ <xsd:selector xpath="xlf:header/xlf:tool"/>
+ <xsd:field xpath="@tool-id"/>
+ </xsd:key>
+ <xsd:keyref name="KR_file_tool-id" refer="xlf:K_tool-id">
+ <xsd:selector xpath="."/>
+ <xsd:field xpath="@tool-id"/>
+ </xsd:keyref>
+ <xsd:keyref name="KR_phase_tool-id" refer="xlf:K_tool-id">
+ <xsd:selector xpath="xlf:header/xlf:phase-group/xlf:phase"/>
+ <xsd:field xpath="@tool-id"/>
+ </xsd:keyref>
+ <xsd:keyref name="KR_alt-trans_tool-id" refer="xlf:K_tool-id">
+ <xsd:selector xpath=".//xlf:trans-unit/xlf:alt-trans"/>
+ <xsd:field xpath="@tool-id"/>
+ </xsd:keyref>
+ <xsd:key name="K_count-group_name">
+ <xsd:selector xpath=".//xlf:count-group"/>
+ <xsd:field xpath="@name"/>
+ </xsd:key>
+ <xsd:unique name="U_context-group_name">
+ <xsd:selector xpath=".//xlf:context-group"/>
+ <xsd:field xpath="@name"/>
+ </xsd:unique>
+ <xsd:key name="K_phase-name">
+ <xsd:selector xpath="xlf:header/xlf:phase-group/xlf:phase"/>
+ <xsd:field xpath="@phase-name"/>
+ </xsd:key>
+ <xsd:keyref name="KR_phase-name" refer="xlf:K_phase-name">
+ <xsd:selector xpath=".//xlf:count|.//xlf:trans-unit|.//xlf:target|.//bin-unit|.//bin-target"/>
+ <xsd:field xpath="@phase-name"/>
+ </xsd:keyref>
+ <xsd:unique name="U_uid">
+ <xsd:selector xpath=".//xlf:external-file"/>
+ <xsd:field xpath="@uid"/>
+ </xsd:unique>
+ </xsd:element>
+ <xsd:element name="header">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element minOccurs="0" name="skl" type="xlf:ElemType_ExternalReference"/>
+ <xsd:element minOccurs="0" ref="xlf:phase-group"/>
+ <xsd:choice maxOccurs="unbounded" minOccurs="0">
+ <xsd:element name="glossary" type="xlf:ElemType_ExternalReference"/>
+ <xsd:element name="reference" type="xlf:ElemType_ExternalReference"/>
+ <xsd:element ref="xlf:count-group"/>
+ <xsd:element ref="xlf:note"/>
+ <xsd:element ref="xlf:tool"/>
+ </xsd:choice>
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="internal-file">
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute name="form" type="xsd:string"/>
+ <xsd:attribute name="crc" type="xsd:NMTOKEN"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="external-file">
+ <xsd:complexType>
+ <xsd:attribute name="href" type="xsd:string" use="required"/>
+ <xsd:attribute name="crc" type="xsd:NMTOKEN"/>
+ <xsd:attribute name="uid" type="xsd:NMTOKEN"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="note">
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:attribute default="1" name="priority" type="xlf:AttrType_priority" use="optional"/>
+ <xsd:attribute name="from" type="xsd:string" use="optional"/>
+ <xsd:attribute default="general" name="annotates" type="xlf:AttrType_annotates" use="optional"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="phase-group">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded">
+ <xsd:element ref="xlf:phase"/>
+ </xsd:sequence>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="phase">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:note"/>
+ </xsd:sequence>
+ <xsd:attribute name="phase-name" type="xsd:string" use="required"/>
+ <xsd:attribute name="process-name" type="xsd:string" use="required"/>
+ <xsd:attribute name="company-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="tool-id" type="xsd:string" use="optional"/>
+ <xsd:attribute name="date" type="xsd:dateTime" use="optional"/>
+ <xsd:attribute name="job-id" type="xsd:string" use="optional"/>
+ <xsd:attribute name="contact-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="contact-email" type="xsd:string" use="optional"/>
+ <xsd:attribute name="contact-phone" type="xsd:string" use="optional"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="count-group">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:count"/>
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="count">
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute name="count-type" type="xlf:AttrType_count-type" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:string" use="optional"/>
+ <xsd:attribute default="word" name="unit" type="xlf:AttrType_unit" use="optional"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="context-group">
+ <xsd:complexType>
+ <xsd:sequence maxOccurs="unbounded">
+ <xsd:element ref="xlf:context"/>
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="purpose" type="xlf:AttrType_purpose" use="optional"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="context">
+ <xsd:complexType>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute name="context-type" type="xlf:AttrType_context-type" use="required"/>
+ <xsd:attribute default="no" name="match-mandatory" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="tool">
+ <xsd:complexType mixed="true">
+ <xsd:sequence>
+ <xsd:any namespace="##any" processContents="strict" minOccurs="0" maxOccurs="unbounded"/>
+ </xsd:sequence>
+ <xsd:attribute name="tool-id" type="xsd:string" use="required"/>
+ <xsd:attribute name="tool-name" type="xsd:string" use="required"/>
+ <xsd:attribute name="tool-version" type="xsd:string" use="optional"/>
+ <xsd:attribute name="tool-company" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="body">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded" minOccurs="0">
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:group"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:trans-unit"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:bin-unit"/>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="group">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:sequence>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:context-group"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:count-group"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:note"/>
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ </xsd:sequence>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:group"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:trans-unit"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:bin-unit"/>
+ </xsd:choice>
+ </xsd:sequence>
+ <xsd:attribute name="id" type="xsd:string" use="optional"/>
+ <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/>
+ <xsd:attribute default="default" ref="xml:space" use="optional"/>
+ <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extradata" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extype" type="xsd:string" use="optional"/>
+ <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="menu" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-option" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/>
+ <xsd:attribute name="font" type="xsd:string" use="optional"/>
+ <xsd:attribute name="css-style" type="xsd:string" use="optional"/>
+ <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/>
+ <xsd:attribute default="pixel" name="size-unit" type="xlf:AttrType_size-unit" use="optional"/>
+ <xsd:attribute name="maxwidth" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minwidth" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="maxheight" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minheight" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="maxbytes" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minbytes" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="charclass" type="xsd:string" use="optional"/>
+ <xsd:attribute default="no" name="merged-trans" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="trans-unit">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="xlf:source"/>
+ <xsd:element minOccurs="0" ref="xlf:seg-source"/>
+ <xsd:element minOccurs="0" ref="xlf:target"/>
+ <xsd:choice maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:context-group"/>
+ <xsd:element ref="xlf:count-group"/>
+ <xsd:element ref="xlf:note"/>
+ <xsd:element ref="xlf:alt-trans"/>
+ </xsd:choice>
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ </xsd:sequence>
+ <xsd:attribute name="id" type="xsd:string" use="required"/>
+ <xsd:attribute name="approved" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/>
+ <xsd:attribute default="default" ref="xml:space" use="optional"/>
+ <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extradata" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extype" type="xsd:string" use="optional"/>
+ <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="menu" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-option" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/>
+ <xsd:attribute name="font" type="xsd:string" use="optional"/>
+ <xsd:attribute name="css-style" type="xsd:string" use="optional"/>
+ <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute default="pixel" name="size-unit" type="xlf:AttrType_size-unit" use="optional"/>
+ <xsd:attribute name="maxwidth" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minwidth" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="maxheight" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minheight" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="maxbytes" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="minbytes" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="charclass" type="xsd:string" use="optional"/>
+ <xsd:attribute default="yes" name="merged-trans" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_tu_segsrc_mid">
+ <xsd:selector xpath="./xlf:seg-source/xlf:mrk"/>
+ <xsd:field xpath="@mid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_tu_segsrc_mid" refer="xlf:U_tu_segsrc_mid">
+ <xsd:selector xpath="./xlf:target/xlf:mrk|./xlf:alt-trans"/>
+ <xsd:field xpath="@mid"/>
+ </xsd:keyref>
+ </xsd:element>
+ <xsd:element name="source">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_source_bpt_rid">
+ <xsd:selector xpath=".//xlf:bpt"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_source_ept_rid" refer="xlf:U_source_bpt_rid">
+ <xsd:selector xpath=".//xlf:ept"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ <xsd:unique name="U_source_bx_rid">
+ <xsd:selector xpath=".//xlf:bx"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_source_ex_rid" refer="xlf:U_source_bx_rid">
+ <xsd:selector xpath=".//xlf:ex"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ </xsd:element>
+ <xsd:element name="seg-source">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_segsrc_bpt_rid">
+ <xsd:selector xpath=".//xlf:bpt"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_segsrc_ept_rid" refer="xlf:U_segsrc_bpt_rid">
+ <xsd:selector xpath=".//xlf:ept"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ <xsd:unique name="U_segsrc_bx_rid">
+ <xsd:selector xpath=".//xlf:bx"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_segsrc_ex_rid" refer="xlf:U_segsrc_bx_rid">
+ <xsd:selector xpath=".//xlf:ex"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ </xsd:element>
+ <xsd:element name="target">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute name="state" type="xlf:AttrType_state" use="optional"/>
+ <xsd:attribute name="state-qualifier" type="xlf:AttrType_state-qualifier" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/>
+ <xsd:attribute name="font" type="xsd:string" use="optional"/>
+ <xsd:attribute name="css-style" type="xsd:string" use="optional"/>
+ <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute default="yes" name="equiv-trans" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_target_bpt_rid">
+ <xsd:selector xpath=".//xlf:bpt"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_target_ept_rid" refer="xlf:U_target_bpt_rid">
+ <xsd:selector xpath=".//xlf:ept"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ <xsd:unique name="U_target_bx_rid">
+ <xsd:selector xpath=".//xlf:bx"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_target_ex_rid" refer="xlf:U_target_bx_rid">
+ <xsd:selector xpath=".//xlf:ex"/>
+ <xsd:field xpath="@rid"/>
+ </xsd:keyref>
+ </xsd:element>
+ <xsd:element name="alt-trans">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element minOccurs="0" ref="xlf:source"/>
+ <xsd:element minOccurs="0" ref="xlf:seg-source"/>
+ <xsd:element maxOccurs="1" ref="xlf:target"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:context-group"/>
+ <xsd:element maxOccurs="unbounded" minOccurs="0" ref="xlf:note"/>
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ </xsd:sequence>
+ <xsd:attribute name="match-quality" type="xsd:string" use="optional"/>
+ <xsd:attribute name="tool-id" type="xsd:string" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute ref="xml:lang" use="optional"/>
+ <xsd:attribute name="origin" type="xsd:string" use="optional"/>
+ <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/>
+ <xsd:attribute default="default" ref="xml:space" use="optional"/>
+ <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extradata" type="xsd:string" use="optional"/>
+ <xsd:attribute name="extype" type="xsd:string" use="optional"/>
+ <xsd:attribute name="help-id" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="menu" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-option" type="xsd:string" use="optional"/>
+ <xsd:attribute name="menu-name" type="xsd:string" use="optional"/>
+ <xsd:attribute name="mid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="coord" type="xlf:AttrType_Coordinates" use="optional"/>
+ <xsd:attribute name="font" type="xsd:string" use="optional"/>
+ <xsd:attribute name="css-style" type="xsd:string" use="optional"/>
+ <xsd:attribute name="style" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="exstyle" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute default="proposal" name="alttranstype" type="xlf:AttrType_alttranstype" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ <xsd:unique name="U_at_segsrc_mid">
+ <xsd:selector xpath="./xlf:seg-source/xlf:mrk"/>
+ <xsd:field xpath="@mid"/>
+ </xsd:unique>
+ <xsd:keyref name="KR_at_segsrc_mid" refer="xlf:U_at_segsrc_mid">
+ <xsd:selector xpath="./xlf:target/xlf:mrk"/>
+ <xsd:field xpath="@mid"/>
+ </xsd:keyref>
+ </xsd:element>
+ <xsd:element name="bin-unit">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element ref="xlf:bin-source"/>
+ <xsd:element minOccurs="0" ref="xlf:bin-target"/>
+ <xsd:choice maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:context-group"/>
+ <xsd:element ref="xlf:count-group"/>
+ <xsd:element ref="xlf:note"/>
+ <xsd:element ref="xlf:trans-unit"/>
+ </xsd:choice>
+ <xsd:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="strict"/>
+ </xsd:sequence>
+ <xsd:attribute name="id" type="xsd:string" use="required"/>
+ <xsd:attribute name="mime-type" type="xlf:mime-typeValueList" use="required"/>
+ <xsd:attribute name="approved" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute default="yes" name="translate" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attribute default="yes" name="reformat" type="xlf:AttrType_reformat" use="optional"/>
+ <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="bin-source">
+ <xsd:complexType>
+ <xsd:choice>
+ <xsd:element ref="xlf:internal-file"/>
+ <xsd:element ref="xlf:external-file"/>
+ </xsd:choice>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="bin-target">
+ <xsd:complexType>
+ <xsd:choice>
+ <xsd:element ref="xlf:internal-file"/>
+ <xsd:element ref="xlf:external-file"/>
+ </xsd:choice>
+ <xsd:attribute name="mime-type" type="xlf:mime-typeValueList" use="optional"/>
+ <xsd:attribute name="state" type="xlf:AttrType_state" use="optional"/>
+ <xsd:attribute name="state-qualifier" type="xlf:AttrType_state-qualifier" use="optional"/>
+ <xsd:attribute name="phase-name" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="restype" type="xlf:AttrType_restype" use="optional"/>
+ <xsd:attribute name="resname" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+ <!-- Element for inline codes -->
+ <xsd:element name="g">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/>
+ <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="x">
+ <xsd:complexType>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlinePlaceholders" use="optional"/>
+ <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="bx">
+ <xsd:complexType>
+ <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/>
+ <xsd:attribute default="yes" name="clone" type="xlf:AttrType_YesNo" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ex">
+ <xsd:complexType>
+ <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ph">
+ <xsd:complexType mixed="true">
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:sub"/>
+ </xsd:sequence>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlinePlaceholders" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:string" use="optional"/>
+ <xsd:attribute name="assoc" type="xlf:AttrType_assoc" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="bpt">
+ <xsd:complexType mixed="true">
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:sub"/>
+ </xsd:sequence>
+ <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:string" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="ept">
+ <xsd:complexType mixed="true">
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:sub"/>
+ </xsd:sequence>
+ <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:string" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="it">
+ <xsd:complexType mixed="true">
+ <xsd:sequence maxOccurs="unbounded" minOccurs="0">
+ <xsd:element ref="xlf:sub"/>
+ </xsd:sequence>
+ <xsd:attribute name="pos" type="xlf:AttrType_Position" use="required"/>
+ <xsd:attribute name="rid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/>
+ <xsd:attribute name="crc" type="xsd:string" use="optional"/>
+ <xsd:attributeGroup ref="xlf:AttrGroup_TextContent"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="sub">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute name="datatype" type="xlf:AttrType_datatype" use="optional"/>
+ <xsd:attribute name="ctype" type="xlf:AttrType_InlineDelimiters" use="optional"/>
+ <xsd:attribute name="xid" type="xsd:string" use="optional"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="mrk">
+ <xsd:complexType mixed="true">
+ <xsd:group maxOccurs="unbounded" minOccurs="0" ref="xlf:ElemGroup_TextContent"/>
+ <xsd:attribute name="mtype" type="xlf:AttrType_mtype" use="required"/>
+ <xsd:attribute name="mid" type="xsd:NMTOKEN" use="optional"/>
+ <xsd:attribute name="comment" type="xsd:string" use="optional"/>
+ <xsd:anyAttribute namespace="##other" processContents="strict"/>
+ </xsd:complexType>
+ </xsd:element>
+</xsd:schema>
--- /dev/null
+<?xml version='1.0'?>
+<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
+<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns ="http://www.w3.org/1999/xhtml"
+ xml:lang="en">
+
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+ <h1>About the XML namespace</h1>
+
+ <div class="bodytext">
+ <p>
+
+ This schema document describes the XML namespace, in a form
+ suitable for import by other schema documents.
+ </p>
+ <p>
+ See <a href="http://www.w3.org/XML/1998/namespace.html">
+ http://www.w3.org/XML/1998/namespace.html</a> and
+ <a href="http://www.w3.org/TR/REC-xml">
+ http://www.w3.org/TR/REC-xml</a> for information
+ about this namespace.
+ </p>
+
+ <p>
+ Note that local names in this namespace are intended to be
+ defined only by the World Wide Web Consortium or its subgroups.
+ The names currently defined in this namespace are listed below.
+ They should not be used with conflicting semantics by any Working
+ Group, specification, or document instance.
+ </p>
+ <p>
+ See further below in this document for more information about <a
+ href="#usage">how to refer to this schema document from your own
+ XSD schema documents</a> and about <a href="#nsversioning">the
+ namespace-versioning policy governing this schema document</a>.
+ </p>
+ </div>
+ </div>
+
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:attribute name="lang">
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>lang (as an attribute name)</h3>
+ <p>
+
+ denotes an attribute whose value
+ is a language code for the natural language of the content of
+ any element; its value is inherited. This name is reserved
+ by virtue of its definition in the XML specification.</p>
+
+ </div>
+ <div>
+ <h4>Notes</h4>
+ <p>
+ Attempting to install the relevant ISO 2- and 3-letter
+ codes as the enumerated possible values is probably never
+ going to be a realistic possibility.
+ </p>
+ <p>
+
+ See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
+ http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
+ and the IANA language subtag registry at
+ <a href="http://www.iana.org/assignments/language-subtag-registry">
+ http://www.iana.org/assignments/language-subtag-registry</a>
+ for further information.
+ </p>
+ <p>
+
+ The union allows for the 'un-declaration' of xml:lang with
+ the empty string.
+ </p>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:simpleType>
+ <xs:union memberTypes="xs:language">
+ <xs:simpleType>
+ <xs:restriction base="xs:string">
+ <xs:enumeration value=""/>
+
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:union>
+ </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="space">
+ <xs:annotation>
+ <xs:documentation>
+
+ <div>
+
+ <h3>space (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose
+ value is a keyword indicating what whitespace processing
+ discipline is intended for the content of the element; its
+ value is inherited. This name is reserved by virtue of its
+ definition in the XML specification.</p>
+
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ <xs:simpleType>
+
+ <xs:restriction base="xs:NCName">
+ <xs:enumeration value="default"/>
+ <xs:enumeration value="preserve"/>
+ </xs:restriction>
+ </xs:simpleType>
+ </xs:attribute>
+
+ <xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
+ <xs:documentation>
+
+ <div>
+
+ <h3>base (as an attribute name)</h3>
+ <p>
+ denotes an attribute whose value
+ provides a URI to be used as the base for interpreting any
+ relative URIs in the scope of the element on which it
+ appears; its value is inherited. This name is reserved
+ by virtue of its definition in the XML Base specification.</p>
+
+ <p>
+ See <a
+ href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
+ for information about this attribute.
+ </p>
+
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+ </xs:attribute>
+
+ <xs:attribute name="id" type="xs:ID">
+ <xs:annotation>
+ <xs:documentation>
+ <div>
+
+ <h3>id (as an attribute name)</h3>
+ <p>
+
+ denotes an attribute whose value
+ should be interpreted as if declared to be of type ID.
+ This name is reserved by virtue of its definition in the
+ xml:id specification.</p>
+
+ <p>
+ See <a
+ href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
+ for information about this attribute.
+ </p>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ </xs:attribute>
+
+ <xs:attributeGroup name="specialAttrs">
+ <xs:attribute ref="xml:base"/>
+ <xs:attribute ref="xml:lang"/>
+ <xs:attribute ref="xml:space"/>
+ <xs:attribute ref="xml:id"/>
+ </xs:attributeGroup>
+
+ <xs:annotation>
+
+ <xs:documentation>
+ <div>
+
+ <h3>Father (in any context at all)</h3>
+
+ <div class="bodytext">
+ <p>
+ denotes Jon Bosak, the chair of
+ the original XML Working Group. This name is reserved by
+ the following decision of the W3C XML Plenary and
+ XML Coordination groups:
+ </p>
+ <blockquote>
+ <p>
+
+ In appreciation for his vision, leadership and
+ dedication the W3C XML Plenary on this 10th day of
+ February, 2000, reserves for Jon Bosak in perpetuity
+ the XML name "xml:Father".
+ </p>
+ </blockquote>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+ <xs:documentation>
+
+ <div xml:id="usage" id="usage">
+ <h2><a name="usage">About this schema document</a></h2>
+
+ <div class="bodytext">
+ <p>
+ This schema defines attributes and an attribute group suitable
+ for use by schemas wishing to allow <code>xml:base</code>,
+ <code>xml:lang</code>, <code>xml:space</code> or
+ <code>xml:id</code> attributes on elements they define.
+ </p>
+
+ <p>
+ To enable this, such a schema must import this schema for
+ the XML namespace, e.g. as follows:
+ </p>
+ <pre>
+ <schema.. .>
+ .. .
+ <import namespace="http://www.w3.org/XML/1998/namespace"
+ schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+ </pre>
+ <p>
+ or
+ </p>
+ <pre>
+
+ <import namespace="http://www.w3.org/XML/1998/namespace"
+ schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+ </pre>
+ <p>
+ Subsequently, qualified reference to any of the attributes or the
+ group defined below will have the desired effect, e.g.
+ </p>
+ <pre>
+ <type.. .>
+ .. .
+ <attributeGroup ref="xml:specialAttrs"/>
+ </pre>
+ <p>
+ will define a type which will schema-validate an instance element
+ with any of those attributes.
+ </p>
+
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+ <xs:annotation>
+ <xs:documentation>
+ <div id="nsversioning" xml:id="nsversioning">
+ <h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
+
+ <div class="bodytext">
+ <p>
+ In keeping with the XML Schema WG's standard versioning
+ policy, this schema document will persist at
+ <a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd</a>.
+ </p>
+ <p>
+ At the date of issue it can also be found at
+ <a href="http://www.w3.org/2001/xml.xsd">
+ http://www.w3.org/2001/xml.xsd</a>.
+ </p>
+
+ <p>
+ The schema document at that URI may however change in the future,
+ in order to remain compatible with the latest version of XML
+ Schema itself, or with the XML namespace itself. In other words,
+ if the XML Schema or XML namespaces change, the version of this
+ document at <a href="http://www.w3.org/2001/xml.xsd">
+ http://www.w3.org/2001/xml.xsd
+ </a>
+ will change accordingly; the version at
+ <a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd
+ </a>
+ will not change.
+ </p>
+ <p>
+
+ Previous dated (and unchanging) versions of this schema
+ document are at:
+ </p>
+ <ul>
+ <li><a href="http://www.w3.org/2009/01/xml.xsd">
+ http://www.w3.org/2009/01/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2007/08/xml.xsd">
+ http://www.w3.org/2007/08/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2004/10/xml.xsd">
+
+ http://www.w3.org/2004/10/xml.xsd</a></li>
+ <li><a href="http://www.w3.org/2001/03/xml.xsd">
+ http://www.w3.org/2001/03/xml.xsd</a></li>
+ </ul>
+ </div>
+ </div>
+ </xs:documentation>
+ </xs:annotation>
+
+</xs:schema>
--- /dev/null
+<?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\Translation;
+
+use Symfony\Component\Config\Resource\ResourceInterface;
+
+/**
+ * MessageCatalogue.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface
+{
+ private $messages = array();
+ private $metadata = array();
+ private $resources = array();
+ private $locale;
+ private $fallbackCatalogue;
+ private $parent;
+
+ /**
+ * Constructor.
+ *
+ * @param string $locale The locale
+ * @param array $messages An array of messages classified by domain
+ *
+ * @api
+ */
+ public function __construct($locale, array $messages = array())
+ {
+ $this->locale = $locale;
+ $this->messages = $messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getLocale()
+ {
+ return $this->locale;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getDomains()
+ {
+ return array_keys($this->messages);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function all($domain = null)
+ {
+ if (null === $domain) {
+ return $this->messages;
+ }
+
+ return isset($this->messages[$domain]) ? $this->messages[$domain] : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function set($id, $translation, $domain = 'messages')
+ {
+ $this->add(array($id => $translation), $domain);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function has($id, $domain = 'messages')
+ {
+ if (isset($this->messages[$domain][$id])) {
+ return true;
+ }
+
+ if (null !== $this->fallbackCatalogue) {
+ return $this->fallbackCatalogue->has($id, $domain);
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defines($id, $domain = 'messages')
+ {
+ return isset($this->messages[$domain][$id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function get($id, $domain = 'messages')
+ {
+ if (isset($this->messages[$domain][$id])) {
+ return $this->messages[$domain][$id];
+ }
+
+ if (null !== $this->fallbackCatalogue) {
+ return $this->fallbackCatalogue->get($id, $domain);
+ }
+
+ return $id;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function replace($messages, $domain = 'messages')
+ {
+ $this->messages[$domain] = array();
+
+ $this->add($messages, $domain);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function add($messages, $domain = 'messages')
+ {
+ if (!isset($this->messages[$domain])) {
+ $this->messages[$domain] = $messages;
+ } else {
+ $this->messages[$domain] = array_replace($this->messages[$domain], $messages);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function addCatalogue(MessageCatalogueInterface $catalogue)
+ {
+ if ($catalogue->getLocale() !== $this->locale) {
+ throw new \LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s"', $catalogue->getLocale(), $this->locale));
+ }
+
+ foreach ($catalogue->all() as $domain => $messages) {
+ $this->add($messages, $domain);
+ }
+
+ foreach ($catalogue->getResources() as $resource) {
+ $this->addResource($resource);
+ }
+
+ if ($catalogue instanceof MetadataAwareInterface) {
+ $metadata = $catalogue->getMetadata('', '');
+ $this->addMetadata($metadata);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function addFallbackCatalogue(MessageCatalogueInterface $catalogue)
+ {
+ // detect circular references
+ $c = $this;
+ do {
+ if ($c->getLocale() === $catalogue->getLocale()) {
+ throw new \LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale()));
+ }
+ } while ($c = $c->parent);
+
+ $catalogue->parent = $this;
+ $this->fallbackCatalogue = $catalogue;
+
+ foreach ($catalogue->getResources() as $resource) {
+ $this->addResource($resource);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getFallbackCatalogue()
+ {
+ return $this->fallbackCatalogue;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getResources()
+ {
+ return array_values($this->resources);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function addResource(ResourceInterface $resource)
+ {
+ $this->resources[$resource->__toString()] = $resource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadata($key = '', $domain = 'messages')
+ {
+ if ('' == $domain) {
+ return $this->metadata;
+ }
+
+ if (isset($this->metadata[$domain])) {
+ if ('' == $key) {
+ return $this->metadata[$domain];
+ }
+
+ if (isset($this->metadata[$domain][$key])) {
+ return $this->metadata[$domain][$key];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMetadata($key, $value, $domain = 'messages')
+ {
+ $this->metadata[$domain][$key] = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteMetadata($key = '', $domain = 'messages')
+ {
+ if ('' == $domain) {
+ $this->metadata = array();
+ } elseif ('' == $key) {
+ unset($this->metadata[$domain]);
+ } else {
+ unset($this->metadata[$domain][$key]);
+ }
+ }
+
+ /**
+ * Adds current values with the new values.
+ *
+ * @param array $values Values to add
+ */
+ private function addMetadata(array $values)
+ {
+ foreach ($values as $domain => $keys) {
+ foreach ($keys as $key => $value) {
+ $this->setMetadata($key, $value, $domain);
+ }
+ }
+ }
+}
--- /dev/null
+<?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\Translation;
+
+use Symfony\Component\Config\Resource\ResourceInterface;
+
+/**
+ * MessageCatalogueInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface MessageCatalogueInterface
+{
+ /**
+ * Gets the catalogue locale.
+ *
+ * @return string The locale
+ *
+ * @api
+ */
+ public function getLocale();
+
+ /**
+ * Gets the domains.
+ *
+ * @return array An array of domains
+ *
+ * @api
+ */
+ public function getDomains();
+
+ /**
+ * Gets the messages within a given domain.
+ *
+ * If $domain is null, it returns all messages.
+ *
+ * @param string $domain The domain name
+ *
+ * @return array An array of messages
+ *
+ * @api
+ */
+ public function all($domain = null);
+
+ /**
+ * Sets a message translation.
+ *
+ * @param string $id The message id
+ * @param string $translation The messages translation
+ * @param string $domain The domain name
+ *
+ * @api
+ */
+ public function set($id, $translation, $domain = 'messages');
+
+ /**
+ * Checks if a message has a translation.
+ *
+ * @param string $id The message id
+ * @param string $domain The domain name
+ *
+ * @return Boolean true if the message has a translation, false otherwise
+ *
+ * @api
+ */
+ public function has($id, $domain = 'messages');
+
+ /**
+ * Checks if a message has a translation (it does not take into account the fallback mechanism).
+ *
+ * @param string $id The message id
+ * @param string $domain The domain name
+ *
+ * @return Boolean true if the message has a translation, false otherwise
+ *
+ * @api
+ */
+ public function defines($id, $domain = 'messages');
+
+ /**
+ * Gets a message translation.
+ *
+ * @param string $id The message id
+ * @param string $domain The domain name
+ *
+ * @return string The message translation
+ *
+ * @api
+ */
+ public function get($id, $domain = 'messages');
+
+ /**
+ * Sets translations for a given domain.
+ *
+ * @param array $messages An array of translations
+ * @param string $domain The domain name
+ *
+ * @api
+ */
+ public function replace($messages, $domain = 'messages');
+
+ /**
+ * Adds translations for a given domain.
+ *
+ * @param array $messages An array of translations
+ * @param string $domain The domain name
+ *
+ * @api
+ */
+ public function add($messages, $domain = 'messages');
+
+ /**
+ * Merges translations from the given Catalogue into the current one.
+ *
+ * The two catalogues must have the same locale.
+ *
+ * @param MessageCatalogueInterface $catalogue A MessageCatalogueInterface instance
+ *
+ * @api
+ */
+ public function addCatalogue(MessageCatalogueInterface $catalogue);
+
+ /**
+ * Merges translations from the given Catalogue into the current one
+ * only when the translation does not exist.
+ *
+ * This is used to provide default translations when they do not exist for the current locale.
+ *
+ * @param MessageCatalogueInterface $catalogue A MessageCatalogueInterface instance
+ *
+ * @api
+ */
+ public function addFallbackCatalogue(MessageCatalogueInterface $catalogue);
+
+ /**
+ * Gets the fallback catalogue.
+ *
+ * @return MessageCatalogueInterface|null A MessageCatalogueInterface instance or null when no fallback has been set
+ *
+ * @api
+ */
+ public function getFallbackCatalogue();
+
+ /**
+ * Returns an array of resources loaded to build this collection.
+ *
+ * @return ResourceInterface[] An array of resources
+ *
+ * @api
+ */
+ public function getResources();
+
+ /**
+ * Adds a resource for this collection.
+ *
+ * @param ResourceInterface $resource A resource instance
+ *
+ * @api
+ */
+ public function addResource(ResourceInterface $resource);
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * MessageSelector.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class MessageSelector
+{
+ /**
+ * Given a message with different plural translations separated by a
+ * pipe (|), this method returns the correct portion of the message based
+ * on the given number, locale and the pluralization rules in the message
+ * itself.
+ *
+ * The message supports two different types of pluralization rules:
+ *
+ * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
+ * indexed: There is one apple|There are %count% apples
+ *
+ * The indexed solution can also contain labels (e.g. one: There is one apple).
+ * This is purely for making the translations more clear - it does not
+ * affect the functionality.
+ *
+ * The two methods can also be mixed:
+ * {0} There are no apples|one: There is one apple|more: There are %count% apples
+ *
+ * @param string $message The message being translated
+ * @param integer $number The number of items represented for the message
+ * @param string $locale The locale to use for choosing
+ *
+ * @return string
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @api
+ */
+ public function choose($message, $number, $locale)
+ {
+ $parts = explode('|', $message);
+ $explicitRules = array();
+ $standardRules = array();
+ foreach ($parts as $part) {
+ $part = trim($part);
+
+ if (preg_match('/^(?P<interval>'.Interval::getIntervalRegexp().')\s*(?P<message>.*?)$/x', $part, $matches)) {
+ $explicitRules[$matches['interval']] = $matches['message'];
+ } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) {
+ $standardRules[] = $matches[1];
+ } else {
+ $standardRules[] = $part;
+ }
+ }
+
+ // try to match an explicit rule, then fallback to the standard ones
+ foreach ($explicitRules as $interval => $m) {
+ if (Interval::test($number, $interval)) {
+ return $m;
+ }
+ }
+
+ $position = PluralizationRules::get($number, $locale);
+ if (!isset($standardRules[$position])) {
+ throw new \InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale));
+ }
+
+ return $standardRules[$position];
+ }
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * MetadataAwareInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+interface MetadataAwareInterface
+{
+ /**
+ * Gets metadata for the given domain and key.
+ *
+ * Passing an empty domain will return an array with all metadata indexed by
+ * domain and then by key. Passing an empty key will return an array with all
+ * metadata for the given domain.
+ *
+ * @param string $domain The domain name
+ * @param string $key The key
+ *
+ * @return mixed The value that was set or an array with the domains/keys or null
+ */
+ public function getMetadata($key = '', $domain = 'messages');
+
+ /**
+ * Adds metadata to a message domain.
+ *
+ * @param string $key The key
+ * @param mixed $value The value
+ * @param string $domain The domain name
+ */
+ public function setMetadata($key, $value, $domain = 'messages');
+
+ /**
+ * Deletes metadata for the given key and domain.
+ *
+ * Passing an empty domain will delete all metadata. Passing an empty key will
+ * delete all metadata for the given domain.
+ *
+ * @param string $domain The domain name
+ * @param string $key The key
+ */
+ public function deleteMetadata($key = '', $domain = 'messages');
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * Returns the plural rules for a given locale.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class PluralizationRules
+{
+ // @codeCoverageIgnoreStart
+ private static $rules = array();
+
+ /**
+ * Returns the plural position to use for the given locale and number.
+ *
+ * @param integer $number The number
+ * @param string $locale The locale
+ *
+ * @return integer The plural position
+ */
+ public static function get($number, $locale)
+ {
+ if ("pt_BR" == $locale) {
+ // temporary set a locale for brazilian
+ $locale = "xbr";
+ }
+
+ if (strlen($locale) > 3) {
+ $locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
+ }
+
+ if (isset(self::$rules[$locale])) {
+ $return = call_user_func(self::$rules[$locale], $number);
+
+ if (!is_int($return) || $return < 0) {
+ return 0;
+ }
+
+ return $return;
+ }
+
+ /*
+ * The plural rules are derived from code of the Zend Framework (2010-09-25),
+ * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
+ * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ */
+ switch ($locale) {
+ case 'bo':
+ case 'dz':
+ case 'id':
+ case 'ja':
+ case 'jv':
+ case 'ka':
+ case 'km':
+ case 'kn':
+ case 'ko':
+ case 'ms':
+ case 'th':
+ case 'tr':
+ case 'vi':
+ case 'zh':
+ return 0;
+ break;
+
+ case 'af':
+ case 'az':
+ case 'bn':
+ case 'bg':
+ case 'ca':
+ case 'da':
+ case 'de':
+ case 'el':
+ case 'en':
+ case 'eo':
+ case 'es':
+ case 'et':
+ case 'eu':
+ case 'fa':
+ case 'fi':
+ case 'fo':
+ case 'fur':
+ case 'fy':
+ case 'gl':
+ case 'gu':
+ case 'ha':
+ case 'he':
+ case 'hu':
+ case 'is':
+ case 'it':
+ case 'ku':
+ case 'lb':
+ case 'ml':
+ case 'mn':
+ case 'mr':
+ case 'nah':
+ case 'nb':
+ case 'ne':
+ case 'nl':
+ case 'nn':
+ case 'no':
+ case 'om':
+ case 'or':
+ case 'pa':
+ case 'pap':
+ case 'ps':
+ case 'pt':
+ case 'so':
+ case 'sq':
+ case 'sv':
+ case 'sw':
+ case 'ta':
+ case 'te':
+ case 'tk':
+ case 'ur':
+ case 'zu':
+ return ($number == 1) ? 0 : 1;
+
+ case 'am':
+ case 'bh':
+ case 'fil':
+ case 'fr':
+ case 'gun':
+ case 'hi':
+ case 'ln':
+ case 'mg':
+ case 'nso':
+ case 'xbr':
+ case 'ti':
+ case 'wa':
+ return (($number == 0) || ($number == 1)) ? 0 : 1;
+
+ case 'be':
+ case 'bs':
+ case 'hr':
+ case 'ru':
+ case 'sr':
+ case 'uk':
+ return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
+
+ case 'cs':
+ case 'sk':
+ return ($number == 1) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2);
+
+ case 'ga':
+ return ($number == 1) ? 0 : (($number == 2) ? 1 : 2);
+
+ case 'lt':
+ return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
+
+ case 'sl':
+ return ($number % 100 == 1) ? 0 : (($number % 100 == 2) ? 1 : ((($number % 100 == 3) || ($number % 100 == 4)) ? 2 : 3));
+
+ case 'mk':
+ return ($number % 10 == 1) ? 0 : 1;
+
+ case 'mt':
+ return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3));
+
+ case 'lv':
+ return ($number == 0) ? 0 : ((($number % 10 == 1) && ($number % 100 != 11)) ? 1 : 2);
+
+ case 'pl':
+ return ($number == 1) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2);
+
+ case 'cy':
+ return ($number == 1) ? 0 : (($number == 2) ? 1 : ((($number == 8) || ($number == 11)) ? 2 : 3));
+
+ case 'ro':
+ return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2);
+
+ case 'ar':
+ return ($number == 0) ? 0 : (($number == 1) ? 1 : (($number == 2) ? 2 : ((($number >= 3) && ($number <= 10)) ? 3 : ((($number >= 11) && ($number <= 99)) ? 4 : 5))));
+
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Overrides the default plural rule for a given locale.
+ *
+ * @param string $rule A PHP callable
+ * @param string $locale The locale
+ *
+ * @return null
+ *
+ * @throws \LogicException
+ */
+ public static function set($rule, $locale)
+ {
+ if ("pt_BR" == $locale) {
+ // temporary set a locale for brazilian
+ $locale = "xbr";
+ }
+
+ if (strlen($locale) > 3) {
+ $locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
+ }
+
+ if (!is_callable($rule)) {
+ throw new \LogicException('The given rule can not be called');
+ }
+
+ self::$rules[$locale] = $rule;
+ }
+
+ // @codeCoverageIgnoreEnd
+}
--- /dev/null
+Translation Component
+=====================
+
+Translation provides tools for loading translation files and generating
+translated strings from these including support for pluralization.
+
+ use Symfony\Component\Translation\Translator;
+ use Symfony\Component\Translation\MessageSelector;
+ use Symfony\Component\Translation\Loader\ArrayLoader;
+
+ $translator = new Translator('fr_FR', new MessageSelector());
+ $translator->setFallbackLocales(array('fr'));
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array(
+ 'Hello World!' => 'Bonjour',
+ ), 'fr');
+
+ echo $translator->trans('Hello World!')."\n";
+
+Resources
+---------
+
+Silex integration:
+
+https://github.com/fabpot/Silex/blob/master/src/Silex/Provider/TranslationServiceProvider.php
+
+Documentation:
+
+http://symfony.com/doc/2.3/book/translation.html
+
+You can run the unit tests with the following command:
+
+ $ cd path/to/Symfony/Component/Translation/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+<?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\Translation\Test\Catalogue;
+
+use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\MessageCatalogueInterface;
+
+abstract class AbstractOperationTest extends TestCase
+{
+ public function testGetEmptyDomains()
+ {
+ $this->assertEquals(
+ array(),
+ $this->createOperation(
+ new MessageCatalogue('en'),
+ new MessageCatalogue('en')
+ )->getDomains()
+ );
+ }
+
+ public function testGetMergedDomains()
+ {
+ $this->assertEquals(
+ array('a', 'b', 'c'),
+ $this->createOperation(
+ new MessageCatalogue('en', array('a' => array(), 'b' => array())),
+ new MessageCatalogue('en', array('b' => array(), 'c' => array()))
+ )->getDomains()
+ );
+ }
+
+ public function testGetMessagesFromUnknownDomain()
+ {
+ $this->setExpectedException('InvalidArgumentException');
+ $this->createOperation(
+ new MessageCatalogue('en'),
+ new MessageCatalogue('en')
+ )->getMessages('domain');
+ }
+
+ public function testGetEmptyMessages()
+ {
+ $this->assertEquals(
+ array(),
+ $this->createOperation(
+ new MessageCatalogue('en', array('a' => array())),
+ new MessageCatalogue('en')
+ )->getMessages('a')
+ );
+ }
+
+ public function testGetEmptyResult()
+ {
+ $this->assertEquals(
+ new MessageCatalogue('en'),
+ $this->createOperation(
+ new MessageCatalogue('en'),
+ new MessageCatalogue('en')
+ )->getResult()
+ );
+ }
+
+ abstract protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target);
+}
--- /dev/null
+<?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\Translation\Test\Catalogue;
+
+use Symfony\Component\Translation\Catalogue\DiffOperation;
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\MessageCatalogueInterface;
+
+class DiffOperationTest extends AbstractOperationTest
+{
+ public function testGetMessagesFromSingleDomain()
+ {
+ $operation = $this->createOperation(
+ new MessageCatalogue('en', array('messages' => array('a' => 'old_a', 'b' => 'old_b'))),
+ new MessageCatalogue('en', array('messages' => array('a' => 'new_a', 'c' => 'new_c')))
+ );
+
+ $this->assertEquals(
+ array('a' => 'old_a', 'c' => 'new_c'),
+ $operation->getMessages('messages')
+ );
+
+ $this->assertEquals(
+ array('c' => 'new_c'),
+ $operation->getNewMessages('messages')
+ );
+
+ $this->assertEquals(
+ array('b' => 'old_b'),
+ $operation->getObsoleteMessages('messages')
+ );
+ }
+
+ public function testGetResultFromSingleDomain()
+ {
+ $this->assertEquals(
+ new MessageCatalogue('en', array(
+ 'messages' => array('a' => 'old_a', 'c' => 'new_c')
+ )),
+ $this->createOperation(
+ new MessageCatalogue('en', array('messages' => array('a' => 'old_a', 'b' => 'old_b'))),
+ new MessageCatalogue('en', array('messages' => array('a' => 'new_a', 'c' => 'new_c')))
+ )->getResult()
+ );
+ }
+
+ protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
+ {
+ return new DiffOperation($source, $target);
+ }
+}
--- /dev/null
+<?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\Translation\Test\Catalogue;
+
+use Symfony\Component\Translation\Catalogue\MergeOperation;
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\MessageCatalogueInterface;
+
+class MergeOperationTest extends AbstractOperationTest
+{
+ public function testGetMessagesFromSingleDomain()
+ {
+ $operation = $this->createOperation(
+ new MessageCatalogue('en', array('messages' => array('a' => 'old_a', 'b' => 'old_b'))),
+ new MessageCatalogue('en', array('messages' => array('a' => 'new_a', 'c' => 'new_c')))
+ );
+
+ $this->assertEquals(
+ array('a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c'),
+ $operation->getMessages('messages')
+ );
+
+ $this->assertEquals(
+ array('c' => 'new_c'),
+ $operation->getNewMessages('messages')
+ );
+
+ $this->assertEquals(
+ array(),
+ $operation->getObsoleteMessages('messages')
+ );
+ }
+
+ public function testGetResultFromSingleDomain()
+ {
+ $this->assertEquals(
+ new MessageCatalogue('en', array(
+ 'messages' => array('a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c')
+ )),
+ $this->createOperation(
+ new MessageCatalogue('en', array('messages' => array('a' => 'old_a', 'b' => 'old_b'))),
+ new MessageCatalogue('en', array('messages' => array('a' => 'new_a', 'c' => 'new_c')))
+ )->getResult()
+ );
+ }
+
+ protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
+ {
+ return new MergeOperation($source, $target);
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\CsvFileDumper;
+
+class CsvFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar', 'bar' => 'foo
+foo', 'foo;foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new CsvFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/valid.csv'), file_get_contents($tempDir.'/messages.en.csv'));
+
+ unlink($tempDir.'/messages.en.csv');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\IcuResFileDumper;
+
+class IcuResFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ if (!extension_loaded('mbstring')) {
+ $this->markTestSkipped('This test requires mbstring to work.');
+ }
+
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new IcuResFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resourcebundle/res/en.res'), file_get_contents($tempDir.'/messages/en.res'));
+
+ unlink($tempDir.'/messages/en.res');
+ rmdir($tempDir.'/messages');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\IniFileDumper;
+
+class IniFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new IniFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.ini'), file_get_contents($tempDir.'/messages.en.ini'));
+
+ unlink($tempDir.'/messages.en.ini');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\MoFileDumper;
+
+class MoFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new MoFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.mo'), file_get_contents($tempDir.'/messages.en.mo'));
+
+ unlink($tempDir.'/messages.en.mo');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\PhpFileDumper;
+
+class PhpFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new PhpFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.php'), file_get_contents($tempDir.'/messages.en.php'));
+
+ unlink($tempDir.'/messages.en.php');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\PoFileDumper;
+
+class PoFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new PoFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.po'), file_get_contents($tempDir.'/messages.en.po'));
+
+ unlink($tempDir.'/messages.en.po');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\QtFileDumper;
+
+class QtFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'), 'resources');
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new QtFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.ts'), file_get_contents($tempDir.'/resources.en.ts'));
+
+ unlink($tempDir.'/resources.en.ts');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\XliffFileDumper;
+
+class XliffFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar', 'key' => ''));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new XliffFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources-clean.xlf'), file_get_contents($tempDir.'/messages.en.xlf'));
+
+ unlink($tempDir.'/messages.en.xlf');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Dumper;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\YamlFileDumper;
+
+class YamlFileDumperTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Yaml\Yaml')) {
+ $this->markTestSkipped('The "Yaml" component is not available');
+ }
+ }
+
+ public function testDump()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->add(array('foo' => 'bar'));
+
+ $tempDir = sys_get_temp_dir();
+ $dumper = new YamlFileDumper();
+ $dumper->dump($catalogue, array('path' => $tempDir));
+
+ $this->assertEquals(file_get_contents(__DIR__.'/../fixtures/resources.yml'), file_get_contents($tempDir.'/messages.en.yml'));
+
+ unlink($tempDir.'/messages.en.yml');
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\IdentityTranslator;
+use Symfony\Component\Translation\MessageSelector;
+
+class IdentityTranslatorTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getTransTests
+ */
+ public function testTrans($expected, $id, $parameters)
+ {
+ $translator = new IdentityTranslator(new MessageSelector());
+
+ $this->assertEquals($expected, $translator->trans($id, $parameters));
+ }
+
+ /**
+ * @dataProvider getTransChoiceTests
+ */
+ public function testTransChoice($expected, $id, $number, $parameters)
+ {
+ $translator = new IdentityTranslator(new MessageSelector());
+
+ $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters));
+ }
+
+ // noop
+ public function testGetSetLocale()
+ {
+ $translator = new IdentityTranslator(new MessageSelector());
+ $translator->setLocale('en');
+ $translator->getLocale();
+ }
+
+ public function getTransTests()
+ {
+ return array(
+ array('Symfony2 is great!', 'Symfony2 is great!', array()),
+ array('Symfony2 is awesome!', 'Symfony2 is %what%!', array('%what%' => 'awesome')),
+ );
+ }
+
+ public function getTransChoiceTests()
+ {
+ return array(
+ array('There is 10 apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 10, array('%count%' => 10)),
+ );
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\Interval;
+
+class IntervalTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getTests
+ */
+ public function testTest($expected, $number, $interval)
+ {
+ $this->assertEquals($expected, Interval::test($number, $interval));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testTestException()
+ {
+ Interval::test(1, 'foobar');
+ }
+
+ public function getTests()
+ {
+ return array(
+ array(true, 3, '{1,2, 3 ,4}'),
+ array(false, 10, '{1,2, 3 ,4}'),
+ array(false, 3, '[1,2]'),
+ array(true, 1, '[1,2]'),
+ array(true, 2, '[1,2]'),
+ array(false, 1, ']1,2['),
+ array(false, 2, ']1,2['),
+ array(true, log(0), '[-Inf,2['),
+ array(true, -log(0), '[-2,+Inf]'),
+ );
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\CsvFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class CsvFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new CsvFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.csv';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadDoesNothingIfEmpty()
+ {
+ $loader = new CsvFileLoader();
+ $resource = __DIR__.'/../fixtures/empty.csv';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array(), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new CsvFileLoader();
+ $resource = __DIR__.'/../fixtures/not-exists.csv';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadNonLocalResource()
+ {
+ $loader = new CsvFileLoader();
+ $resource = 'http://example.com/resources.csv';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\IcuDatFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class IcuDatFileLoaderTest extends LocalizedTestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ if (!extension_loaded('intl')) {
+ $this->markTestSkipped('This test requires intl extension to work.');
+ }
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadInvalidResource()
+ {
+ $loader = new IcuDatFileLoader();
+ $loader->load(__DIR__.'/../fixtures/resourcebundle/corrupted/resources', 'es', 'domain2');
+ }
+
+ public function testDatEnglishLoad()
+ {
+ // bundled resource is build using pkgdata command which at least in ICU 4.2 comes in extremely! buggy form
+ // you must specify an temporary build directory which is not the same as current directory and
+ // MUST reside on the same partition. pkgdata -p resources -T /srv -d.packagelist.txt
+ $loader = new IcuDatFileLoader();
+ $resource = __DIR__.'/../fixtures/resourcebundle/dat/resources';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('symfony' => 'Symfony 2 is great'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource.'.dat')), $catalogue->getResources());
+ }
+
+ public function testDatFrenchLoad()
+ {
+ $loader = new IcuDatFileLoader();
+ $resource = __DIR__.'/../fixtures/resourcebundle/dat/resources';
+ $catalogue = $loader->load($resource, 'fr', 'domain1');
+
+ $this->assertEquals(array('symfony' => 'Symfony 2 est génial'), $catalogue->all('domain1'));
+ $this->assertEquals('fr', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource.'.dat')), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new IcuDatFileLoader();
+ $loader->load(__DIR__.'/../fixtures/non-existing.txt', 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\IcuResFileLoader;
+use Symfony\Component\Config\Resource\DirectoryResource;
+
+class IcuResFileLoaderTest extends LocalizedTestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ if (!extension_loaded('intl')) {
+ $this->markTestSkipped('This test requires intl extension to work.');
+ }
+ }
+
+ public function testLoad()
+ {
+ // resource is build using genrb command
+ $loader = new IcuResFileLoader();
+ $resource = __DIR__.'/../fixtures/resourcebundle/res';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new DirectoryResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new IcuResFileLoader();
+ $loader->load(__DIR__.'/../fixtures/non-existing.txt', 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadInvalidResource()
+ {
+ $loader = new IcuResFileLoader();
+ $loader->load(__DIR__.'/../fixtures/resourcebundle/corrupted', 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\IniFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class IniFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new IniFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.ini';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadDoesNothingIfEmpty()
+ {
+ $loader = new IniFileLoader();
+ $resource = __DIR__.'/../fixtures/empty.ini';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array(), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new IniFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.ini';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+abstract class LocalizedTestCase extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!extension_loaded('intl')) {
+ $this->markTestSkipped('The "intl" extension is not available');
+ }
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\MoFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class MoFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new MoFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.mo';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadPlurals()
+ {
+ $loader = new MoFileLoader();
+ $resource = __DIR__.'/../fixtures/plurals.mo';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar', 'foos' => '{0} bar|{1} bars'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new MoFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.mo';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadInvalidResource()
+ {
+ $loader = new MoFileLoader();
+ $resource = __DIR__.'/../fixtures/empty.mo';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\PhpFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class PhpFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new PhpFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.php';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new PhpFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.php';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadThrowsAnExceptionIfFileNotLocal()
+ {
+ $loader = new PhpFileLoader();
+ $resource = 'http://example.com/resources.php';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\PoFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class PoFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new PoFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.po';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadPlurals()
+ {
+ $loader = new PoFileLoader();
+ $resource = __DIR__.'/../fixtures/plurals.po';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar', 'foos' => 'bar|bars'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadDoesNothingIfEmpty()
+ {
+ $loader = new PoFileLoader();
+ $resource = __DIR__.'/../fixtures/empty.po';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array(), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new PoFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.po';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ public function testLoadEmptyTranslation()
+ {
+ $loader = new PoFileLoader();
+ $resource = __DIR__.'/../fixtures/empty-translation.po';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => ''), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\QtFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class QtFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new QtFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.ts';
+ $catalogue = $loader->load($resource, 'en', 'resources');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('resources'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new QtFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.ts';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadNonLocalResource()
+ {
+ $loader = new QtFileLoader();
+ $resource = 'http://domain1.com/resources.ts';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadInvalidResource()
+ {
+ $loader = new QtFileLoader();
+ $resource = __DIR__.'/../fixtures/invalid-xml-resources.xlf';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\XliffFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class XliffFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new XliffFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.xlf';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadWithResname()
+ {
+ $loader = new XliffFileLoader();
+ $catalogue = $loader->load(__DIR__.'/../fixtures/resname.xlf', 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo'), $catalogue->all('domain1'));
+ }
+
+ public function testIncompleteResource()
+ {
+ $loader = new XliffFileLoader();
+ $catalogue = $loader->load(__DIR__.'/../fixtures/resources.xlf', 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar', 'key' => '', 'test' => 'with'), $catalogue->all('domain1'));
+ $this->assertFalse($catalogue->has('extra', 'domain1'));
+ }
+
+ public function testEncoding()
+ {
+ if (!function_exists('iconv') && !function_exists('mb_convert_encoding')) {
+ $this->markTestSkipped('The iconv and mbstring extensions are not available.');
+ }
+
+ $loader = new XliffFileLoader();
+ $catalogue = $loader->load(__DIR__.'/../fixtures/encoding.xlf', 'en', 'domain1');
+
+ $this->assertEquals(utf8_decode('föö'), $catalogue->get('bar', 'domain1'));
+ $this->assertEquals(utf8_decode('bär'), $catalogue->get('foo', 'domain1'));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadInvalidResource()
+ {
+ $loader = new XliffFileLoader();
+ $loader->load(__DIR__.'/../fixtures/resources.php', 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadResourceDoesNotValidate()
+ {
+ $loader = new XliffFileLoader();
+ $loader->load(__DIR__.'/../fixtures/non-valid.xlf', 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new XliffFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.xlf';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadThrowsAnExceptionIfFileNotLocal()
+ {
+ $loader = new XliffFileLoader();
+ $resource = 'http://example.com/resources.xlf';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ * @expectedExceptionMessage Document types are not allowed.
+ */
+ public function testDocTypeIsNotAllowed()
+ {
+ $loader = new XliffFileLoader();
+ $loader->load(__DIR__.'/../fixtures/withdoctype.xlf', 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests\Loader;
+
+use Symfony\Component\Translation\Loader\YamlFileLoader;
+use Symfony\Component\Config\Resource\FileResource;
+
+class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\Yaml\Yaml')) {
+ $this->markTestSkipped('The "Yaml" component is not available');
+ }
+ }
+
+ public function testLoad()
+ {
+ $loader = new YamlFileLoader();
+ $resource = __DIR__.'/../fixtures/resources.yml';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array('foo' => 'bar'), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ public function testLoadDoesNothingIfEmpty()
+ {
+ $loader = new YamlFileLoader();
+ $resource = __DIR__.'/../fixtures/empty.yml';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertEquals(array(), $catalogue->all('domain1'));
+ $this->assertEquals('en', $catalogue->getLocale());
+ $this->assertEquals(array(new FileResource($resource)), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testLoadNonExistingResource()
+ {
+ $loader = new YamlFileLoader();
+ $resource = __DIR__.'/../fixtures/non-existing.yml';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadThrowsAnExceptionIfFileNotLocal()
+ {
+ $loader = new YamlFileLoader();
+ $resource = 'http://example.com/resources.yml';
+ $loader->load($resource, 'en', 'domain1');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Translation\Exception\InvalidResourceException
+ */
+ public function testLoadThrowsAnExceptionIfNotAnArray()
+ {
+ $loader = new YamlFileLoader();
+ $resource = __DIR__.'/../fixtures/non-valid.yml';
+ $loader->load($resource, 'en', 'domain1');
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\MessageCatalogue;
+
+class MessageCatalogueTest extends \PHPUnit_Framework_TestCase
+{
+ public function testGetLocale()
+ {
+ $catalogue = new MessageCatalogue('en');
+
+ $this->assertEquals('en', $catalogue->getLocale());
+ }
+
+ public function testGetDomains()
+ {
+ $catalogue = new MessageCatalogue('en', array('domain1' => array(), 'domain2' => array()));
+
+ $this->assertEquals(array('domain1', 'domain2'), $catalogue->getDomains());
+ }
+
+ public function testAll()
+ {
+ $catalogue = new MessageCatalogue('en', $messages = array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+
+ $this->assertEquals(array('foo' => 'foo'), $catalogue->all('domain1'));
+ $this->assertEquals(array(), $catalogue->all('domain88'));
+ $this->assertEquals($messages, $catalogue->all());
+ }
+
+ public function testHas()
+ {
+ $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+
+ $this->assertTrue($catalogue->has('foo', 'domain1'));
+ $this->assertFalse($catalogue->has('bar', 'domain1'));
+ $this->assertFalse($catalogue->has('foo', 'domain88'));
+ }
+
+ public function testGetSet()
+ {
+ $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+ $catalogue->set('foo1', 'foo1', 'domain1');
+
+ $this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
+ $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
+ }
+
+ public function testAdd()
+ {
+ $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+ $catalogue->add(array('foo1' => 'foo1'), 'domain1');
+
+ $this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
+ $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
+
+ $catalogue->add(array('foo' => 'bar'), 'domain1');
+ $this->assertEquals('bar', $catalogue->get('foo', 'domain1'));
+ $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
+
+ $catalogue->add(array('foo' => 'bar'), 'domain88');
+ $this->assertEquals('bar', $catalogue->get('foo', 'domain88'));
+ }
+
+ public function testReplace()
+ {
+ $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+ $catalogue->replace($messages = array('foo1' => 'foo1'), 'domain1');
+
+ $this->assertEquals($messages, $catalogue->all('domain1'));
+ }
+
+ public function testAddCatalogue()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r->expects($this->any())->method('__toString')->will($this->returnValue('r'));
+
+ $r1 = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r1->expects($this->any())->method('__toString')->will($this->returnValue('r1'));
+
+ $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+ $catalogue->addResource($r);
+
+ $catalogue1 = new MessageCatalogue('en', array('domain1' => array('foo1' => 'foo1')));
+ $catalogue1->addResource($r1);
+
+ $catalogue->addCatalogue($catalogue1);
+
+ $this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
+ $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
+
+ $this->assertEquals(array($r, $r1), $catalogue->getResources());
+ }
+
+ public function testAddFallbackCatalogue()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r->expects($this->any())->method('__toString')->will($this->returnValue('r'));
+
+ $r1 = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r1->expects($this->any())->method('__toString')->will($this->returnValue('r1'));
+
+ $catalogue = new MessageCatalogue('en_US', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar')));
+ $catalogue->addResource($r);
+
+ $catalogue1 = new MessageCatalogue('en', array('domain1' => array('foo' => 'bar', 'foo1' => 'foo1')));
+ $catalogue1->addResource($r1);
+
+ $catalogue->addFallbackCatalogue($catalogue1);
+
+ $this->assertEquals('foo', $catalogue->get('foo', 'domain1'));
+ $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1'));
+
+ $this->assertEquals(array($r, $r1), $catalogue->getResources());
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testAddFallbackCatalogueWithCircularReference()
+ {
+ $main = new MessageCatalogue('en_US');
+ $fallback = new MessageCatalogue('fr_FR');
+
+ $fallback->addFallbackCatalogue($main);
+ $main->addFallbackCatalogue($fallback);
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testAddCatalogueWhenLocaleIsNotTheSameAsTheCurrentOne()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->addCatalogue(new MessageCatalogue('fr', array()));
+ }
+
+ public function testGetAddResource()
+ {
+ if (!class_exists('Symfony\Component\Config\Loader\Loader')) {
+ $this->markTestSkipped('The "Config" component is not available');
+ }
+
+ $catalogue = new MessageCatalogue('en');
+ $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r->expects($this->any())->method('__toString')->will($this->returnValue('r'));
+ $catalogue->addResource($r);
+ $catalogue->addResource($r);
+ $r1 = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
+ $r1->expects($this->any())->method('__toString')->will($this->returnValue('r1'));
+ $catalogue->addResource($r1);
+
+ $this->assertEquals(array($r, $r1), $catalogue->getResources());
+ }
+
+ public function testMetadataDelete()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $this->assertEquals(array(), $catalogue->getMetadata('', ''), 'Metadata is empty');
+ $catalogue->deleteMetadata('key', 'messages');
+ $catalogue->deleteMetadata('', 'messages');
+ $catalogue->deleteMetadata();
+ }
+
+ public function testMetadataSetGetDelete()
+ {
+ $catalogue = new MessageCatalogue('en');
+ $catalogue->setMetadata('key', 'value');
+ $this->assertEquals('value', $catalogue->getMetadata('key', 'messages'), "Metadata 'key' = 'value'");
+
+ $catalogue->setMetadata('key2', array());
+ $this->assertEquals(array(), $catalogue->getMetadata('key2', 'messages'), 'Metadata key2 is array');
+
+ $catalogue->deleteMetadata('key2', 'messages');
+ $this->assertEquals(null, $catalogue->getMetadata('key2', 'messages'), 'Metadata key2 should is deleted.');
+
+ $catalogue->deleteMetadata('key2', 'domain');
+ $this->assertEquals(null, $catalogue->getMetadata('key2', 'domain'), 'Metadata key2 should is deleted.');
+ }
+
+ public function testMetadataMerge()
+ {
+ $cat1 = new MessageCatalogue('en');
+ $cat1->setMetadata('a', 'b');
+ $this->assertEquals(array('messages' => array('a' => 'b')), $cat1->getMetadata('', ''), 'Cat1 contains messages metadata.');
+
+ $cat2 = new MessageCatalogue('en');
+ $cat2->setMetadata('b', 'c', 'domain');
+ $this->assertEquals(array('domain' => array('b' => 'c')), $cat2->getMetadata('', ''), 'Cat2 contains domain metadata.');
+
+ $cat1->addCatalogue($cat2);
+ $this->assertEquals(array('messages' => array('a' => 'b'), 'domain' => array('b' => 'c')), $cat1->getMetadata('', ''), 'Cat1 contains merged metadata.');
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\MessageSelector;
+
+class MessageSelectorTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getChooseTests
+ */
+ public function testChoose($expected, $id, $number)
+ {
+ $selector = new MessageSelector();
+
+ $this->assertEquals($expected, $selector->choose($id, $number, 'en'));
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testChooseWhenNoEnoughChoices()
+ {
+ $selector = new MessageSelector();
+
+ $selector->choose('foo', 10, 'en');
+ }
+
+ public function getChooseTests()
+ {
+ return array(
+ array('There is no apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0),
+ array('There is no apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0),
+ array('There is no apples', '{0}There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 0),
+
+ array('There is one apple', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 1),
+
+ array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 10),
+ array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf]There is %count% apples', 10),
+ array('There is %count% apples', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', 10),
+
+ array('There is %count% apples', 'There is one apple|There is %count% apples', 0),
+ array('There is one apple', 'There is one apple|There is %count% apples', 1),
+ array('There is %count% apples', 'There is one apple|There is %count% apples', 10),
+
+ array('There is %count% apples', 'one: There is one apple|more: There is %count% apples', 0),
+ array('There is one apple', 'one: There is one apple|more: There is %count% apples', 1),
+ array('There is %count% apples', 'one: There is one apple|more: There is %count% apples', 10),
+
+ array('There is no apples', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 0),
+ array('There is one apple', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 1),
+ array('There is %count% apples', '{0} There is no apples|one: There is one apple|more: There is %count% apples', 10),
+
+ array('', '{0}|{1} There is one apple|]1,Inf] There is %count% apples', 0),
+ array('', '{0} There is no apples|{1}|]1,Inf] There is %count% apples', 1),
+
+ // Indexed only tests which are Gettext PoFile* compatible strings.
+ array('There are %count% apples', 'There is one apple|There are %count% apples', 0),
+ array('There is one apple', 'There is one apple|There are %count% apples', 1),
+ array('There are %count% apples', 'There is one apple|There are %count% apples', 2),
+
+ // Tests for float numbers
+ array('There is almost one apple', '{0} There is no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7),
+ array('There is one apple', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1),
+ array('There is more than one apple', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7),
+ array('There is no apples', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0),
+ array('There is no apples', '{0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0),
+ array('There is no apples', '{0.0} There is no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0),
+ );
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\PluralizationRules;
+
+/**
+ * Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms
+ * and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
+ *
+ * See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms.
+ * The mozilla code is also interesting to check for.
+ *
+ * As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199
+ *
+ * The goal to cover all languages is to far fetched so this test case is smaller.
+ *
+ * @author Clemens Tolboom clemens@build2be.nl
+ */
+class PluralizationRulesTest extends \PHPUnit_Framework_TestCase
+{
+
+ /**
+ * We test failed langcode here.
+ *
+ * TODO: The languages mentioned in the data provide need to get fixed somehow within PluralizationRules.
+ *
+ * @dataProvider failingLangcodes
+ */
+ public function testFailedLangcodes($nplural, $langCodes)
+ {
+ $matrix = $this->generateTestData($nplural, $langCodes);
+ $this->validateMatrix($nplural, $matrix, false);
+ }
+
+ /**
+ * @dataProvider successLangcodes
+ */
+ public function testLangcodes($nplural, $langCodes)
+ {
+ $matrix = $this->generateTestData($nplural, $langCodes);
+ $this->validateMatrix($nplural, $matrix);
+ }
+
+ /**
+ * This array should contain all currently known langcodes.
+ *
+ * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete.
+ *
+ * @return type
+ */
+ public function successLangcodes()
+ {
+ return array(
+ array('1' , array('ay','bo', 'cgg','dz','id', 'ja', 'jbo', 'ka','kk','km','ko','ky')),
+ array('2' , array('nl', 'fr', 'en', 'de', 'de_GE')),
+ array('3' , array('be','bs','cs','hr')),
+ array('4' , array('cy','mt', 'sl')),
+ array('5' , array()),
+ array('6' , array('ar')),
+ );
+ }
+
+ /**
+ * This array should be at least empty within the near future.
+ *
+ * This both depends on a complete list trying to add above as understanding
+ * the plural rules of the current failing languages.
+ *
+ * @return array with nplural together with langcodes
+ */
+ public function failingLangcodes()
+ {
+ return array(
+ array('1' , array('fa')),
+ array('2' , array('jbo')),
+ array('3' , array('cbs')),
+ array('4' , array('gd','kw')),
+ array('5' , array('ga')),
+ array('6' , array()),
+ );
+ }
+
+ /**
+ * We validate only on the plural coverage. Thus the real rules is not tested.
+ *
+ * @param string $nplural plural expected
+ * @param array $matrix containing langcodes and their plural index values.
+ * @param boolean $expectSuccess
+ */
+ protected function validateMatrix($nplural, $matrix, $expectSuccess = true)
+ {
+ foreach ($matrix as $langCode => $data) {
+ $indexes = array_flip($data);
+ if ($expectSuccess) {
+ $this->assertEquals($nplural, count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
+ } else {
+ $this->assertNotEquals((int) $nplural, count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
+ }
+ }
+ }
+
+ protected function generateTestData($plural, $langCodes)
+ {
+ $matrix = array();
+ foreach ($langCodes as $langCode) {
+ for ($count=0; $count<200; $count++) {
+ $plural = PluralizationRules::get($count, $langCode);
+ $matrix[$langCode][$count] = $plural;
+ }
+ }
+
+ return $matrix;
+ }
+}
--- /dev/null
+<?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\Translation\Tests;
+
+use Symfony\Component\Translation\Translator;
+use Symfony\Component\Translation\MessageSelector;
+use Symfony\Component\Translation\Loader\ArrayLoader;
+
+class TranslatorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testSetGetLocale()
+ {
+ $translator = new Translator('en', new MessageSelector());
+
+ $this->assertEquals('en', $translator->getLocale());
+
+ $translator->setLocale('fr');
+ $this->assertEquals('fr', $translator->getLocale());
+ }
+
+ public function testSetFallbackLocales()
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foofoo'), 'en');
+ $translator->addResource('array', array('bar' => 'foobar'), 'fr');
+
+ // force catalogue loading
+ $translator->trans('bar');
+
+ $translator->setFallbackLocales(array('fr'));
+ $this->assertEquals('foobar', $translator->trans('bar'));
+ }
+
+ public function testSetFallbackLocalesMultiple()
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foo (en)'), 'en');
+ $translator->addResource('array', array('bar' => 'bar (fr)'), 'fr');
+
+ // force catalogue loading
+ $translator->trans('bar');
+
+ $translator->setFallbackLocales(array('fr_FR', 'fr'));
+ $this->assertEquals('bar (fr)', $translator->trans('bar'));
+ }
+
+ public function testTransWithFallbackLocale()
+ {
+ $translator = new Translator('fr_FR', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foofoo'), 'en_US');
+ $translator->addResource('array', array('bar' => 'foobar'), 'en');
+
+ $translator->setFallbackLocales(array('en'));
+
+ $this->assertEquals('foobar', $translator->trans('bar'));
+ }
+
+ public function testAddResourceAfterTrans()
+ {
+ $translator = new Translator('fr', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+
+ $translator->setFallbackLocale(array('en'));
+
+ $translator->addResource('array', array('foo' => 'foofoo'), 'en');
+ $this->assertEquals('foofoo', $translator->trans('foo'));
+
+ $translator->addResource('array', array('bar' => 'foobar'), 'en');
+ $this->assertEquals('foobar', $translator->trans('bar'));
+ }
+
+ /**
+ * @dataProvider getTransFileTests
+ * @expectedException \Symfony\Component\Translation\Exception\NotFoundResourceException
+ */
+ public function testTransWithoutFallbackLocaleFile($format, $loader)
+ {
+ $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader;
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader($format, new $loaderClass());
+ $translator->addResource($format, __DIR__.'/fixtures/non-existing', 'en');
+ $translator->addResource($format, __DIR__.'/fixtures/resources.'.$format, 'en');
+
+ // force catalogue loading
+ $translator->trans('foo');
+ }
+
+ /**
+ * @dataProvider getTransFileTests
+ */
+ public function testTransWithFallbackLocaleFile($format, $loader)
+ {
+ $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader;
+ $translator = new Translator('en_GB', new MessageSelector());
+ $translator->addLoader($format, new $loaderClass());
+ $translator->addResource($format, __DIR__.'/fixtures/non-existing', 'en_GB');
+ $translator->addResource($format, __DIR__.'/fixtures/resources.'.$format, 'en', 'resources');
+
+ $this->assertEquals('bar', $translator->trans('foo', array(), 'resources'));
+ }
+
+ public function testTransWithFallbackLocaleBis()
+ {
+ $translator = new Translator('en_US', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foofoo'), 'en_US');
+ $translator->addResource('array', array('bar' => 'foobar'), 'en');
+ $this->assertEquals('foobar', $translator->trans('bar'));
+ }
+
+ public function testTransWithFallbackLocaleTer()
+ {
+ $translator = new Translator('fr_FR', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foo (en_US)'), 'en_US');
+ $translator->addResource('array', array('bar' => 'bar (en)'), 'en');
+
+ $translator->setFallbackLocales(array('en_US', 'en'));
+
+ $this->assertEquals('foo (en_US)', $translator->trans('foo'));
+ $this->assertEquals('bar (en)', $translator->trans('bar'));
+ }
+
+ public function testTransNonExistentWithFallback()
+ {
+ $translator = new Translator('fr', new MessageSelector());
+ $translator->setFallbackLocales(array('en'));
+ $translator->addLoader('array', new ArrayLoader());
+ $this->assertEquals('non-existent', $translator->trans('non-existent'));
+ }
+
+ /**
+ * @expectedException RuntimeException
+ */
+ public function testWhenAResourceHasNoRegisteredLoader()
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addResource('array', array('foo' => 'foofoo'), 'en');
+
+ $translator->trans('foo');
+ }
+
+ /**
+ * @dataProvider getTransTests
+ */
+ public function testTrans($expected, $id, $translation, $parameters, $locale, $domain)
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array((string) $id => $translation), $locale, $domain);
+
+ $this->assertEquals($expected, $translator->trans($id, $parameters, $domain, $locale));
+ }
+
+ /**
+ * @dataProvider getFlattenedTransTests
+ */
+ public function testFlattenedTrans($expected, $messages, $id)
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', $messages, 'fr', '');
+
+ $this->assertEquals($expected, $translator->trans($id, array(), '', 'fr'));
+ }
+
+ /**
+ * @dataProvider getTransChoiceTests
+ */
+ public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain)
+ {
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array((string) $id => $translation), $locale, $domain);
+
+ $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters, $domain, $locale));
+ }
+
+ public function getTransFileTests()
+ {
+ return array(
+ array('csv', 'CsvFileLoader'),
+ array('ini', 'IniFileLoader'),
+ array('mo', 'MoFileLoader'),
+ array('po', 'PoFileLoader'),
+ array('php', 'PhpFileLoader'),
+ array('ts', 'QtFileLoader'),
+ array('xlf', 'XliffFileLoader'),
+ array('yml', 'YamlFileLoader'),
+ );
+ }
+
+ public function getTransTests()
+ {
+ return array(
+ array('Symfony2 est super !', 'Symfony2 is great!', 'Symfony2 est super !', array(), 'fr', ''),
+ array('Symfony2 est awesome !', 'Symfony2 is %what%!', 'Symfony2 est %what% !', array('%what%' => 'awesome'), 'fr', ''),
+ array('Symfony2 est super !', new String('Symfony2 is great!'), 'Symfony2 est super !', array(), 'fr', ''),
+ );
+ }
+
+ public function getFlattenedTransTests()
+ {
+ $messages = array(
+ 'symfony2' => array(
+ 'is' => array(
+ 'great' => 'Symfony2 est super!'
+ )
+ ),
+ 'foo' => array(
+ 'bar' => array(
+ 'baz' => 'Foo Bar Baz'
+ ),
+ 'baz' => 'Foo Baz',
+ ),
+ );
+
+ return array(
+ array('Symfony2 est super!', $messages, 'symfony2.is.great'),
+ array('Foo Bar Baz', $messages, 'foo.bar.baz'),
+ array('Foo Baz', $messages, 'foo.baz'),
+ );
+ }
+
+ public function getTransChoiceTests()
+ {
+ return array(
+ array('Il y a 0 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
+ array('Il y a 1 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
+ array('Il y a 10 pommes', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
+
+ array('Il y a 0 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
+ array('Il y a 1 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
+ array('Il y a 10 pommes', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
+
+ array('Il y a 0 pomme', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
+ array('Il y a 1 pomme', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
+ array('Il y a 10 pommes', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
+
+ array('Il n\'y a aucune pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
+ array('Il y a 1 pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
+ array('Il y a 10 pommes', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
+
+ array('Il y a 0 pomme', new String('{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples'), '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
+ );
+ }
+
+ public function testTransChoiceFallback()
+ {
+ $translator = new Translator('ru', new MessageSelector());
+ $translator->setFallbackLocales(array('en'));
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('some_message2' => 'one thing|%count% things'), 'en');
+
+ $this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
+ }
+
+ public function testTransChoiceFallbackBis()
+ {
+ $translator = new Translator('ru', new MessageSelector());
+ $translator->setFallbackLocales(array('en_US', 'en'));
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('some_message2' => 'one thing|%count% things'), 'en_US');
+
+ $this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testTransChoiceFallbackWithNoTranslation()
+ {
+ $translator = new Translator('ru', new MessageSelector());
+ $translator->setFallbackLocales(array('en'));
+ $translator->addLoader('array', new ArrayLoader());
+
+ $this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
+ }
+}
+
+class String
+{
+ protected $str;
+
+ public function __construct($str)
+ {
+ $this->str = $str;
+ }
+
+ public function __toString()
+ {
+ return $this->str;
+ }
+}
--- /dev/null
+msgid "foo"
+msgstr ""
+
--- /dev/null
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="1" resname="foo">
+ <source>foo</source>
+ <target>bär</target>
+ </trans-unit>
+ <trans-unit id="2" resname="bar">
+ <source>bar</source>
+ <target>föö</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="1">
+ <source>foo</source>
+ <target>bar
+ </trans-unit>
+ <trans-unit id="2">
+ <source>extra</source>
+ </trans-unit>
+ <trans-unit id="3">
+ <source>key</source>
+ <target></target>
+ </trans-unit>
+ <trans-unit id="4">
+ <source>test</source>
+ <target>with</target>
+ <note>note</note>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?xml version="1.0"?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit>
+ <source>foo</source>
+ <target>bar</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+msgid "foo"
+msgid_plural "foos"
+msgstr[0] "bar"
+msgstr[1] "bars"
+
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="1" resname="foo">
+ <source></source>
+ <target>bar</target>
+ </trans-unit>
+ <trans-unit id="2" resname="bar">
+ <source>bar source</source>
+ <target>baz</target>
+ </trans-unit>
+ <trans-unit id="3">
+ <source>baz</source>
+ <target>foo</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+XXX
\ No newline at end of file
--- /dev/null
+en{
+ symfony{"Symfony 2 is great"}
+}
\ No newline at end of file
--- /dev/null
+fr{
+ symfony{"Symfony 2 est génial"}
+}
\ No newline at end of file
--- /dev/null
+en.res
+fr.res
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="acbd18db4cc2f85cedef654fccc4a4d8" resname="foo">
+ <source>foo</source>
+ <target>bar</target>
+ </trans-unit>
+ <trans-unit id="3c6e0b8a9c15224a8228b9a98ca1531d" resname="key">
+ <source>key</source>
+ <target></target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+"foo"; "bar"
+#"bar"; "foo"
+"incorrect"; "number"; "columns"; "will"; "be"; "ignored"
+"incorrect"
\ No newline at end of file
--- /dev/null
+<?php
+
+return array (
+ 'foo' => 'bar',
+);
--- /dev/null
+msgid "foo"
+msgstr "bar"
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<TS>
+ <context>
+ <name>resources</name>
+ <message>
+ <source>foo</source>
+ <translation>bar</translation>
+ </message>
+ </context>
+</TS>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="1">
+ <source>foo</source>
+ <target>bar</target>
+ </trans-unit>
+ <trans-unit id="2">
+ <source>extra</source>
+ </trans-unit>
+ <trans-unit id="3">
+ <source>key</source>
+ <target></target>
+ </trans-unit>
+ <trans-unit id="4">
+ <source>test</source>
+ <target>with</target>
+ <note>note</note>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+foo;bar
+bar;"foo
+foo"
+"foo;foo";bar
--- /dev/null
+<?xml version="1.0"?>
+<!DOCTYPE foo>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+ <file source-language="en" datatype="plaintext" original="file.ext">
+ <body>
+ <trans-unit id="1">
+ <source>foo</source>
+ <target>bar</target>
+ </trans-unit>
+ </body>
+ </file>
+</xliff>
--- /dev/null
+<?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\Translation;
+
+use Symfony\Component\Translation\Loader\LoaderInterface;
+use Symfony\Component\Translation\Exception\NotFoundResourceException;
+
+/**
+ * Translator.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class Translator implements TranslatorInterface
+{
+ /**
+ * @var MessageCatalogueInterface[]
+ */
+ protected $catalogues = array();
+
+ /**
+ * @var string
+ */
+ protected $locale;
+
+ /**
+ * @var array
+ */
+ private $fallbackLocales = array();
+
+ /**
+ * @var LoaderInterface[]
+ */
+ private $loaders = array();
+
+ /**
+ * @var array
+ */
+ private $resources = array();
+
+ /**
+ * @var MessageSelector
+ */
+ private $selector;
+
+ /**
+ * Constructor.
+ *
+ * @param string $locale The locale
+ * @param MessageSelector|null $selector The message selector for pluralization
+ *
+ * @api
+ */
+ public function __construct($locale, MessageSelector $selector = null)
+ {
+ $this->locale = $locale;
+ $this->selector = $selector ?: new MessageSelector();
+ }
+
+ /**
+ * Adds a Loader.
+ *
+ * @param string $format The name of the loader (@see addResource())
+ * @param LoaderInterface $loader A LoaderInterface instance
+ *
+ * @api
+ */
+ public function addLoader($format, LoaderInterface $loader)
+ {
+ $this->loaders[$format] = $loader;
+ }
+
+ /**
+ * Adds a Resource.
+ *
+ * @param string $format The name of the loader (@see addLoader())
+ * @param mixed $resource The resource name
+ * @param string $locale The locale
+ * @param string $domain The domain
+ *
+ * @api
+ */
+ public function addResource($format, $resource, $locale, $domain = null)
+ {
+ if (null === $domain) {
+ $domain = 'messages';
+ }
+
+ $this->resources[$locale][] = array($format, $resource, $domain);
+
+ if (in_array($locale, $this->fallbackLocales)) {
+ $this->catalogues = array();
+ } else {
+ unset($this->catalogues[$locale]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function setLocale($locale)
+ {
+ $this->locale = $locale;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function getLocale()
+ {
+ return $this->locale;
+ }
+
+ /**
+ * Sets the fallback locale(s).
+ *
+ * @param string|array $locales The fallback locale(s)
+ *
+ * @deprecated since 2.3, to be removed in 3.0. Use setFallbackLocales() instead.
+ *
+ * @api
+ */
+ public function setFallbackLocale($locales)
+ {
+ $this->setFallbackLocales(is_array($locales) ? $locales : array($locales));
+ }
+
+ /**
+ * Sets the fallback locales.
+ *
+ * @param array $locales The fallback locales
+ *
+ * @api
+ */
+ public function setFallbackLocales(array $locales)
+ {
+ // needed as the fallback locales are linked to the already loaded catalogues
+ $this->catalogues = array();
+
+ $this->fallbackLocales = $locales;
+ }
+
+ /**
+ * Gets the fallback locales.
+ *
+ * @return array $locales The fallback locales
+ *
+ * @api
+ */
+ public function getFallbackLocales()
+ {
+ return $this->fallbackLocales;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function trans($id, array $parameters = array(), $domain = null, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = $this->getLocale();
+ }
+
+ if (null === $domain) {
+ $domain = 'messages';
+ }
+
+ if (!isset($this->catalogues[$locale])) {
+ $this->loadCatalogue($locale);
+ }
+
+ return strtr($this->catalogues[$locale]->get((string) $id, $domain), $parameters);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @api
+ */
+ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
+ {
+ if (null === $locale) {
+ $locale = $this->getLocale();
+ }
+
+ if (null === $domain) {
+ $domain = 'messages';
+ }
+
+ if (!isset($this->catalogues[$locale])) {
+ $this->loadCatalogue($locale);
+ }
+
+ $id = (string) $id;
+
+ $catalogue = $this->catalogues[$locale];
+ while (!$catalogue->defines($id, $domain)) {
+ if ($cat = $catalogue->getFallbackCatalogue()) {
+ $catalogue = $cat;
+ $locale = $catalogue->getLocale();
+ } else {
+ break;
+ }
+ }
+
+ return strtr($this->selector->choose($catalogue->get($id, $domain), (int) $number, $locale), $parameters);
+ }
+
+ protected function loadCatalogue($locale)
+ {
+ try {
+ $this->doLoadCatalogue($locale);
+ } catch (NotFoundResourceException $e) {
+ if (!$this->computeFallbackLocales($locale)) {
+ throw $e;
+ }
+ }
+ $this->loadFallbackCatalogues($locale);
+ }
+
+ private function doLoadCatalogue($locale)
+ {
+ $this->catalogues[$locale] = new MessageCatalogue($locale);
+
+ if (isset($this->resources[$locale])) {
+ foreach ($this->resources[$locale] as $resource) {
+ if (!isset($this->loaders[$resource[0]])) {
+ throw new \RuntimeException(sprintf('The "%s" translation loader is not registered.', $resource[0]));
+ }
+ $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
+ }
+ }
+ }
+
+ private function loadFallbackCatalogues($locale)
+ {
+ $current = $this->catalogues[$locale];
+
+ foreach ($this->computeFallbackLocales($locale) as $fallback) {
+ if (!isset($this->catalogues[$fallback])) {
+ $this->doLoadCatalogue($fallback);
+ }
+
+ $current->addFallbackCatalogue($this->catalogues[$fallback]);
+ $current = $this->catalogues[$fallback];
+ }
+ }
+
+ protected function computeFallbackLocales($locale)
+ {
+ $locales = array();
+ foreach ($this->fallbackLocales as $fallback) {
+ if ($fallback === $locale) {
+ continue;
+ }
+
+ $locales[] = $fallback;
+ }
+
+ if (strrchr($locale, '_') !== false) {
+ array_unshift($locales, substr($locale, 0, -strlen(strrchr($locale, '_'))));
+ }
+
+ return array_unique($locales);
+ }
+}
--- /dev/null
+<?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\Translation;
+
+/**
+ * TranslatorInterface.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+interface TranslatorInterface
+{
+ /**
+ * Translates the given message.
+ *
+ * @param string $id The message id (may also be an object that can be cast to string)
+ * @param array $parameters An array of parameters for the message
+ * @param string $domain The domain for the message
+ * @param string $locale The locale
+ *
+ * @return string The translated string
+ *
+ * @api
+ */
+ public function trans($id, array $parameters = array(), $domain = null, $locale = null);
+
+ /**
+ * Translates the given choice message by choosing a translation according to a number.
+ *
+ * @param string $id The message id (may also be an object that can be cast to string)
+ * @param integer $number The number to use to find the indice of the message
+ * @param array $parameters An array of parameters for the message
+ * @param string $domain The domain for the message
+ * @param string $locale The locale
+ *
+ * @return string The translated string
+ *
+ * @api
+ */
+ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);
+
+ /**
+ * Sets the current locale.
+ *
+ * @param string $locale The locale
+ *
+ * @api
+ */
+ public function setLocale($locale);
+
+ /**
+ * Returns the current locale.
+ *
+ * @return string The locale
+ *
+ * @api
+ */
+ public function getLocale();
+}
--- /dev/null
+<?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\Translation\Writer;
+
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Component\Translation\Dumper\DumperInterface;
+
+/**
+ * TranslationWriter writes translation messages.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ */
+class TranslationWriter
+{
+ /**
+ * Dumpers used for export.
+ *
+ * @var array
+ */
+ private $dumpers = array();
+
+ /**
+ * Adds a dumper to the writer.
+ *
+ * @param string $format The format of the dumper
+ * @param DumperInterface $dumper The dumper
+ */
+ public function addDumper($format, DumperInterface $dumper)
+ {
+ $this->dumpers[$format] = $dumper;
+ }
+
+ /**
+ * Obtains the list of supported formats.
+ *
+ * @return array
+ */
+ public function getFormats()
+ {
+ return array_keys($this->dumpers);
+ }
+
+ /**
+ * Writes translation from the catalogue according to the selected format.
+ *
+ * @param MessageCatalogue $catalogue The message catalogue to dump
+ * @param string $format The format to use to dump the messages
+ * @param array $options Options that are passed to the dumper
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function writeTranslations(MessageCatalogue $catalogue, $format, $options = array())
+ {
+ if (!isset($this->dumpers[$format])) {
+ throw new \InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format));
+ }
+
+ // get the right dumper
+ $dumper = $this->dumpers[$format];
+
+ // save
+ $dumper->dump($catalogue, $options);
+ }
+}
--- /dev/null
+{
+ "name": "symfony/translation",
+ "type": "library",
+ "description": "Symfony Translation 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"
+ },
+ "require-dev": {
+ "symfony/config": "~2.0",
+ "symfony/yaml": "~2.2"
+ },
+ "suggest": {
+ "symfony/config": "",
+ "symfony/yaml": ""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Component\\Translation\\": "" }
+ },
+ "target-dir": "Symfony/Component/Translation",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Translation Component Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./vendor</directory>
+ <directory>./Tests</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+vendor/
+composer.lock
+phpunit.xml
+
--- /dev/null
+CHANGELOG
+=========
+
+2.3.0
+-----
+
+ * added helpers form(), form_start() and form_end()
+ * deprecated form_enctype() in favor of form_start()
+
+2.2.0
+-----
+
+ * added a `controller` function to help generating controller references
+ * added a `render_esi` and a `render_hinclude` function
+ * [BC BREAK] restricted the `render` tag to only accept URIs or ControllerReference instances (the signature changed)
+ * added a `render` function to render a request
+ * The `app` global variable is now injected even when using the twig service directly.
+ * Added an optional parameter to the `path` and `url` function which allows to generate
+ relative paths (e.g. "../parent-file") and scheme-relative URLs (e.g. "//example.com/dir/file").
+
+2.1.0
+-----
+
+ * added global variables access in a form theme
+ * added TwigEngine
+ * added TwigExtractor
+ * added a csrf_token function
+ * added a way to specify a default domain for a Twig template (via the
+ 'trans_default_domain' tag)
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+if (!defined('ENT_SUBSTITUTE')) {
+ define('ENT_SUBSTITUTE', 8);
+}
+
+/**
+ * Twig extension relate to PHP code and used by the profiler and the default exception templates.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class CodeExtension extends \Twig_Extension
+{
+ private $fileLinkFormat;
+ private $rootDir;
+ private $charset;
+
+ /**
+ * Constructor.
+ *
+ * @param string $fileLinkFormat The format for links to source files
+ * @param string $rootDir The project root directory
+ * @param string $charset The charset
+ */
+ public function __construct($fileLinkFormat, $rootDir, $charset)
+ {
+ $this->fileLinkFormat = empty($fileLinkFormat) ? ini_get('xdebug.file_link_format') : $fileLinkFormat;
+ $this->rootDir = str_replace('\\', '/', $rootDir).'/';
+ $this->charset = $charset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters()
+ {
+ return array(
+ 'abbr_class' => new \Twig_Filter_Method($this, 'abbrClass', array('is_safe' => array('html'))),
+ 'abbr_method' => new \Twig_Filter_Method($this, 'abbrMethod', array('is_safe' => array('html'))),
+ 'format_args' => new \Twig_Filter_Method($this, 'formatArgs', array('is_safe' => array('html'))),
+ 'format_args_as_text' => new \Twig_Filter_Method($this, 'formatArgsAsText'),
+ 'file_excerpt' => new \Twig_Filter_Method($this, 'fileExcerpt', array('is_safe' => array('html'))),
+ 'format_file' => new \Twig_Filter_Method($this, 'formatFile', array('is_safe' => array('html'))),
+ 'format_file_from_text' => new \Twig_Filter_Method($this, 'formatFileFromText', array('is_safe' => array('html'))),
+ 'file_link' => new \Twig_Filter_Method($this, 'getFileLink', array('is_safe' => array('html'))),
+ );
+ }
+
+ public function abbrClass($class)
+ {
+ $parts = explode('\\', $class);
+ $short = array_pop($parts);
+
+ return sprintf("<abbr title=\"%s\">%s</abbr>", $class, $short);
+ }
+
+ public function abbrMethod($method)
+ {
+ if (false !== strpos($method, '::')) {
+ list($class, $method) = explode('::', $method, 2);
+ $result = sprintf("%s::%s()", $this->abbrClass($class), $method);
+ } elseif ('Closure' === $method) {
+ $result = sprintf("<abbr title=\"%s\">%s</abbr>", $method, $method);
+ } else {
+ $result = sprintf("<abbr title=\"%s\">%s</abbr>()", $method, $method);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Formats an array as a string.
+ *
+ * @param array $args The argument array
+ *
+ * @return string
+ */
+ public function formatArgs($args)
+ {
+ $result = array();
+ foreach ($args as $key => $item) {
+ if ('object' === $item[0]) {
+ $parts = explode('\\', $item[1]);
+ $short = array_pop($parts);
+ $formattedValue = sprintf("<em>object</em>(<abbr title=\"%s\">%s</abbr>)", $item[1], $short);
+ } elseif ('array' === $item[0]) {
+ $formattedValue = sprintf("<em>array</em>(%s)", is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
+ } elseif ('string' === $item[0]) {
+ $formattedValue = sprintf("'%s'", htmlspecialchars($item[1], ENT_QUOTES, $this->charset));
+ } elseif ('null' === $item[0]) {
+ $formattedValue = '<em>null</em>';
+ } elseif ('boolean' === $item[0]) {
+ $formattedValue = '<em>'.strtolower(var_export($item[1], true)).'</em>';
+ } elseif ('resource' === $item[0]) {
+ $formattedValue = '<em>resource</em>';
+ } else {
+ $formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], ENT_QUOTES, $this->charset), true));
+ }
+
+ $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
+ }
+
+ return implode(', ', $result);
+ }
+
+ /**
+ * Formats an array as a string.
+ *
+ * @param array $args The argument array
+ *
+ * @return string
+ */
+ public function formatArgsAsText($args)
+ {
+ return strip_tags($this->formatArgs($args));
+ }
+
+ /**
+ * Returns an excerpt of a code file around the given line number.
+ *
+ * @param string $file A file path
+ * @param int $line The selected line number
+ *
+ * @return string An HTML string
+ */
+ public function fileExcerpt($file, $line)
+ {
+ if (is_readable($file)) {
+ $code = highlight_file($file, true);
+ // remove main code/span tags
+ $code = preg_replace('#^<code.*?>\s*<span.*?>(.*)</span>\s*</code>#s', '\\1', $code);
+ $content = preg_split('#<br />#', $code);
+
+ $lines = array();
+ for ($i = max($line - 3, 1), $max = min($line + 3, count($content)); $i <= $max; $i++) {
+ $lines[] = '<li'.($i == $line ? ' class="selected"' : '').'><code>'.self::fixCodeMarkup($content[$i - 1]).'</code></li>';
+ }
+
+ return '<ol start="'.max($line - 3, 1).'">'.implode("\n", $lines).'</ol>';
+ }
+ }
+
+ /**
+ * Formats a file path.
+ *
+ * @param string $file An absolute file path
+ * @param integer $line The line number
+ * @param string $text Use this text for the link rather than the file path
+ *
+ * @return string
+ */
+ public function formatFile($file, $line, $text = null)
+ {
+ if (null === $text) {
+ $file = trim($file);
+ $text = $file;
+ if (0 === strpos($text, $this->rootDir)) {
+ $text = str_replace($this->rootDir, '', str_replace('\\', '/', $text));
+ $text = sprintf('<abbr title="%s">kernel.root_dir</abbr>/%s', $this->rootDir, $text);
+ }
+ }
+
+ $text = "$text at line $line";
+
+ if (false !== $link = $this->getFileLink($file, $line)) {
+ return sprintf('<a href="%s" title="Click to open this file" class="file_link">%s</a>', htmlspecialchars($link, ENT_QUOTES | ENT_SUBSTITUTE, $this->charset), $text);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Returns the link for a given file/line pair.
+ *
+ * @param string $file An absolute file path
+ * @param integer $line The line number
+ *
+ * @return string A link of false
+ */
+ public function getFileLink($file, $line)
+ {
+ if ($this->fileLinkFormat && is_file($file)) {
+ return strtr($this->fileLinkFormat, array('%f' => $file, '%l' => $line));
+ }
+
+ return false;
+ }
+
+ public function formatFileFromText($text)
+ {
+ $that = $this;
+
+ return preg_replace_callback('/in ("|")?(.+?)\1(?: +(?:on|at))? +line (\d+)/s', function ($match) use ($that) {
+ return 'in '.$that->formatFile($match[2], $match[3]);
+ }, $text);
+ }
+
+ public function getName()
+ {
+ return 'code';
+ }
+
+ protected static function fixCodeMarkup($line)
+ {
+ // </span> ending tag from previous line
+ $opening = strpos($line, '<span');
+ $closing = strpos($line, '</span>');
+ if (false !== $closing && (false === $opening || $closing < $opening)) {
+ $line = substr_replace($line, '', $closing, 7);
+ }
+
+ // missing </span> tag at the end of line
+ $opening = strpos($line, '<span');
+ $closing = strpos($line, '</span>');
+ if (false !== $opening && (false === $closing || $closing > $opening)) {
+ $line .= '</span>';
+ }
+
+ return $line;
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
+use Symfony\Bridge\Twig\Form\TwigRendererInterface;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+
+/**
+ * FormExtension extends Twig with form capabilities.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class FormExtension extends \Twig_Extension
+{
+ /**
+ * This property is public so that it can be accessed directly from compiled
+ * templates without having to call a getter, which slightly decreases performance.
+ *
+ * @var TwigRendererInterface
+ */
+ public $renderer;
+
+ public function __construct(TwigRendererInterface $renderer)
+ {
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initRuntime(\Twig_Environment $environment)
+ {
+ $this->renderer->setEnvironment($environment);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTokenParsers()
+ {
+ return array(
+ // {% form_theme form "SomeBundle::widgets.twig" %}
+ new FormThemeTokenParser(),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFunctions()
+ {
+ return array(
+ 'form_enctype' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\FormEnctypeNode', array('is_safe' => array('html'))),
+ 'form_widget' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
+ 'form_errors' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
+ 'form_label' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
+ 'form_row' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
+ 'form_rest' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode', array('is_safe' => array('html'))),
+ 'form' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\RenderBlockNode', array('is_safe' => array('html'))),
+ 'form_start' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\RenderBlockNode', array('is_safe' => array('html'))),
+ 'form_end' => new \Twig_Function_Node('Symfony\Bridge\Twig\Node\RenderBlockNode', array('is_safe' => array('html'))),
+ 'csrf_token' => new \Twig_Function_Method($this, 'renderer->renderCsrfToken'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters()
+ {
+ return array(
+ 'humanize' => new \Twig_Filter_Method($this, 'renderer->humanize'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTests()
+ {
+ return array(
+ 'selectedchoice' => new \Twig_Test_Method($this, 'isSelectedChoice'),
+ );
+ }
+
+ /**
+ * Returns whether a choice is selected for a given form value.
+ *
+ * Unfortunately Twig does not support an efficient way to execute the
+ * "is_selected" closure passed to the template by ChoiceType. It is faster
+ * to implement the logic here (around 65ms for a specific form).
+ *
+ * Directly implementing the logic here is also faster than doing so in
+ * ChoiceView (around 30ms).
+ *
+ * The worst option tested so far is to implement the logic in ChoiceView
+ * and access the ChoiceView method directly in the template. Doing so is
+ * around 220ms slower than doing the method call here in the filter. Twig
+ * seems to be much more efficient at executing filters than at executing
+ * methods of an object.
+ *
+ * @param ChoiceView $choice The choice to check.
+ * @param string|array $selectedValue The selected value to compare.
+ *
+ * @return Boolean Whether the choice is selected.
+ *
+ * @see ChoiceView::isSelected()
+ */
+ public function isSelectedChoice(ChoiceView $choice, $selectedValue)
+ {
+ if (is_array($selectedValue)) {
+ return false !== array_search($choice->value, $selectedValue, true);
+ }
+
+ return $choice->value === $selectedValue;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'form';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
+use Symfony\Component\HttpKernel\Controller\ControllerReference;
+
+/**
+ * Provides integration with the HttpKernel component.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class HttpKernelExtension extends \Twig_Extension
+{
+ private $handler;
+
+ /**
+ * Constructor.
+ *
+ * @param FragmentHandler $handler A FragmentHandler instance
+ */
+ public function __construct(FragmentHandler $handler)
+ {
+ $this->handler = $handler;
+ }
+
+ public function getFunctions()
+ {
+ return array(
+ 'render' => new \Twig_Function_Method($this, 'renderFragment', array('is_safe' => array('html'))),
+ 'render_*' => new \Twig_Function_Method($this, 'renderFragmentStrategy', array('is_safe' => array('html'))),
+ 'controller' => new \Twig_Function_Method($this, 'controller'),
+ );
+ }
+
+ /**
+ * Renders a fragment.
+ *
+ * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance
+ * @param array $options An array of options
+ *
+ * @return string The fragment content
+ *
+ * @see Symfony\Component\HttpKernel\Fragment\FragmentHandler::render()
+ */
+ public function renderFragment($uri, $options = array())
+ {
+ $strategy = isset($options['strategy']) ? $options['strategy'] : 'inline';
+ unset($options['strategy']);
+
+ return $this->handler->render($uri, $strategy, $options);
+ }
+
+ /**
+ * Renders a fragment.
+ *
+ * @param string $strategy A strategy name
+ * @param string|ControllerReference $uri A URI as a string or a ControllerReference instance
+ * @param array $options An array of options
+ *
+ * @return string The fragment content
+ *
+ * @see Symfony\Component\HttpKernel\Fragment\FragmentHandler::render()
+ */
+ public function renderFragmentStrategy($strategy, $uri, $options = array())
+ {
+ return $this->handler->render($uri, $strategy, $options);
+ }
+
+ public function controller($controller, $attributes = array(), $query = array())
+ {
+ return new ControllerReference($controller, $attributes, $query);
+ }
+
+ public function getName()
+ {
+ return 'http_kernel';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+
+/**
+ * Provides integration of the Routing component with Twig.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class RoutingExtension extends \Twig_Extension
+{
+ private $generator;
+
+ public function __construct(UrlGeneratorInterface $generator)
+ {
+ $this->generator = $generator;
+ }
+
+ /**
+ * Returns a list of functions to add to the existing list.
+ *
+ * @return array An array of functions
+ */
+ public function getFunctions()
+ {
+ return array(
+ 'url' => new \Twig_Function_Method($this, 'getUrl', array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
+ 'path' => new \Twig_Function_Method($this, 'getPath', array('is_safe_callback' => array($this, 'isUrlGenerationSafe'))),
+ );
+ }
+
+ public function getPath($name, $parameters = array(), $relative = false)
+ {
+ return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
+ }
+
+ public function getUrl($name, $parameters = array(), $schemeRelative = false)
+ {
+ return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
+ }
+
+ /**
+ * Determines at compile time whether the generated URL will be safe and thus
+ * saving the unneeded automatic escaping for performance reasons.
+ *
+ * The URL generation process percent encodes non-alphanumeric characters. So there is no risk
+ * that malicious/invalid characters are part of the URL. The only character within an URL that
+ * must be escaped in html is the ampersand ("&") which separates query params. So we cannot mark
+ * the URL generation as always safe, but only when we are sure there won't be multiple query
+ * params. This is the case when there are none or only one constant parameter given.
+ * E.g. we know beforehand this will be safe:
+ * - path('route')
+ * - path('route', {'param': 'value'})
+ * But the following may not:
+ * - path('route', var)
+ * - path('route', {'param': ['val1', 'val2'] }) // a sub-array
+ * - path('route', {'param1': 'value1', 'param2': 'value2'})
+ * If param1 and param2 reference placeholder in the route, it would still be safe. But we don't know.
+ *
+ * @param \Twig_Node $argsNode The arguments of the path/url function
+ *
+ * @return array An array with the contexts the URL is safe
+ */
+ public function isUrlGenerationSafe(\Twig_Node $argsNode)
+ {
+ // support named arguments
+ $paramsNode = $argsNode->hasNode('parameters') ? $argsNode->getNode('parameters') : (
+ $argsNode->hasNode(1) ? $argsNode->getNode(1) : null
+ );
+
+ if (null === $paramsNode || $paramsNode instanceof \Twig_Node_Expression_Array && count($paramsNode) <= 2 &&
+ (!$paramsNode->hasNode(1) || $paramsNode->getNode(1) instanceof \Twig_Node_Expression_Constant)
+ ) {
+ return array('html');
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ public function getName()
+ {
+ return 'routing';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Component\Security\Acl\Voter\FieldVote;
+use Symfony\Component\Security\Core\SecurityContextInterface;
+
+/**
+ * SecurityExtension exposes security context features.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class SecurityExtension extends \Twig_Extension
+{
+ private $context;
+
+ public function __construct(SecurityContextInterface $context = null)
+ {
+ $this->context = $context;
+ }
+
+ public function isGranted($role, $object = null, $field = null)
+ {
+ if (null === $this->context) {
+ return false;
+ }
+
+ if (null !== $field) {
+ $object = new FieldVote($object, $field);
+ }
+
+ return $this->context->isGranted($role, $object);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFunctions()
+ {
+ return array(
+ 'is_granted' => new \Twig_Function_Method($this, 'isGranted'),
+ );
+ }
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ public function getName()
+ {
+ return 'security';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Bridge\Twig\TokenParser\TransTokenParser;
+use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser;
+use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser;
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
+use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor;
+
+/**
+ * Provides integration of the Translation component with Twig.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TranslationExtension extends \Twig_Extension
+{
+ private $translator;
+ private $translationNodeVisitor;
+
+ public function __construct(TranslatorInterface $translator, \Twig_NodeVisitorInterface $translationNodeVisitor = null)
+ {
+ if (!$translationNodeVisitor) {
+ $translationNodeVisitor = new TranslationNodeVisitor();
+ }
+
+ $this->translator = $translator;
+ $this->translationNodeVisitor = $translationNodeVisitor;
+ }
+
+ public function getTranslator()
+ {
+ return $this->translator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters()
+ {
+ return array(
+ 'trans' => new \Twig_Filter_Method($this, 'trans'),
+ 'transchoice' => new \Twig_Filter_Method($this, 'transchoice'),
+ );
+ }
+
+ /**
+ * Returns the token parser instance to add to the existing list.
+ *
+ * @return array An array of Twig_TokenParser instances
+ */
+ public function getTokenParsers()
+ {
+ return array(
+ // {% trans %}Symfony is great!{% endtrans %}
+ new TransTokenParser(),
+
+ // {% transchoice count %}
+ // {0} There is no apples|{1} There is one apple|]1,Inf] There is {{ count }} apples
+ // {% endtranschoice %}
+ new TransChoiceTokenParser(),
+
+ // {% trans_default_domain "foobar" %}
+ new TransDefaultDomainTokenParser(),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNodeVisitors()
+ {
+ return array($this->translationNodeVisitor, new TranslationDefaultDomainNodeVisitor());
+ }
+
+ public function getTranslationNodeVisitor()
+ {
+ return $this->translationNodeVisitor;
+ }
+
+ public function trans($message, array $arguments = array(), $domain = null, $locale = null)
+ {
+ if (null === $domain) {
+ $domain = 'messages';
+ }
+
+ return $this->translator->trans($message, $arguments, $domain, $locale);
+ }
+
+ public function transchoice($message, $count, array $arguments = array(), $domain = null, $locale = null)
+ {
+ if (null === $domain) {
+ $domain = 'messages';
+ }
+
+ return $this->translator->transChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale);
+ }
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ public function getName()
+ {
+ return 'translator';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Extension;
+
+use Symfony\Component\Yaml\Dumper as YamlDumper;
+
+/**
+ * Provides integration of the Yaml component with Twig.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class YamlExtension extends \Twig_Extension
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilters()
+ {
+ return array(
+ 'yaml_encode' => new \Twig_Filter_Method($this, 'encode'),
+ 'yaml_dump' => new \Twig_Filter_Method($this, 'dump'),
+ );
+ }
+
+ public function encode($input, $inline = 0, $dumpObjects = false)
+ {
+ static $dumper;
+
+ if (null === $dumper) {
+ $dumper = new YamlDumper();
+ }
+
+ return $dumper->dump($input, $inline, false, $dumpObjects);
+ }
+
+ public function dump($value, $inline = 0, $dumpObjects = false)
+ {
+ if (is_resource($value)) {
+ return '%Resource%';
+ }
+
+ if (is_array($value) || is_object($value)) {
+ return '%'.gettype($value).'% '.$this->encode($value, $inline, $dumpObjects);
+ }
+
+ return $this->encode($value, $inline, $dumpObjects);
+ }
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ public function getName()
+ {
+ return 'yaml';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Form;
+
+use Symfony\Component\Form\FormRenderer;
+use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TwigRenderer extends FormRenderer implements TwigRendererInterface
+{
+ /**
+ * @var TwigRendererEngineInterface
+ */
+ private $engine;
+
+ public function __construct(TwigRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
+ {
+ parent::__construct($engine, $csrfProvider);
+
+ $this->engine = $engine;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEnvironment(\Twig_Environment $environment)
+ {
+ $this->engine->setEnvironment($environment);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Form;
+
+use Symfony\Component\Form\AbstractRendererEngine;
+use Symfony\Component\Form\FormView;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class TwigRendererEngine extends AbstractRendererEngine implements TwigRendererEngineInterface
+{
+ /**
+ * @var \Twig_Environment
+ */
+ private $environment;
+
+ /**
+ * @var \Twig_Template
+ */
+ private $template;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEnvironment(\Twig_Environment $environment)
+ {
+ $this->environment = $environment;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderBlock(FormView $view, $resource, $blockName, array $variables = array())
+ {
+ $cacheKey = $view->vars[self::CACHE_KEY_VAR];
+
+ $context = $this->environment->mergeGlobals($variables);
+
+ ob_start();
+
+ // By contract,This method can only be called after getting the resource
+ // (which is passed to the method). Getting a resource for the first time
+ // (with an empty cache) is guaranteed to invoke loadResourcesFromTheme(),
+ // where the property $template is initialized.
+
+ // We do not call renderBlock here to avoid too many nested level calls
+ // (XDebug limits the level to 100 by default)
+ $this->template->displayBlock($blockName, $context, $this->resources[$cacheKey]);
+
+ return ob_get_clean();
+ }
+
+ /**
+ * Loads the cache with the resource for a given block name.
+ *
+ * This implementation eagerly loads all blocks of the themes assigned to the given view
+ * and all of its ancestors views. This is necessary, because Twig receives the
+ * list of blocks later. At that point, all blocks must already be loaded, for the
+ * case that the function "block()" is used in the Twig template.
+ *
+ * @see getResourceForBlock()
+ *
+ * @param string $cacheKey The cache key of the form view.
+ * @param FormView $view The form view for finding the applying themes.
+ * @param string $blockName The name of the block to load.
+ *
+ * @return Boolean True if the resource could be loaded, false otherwise.
+ */
+ protected function loadResourceForBlockName($cacheKey, FormView $view, $blockName)
+ {
+ // The caller guarantees that $this->resources[$cacheKey][$block] is
+ // not set, but it doesn't have to check whether $this->resources[$cacheKey]
+ // is set. If $this->resources[$cacheKey] is set, all themes for this
+ // $cacheKey are already loaded (due to the eager population, see doc comment).
+ if (isset($this->resources[$cacheKey])) {
+ // As said in the previous, the caller guarantees that
+ // $this->resources[$cacheKey][$block] is not set. Since the themes are
+ // already loaded, it can only be a non-existing block.
+ $this->resources[$cacheKey][$blockName] = false;
+
+ return false;
+ }
+
+ // Recursively try to find the block in the themes assigned to $view,
+ // then of its parent view, then of the parent view of the parent and so on.
+ // When the root view is reached in this recursion, also the default
+ // themes are taken into account.
+
+ // Check each theme whether it contains the searched block
+ if (isset($this->themes[$cacheKey])) {
+ for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
+ $this->loadResourcesFromTheme($cacheKey, $this->themes[$cacheKey][$i]);
+ // CONTINUE LOADING (see doc comment)
+ }
+ }
+
+ // Check the default themes once we reach the root view without success
+ if (!$view->parent) {
+ for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
+ $this->loadResourcesFromTheme($cacheKey, $this->defaultThemes[$i]);
+ // CONTINUE LOADING (see doc comment)
+ }
+ }
+
+ // Proceed with the themes of the parent view
+ if ($view->parent) {
+ $parentCacheKey = $view->parent->vars[self::CACHE_KEY_VAR];
+
+ if (!isset($this->resources[$parentCacheKey])) {
+ $this->loadResourceForBlockName($parentCacheKey, $view->parent, $blockName);
+ }
+
+ // EAGER CACHE POPULATION (see doc comment)
+ foreach ($this->resources[$parentCacheKey] as $nestedBlockName => $resource) {
+ if (!isset($this->resources[$cacheKey][$nestedBlockName])) {
+ $this->resources[$cacheKey][$nestedBlockName] = $resource;
+ }
+ }
+ }
+
+ // Even though we loaded the themes, it can happen that none of them
+ // contains the searched block
+ if (!isset($this->resources[$cacheKey][$blockName])) {
+ // Cache that we didn't find anything to speed up further accesses
+ $this->resources[$cacheKey][$blockName] = false;
+ }
+
+ return false !== $this->resources[$cacheKey][$blockName];
+ }
+
+ /**
+ * Loads the resources for all blocks in a theme.
+ *
+ * @param string $cacheKey The cache key for storing the resource.
+ * @param mixed $theme The theme to load the block from. This parameter
+ * is passed by reference, because it might be necessary
+ * to initialize the theme first. Any changes made to
+ * this variable will be kept and be available upon
+ * further calls to this method using the same theme.
+ */
+ protected function loadResourcesFromTheme($cacheKey, &$theme)
+ {
+ if (!$theme instanceof \Twig_Template) {
+ /* @var \Twig_Template $theme */
+ $theme = $this->environment->loadTemplate($theme);
+ }
+
+ if (null === $this->template) {
+ // Store the first \Twig_Template instance that we find so that
+ // we can call displayBlock() later on. It doesn't matter *which*
+ // template we use for that, since we pass the used blocks manually
+ // anyway.
+ $this->template = $theme;
+ }
+
+ // Use a separate variable for the inheritance traversal, because
+ // theme is a reference and we don't want to change it.
+ $currentTheme = $theme;
+
+ // The do loop takes care of template inheritance.
+ // Add blocks from all templates in the inheritance tree, but avoid
+ // overriding blocks already set.
+ do {
+ foreach ($currentTheme->getBlocks() as $block => $blockData) {
+ if (!isset($this->resources[$cacheKey][$block])) {
+ // The resource given back is the key to the bucket that
+ // contains this block.
+ $this->resources[$cacheKey][$block] = $blockData;
+ }
+ }
+ } while (false !== $currentTheme = $currentTheme->getParent(array()));
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Form;
+
+use Symfony\Component\Form\FormRendererEngineInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface TwigRendererEngineInterface extends FormRendererEngineInterface
+{
+ /**
+ * Sets Twig's environment.
+ *
+ * @param \Twig_Environment $environment
+ */
+ public function setEnvironment(\Twig_Environment $environment);
+}
--- /dev/null
+<?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\Bridge\Twig\Form;
+
+use Symfony\Component\Form\FormRendererInterface;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+interface TwigRendererInterface extends FormRendererInterface
+{
+ /**
+ * Sets Twig's environment.
+ *
+ * @param \Twig_Environment $environment
+ */
+ public function setEnvironment(\Twig_Environment $environment);
+}
--- /dev/null
+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.
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ *
+ * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
+ * the helper "form_start()" instead.
+ */
+class FormEnctypeNode extends SearchAndRenderBlockNode
+{
+ public function compile(\Twig_Compiler $compiler)
+ {
+ parent::compile($compiler);
+
+ $compiler->raw(";\n");
+
+ // Uncomment this as soon as the deprecation note should be shown
+ // $compiler->write('trigger_error(\'The helper form_enctype(form) is deprecated since version 2.3 and will be removed in 3.0. Use form_start(form) instead.\', E_USER_DEPRECATED)');
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class FormThemeNode extends \Twig_Node
+{
+ public function __construct(\Twig_NodeInterface $form, \Twig_NodeInterface $resources, $lineno, $tag = null)
+ {
+ parent::__construct(array('form' => $form, 'resources' => $resources), array(), $lineno, $tag);
+ }
+
+ /**
+ * Compiles the node to PHP.
+ *
+ * @param \Twig_Compiler $compiler A Twig_Compiler instance
+ */
+ public function compile(\Twig_Compiler $compiler)
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$this->env->getExtension(\'form\')->renderer->setTheme(')
+ ->subcompile($this->getNode('form'))
+ ->raw(', ')
+ ->subcompile($this->getNode('resources'))
+ ->raw(");\n");
+ ;
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * Compiles a call to {@link FormRendererInterface::renderBlock()}.
+ *
+ * The function name is used as block name. For example, if the function name
+ * is "foo", the block "foo" will be rendered.
+ *
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class RenderBlockNode extends \Twig_Node_Expression_Function
+{
+ public function compile(\Twig_Compiler $compiler)
+ {
+ $compiler->addDebugInfo($this);
+ $arguments = iterator_to_array($this->getNode('arguments'));
+ $compiler->write('$this->env->getExtension(\'form\')->renderer->renderBlock(');
+
+ if (isset($arguments[0])) {
+ $compiler->subcompile($arguments[0]);
+ $compiler->raw(', \'' . $this->getAttribute('name') . '\'');
+
+ if (isset($arguments[1])) {
+ $compiler->raw(', ');
+ $compiler->subcompile($arguments[1]);
+ }
+ }
+
+ $compiler->raw(')');
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * @author Bernhard Schussek <bschussek@gmail.com>
+ */
+class SearchAndRenderBlockNode extends \Twig_Node_Expression_Function
+{
+ public function compile(\Twig_Compiler $compiler)
+ {
+ $compiler->addDebugInfo($this);
+ $compiler->raw('$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(');
+
+ preg_match('/_([^_]+)$/', $this->getAttribute('name'), $matches);
+
+ $label = null;
+ $arguments = iterator_to_array($this->getNode('arguments'));
+ $blockNameSuffix = $matches[1];
+
+ if (isset($arguments[0])) {
+ $compiler->subcompile($arguments[0]);
+ $compiler->raw(', \''.$blockNameSuffix.'\'');
+
+ if (isset($arguments[1])) {
+ if ('label' === $blockNameSuffix) {
+ // The "label" function expects the label in the second and
+ // the variables in the third argument
+ $label = $arguments[1];
+ $variables = isset($arguments[2]) ? $arguments[2] : null;
+ $lineno = $label->getLine();
+
+ if ($label instanceof \Twig_Node_Expression_Constant) {
+ // If the label argument is given as a constant, we can either
+ // strip it away if it is empty, or integrate it into the array
+ // of variables at compile time.
+ $labelIsExpression = false;
+
+ // Only insert the label into the array if it is not empty
+ if (!twig_test_empty($label->getAttribute('value'))) {
+ $originalVariables = $variables;
+ $variables = new \Twig_Node_Expression_Array(array(), $lineno);
+ $labelKey = new \Twig_Node_Expression_Constant('label', $lineno);
+
+ if (null !== $originalVariables) {
+ foreach ($originalVariables->getKeyValuePairs() as $pair) {
+ // Don't copy the original label attribute over if it exists
+ if ((string) $labelKey !== (string) $pair['key']) {
+ $variables->addElement($pair['value'], $pair['key']);
+ }
+ }
+ }
+
+ // Insert the label argument into the array
+ $variables->addElement($label, $labelKey);
+ }
+ } else {
+ // The label argument is not a constant, but some kind of
+ // expression. This expression needs to be evaluated at runtime.
+ // Depending on the result (whether it is null or not), the
+ // label in the arguments should take precedence over the label
+ // in the attributes or not.
+ $labelIsExpression = true;
+ }
+ } else {
+ // All other functions than "label" expect the variables
+ // in the second argument
+ $label = null;
+ $variables = $arguments[1];
+ $labelIsExpression = false;
+ }
+
+ if (null !== $variables || $labelIsExpression) {
+ $compiler->raw(', ');
+
+ if (null !== $variables) {
+ $compiler->subcompile($variables);
+ }
+
+ if ($labelIsExpression) {
+ if (null !== $variables) {
+ $compiler->raw(' + ');
+ }
+
+ // Check at runtime whether the label is empty.
+ // If not, add it to the array at runtime.
+ $compiler->raw('(twig_test_empty($_label_ = ');
+ $compiler->subcompile($label);
+ $compiler->raw(') ? array() : array("label" => $_label_))');
+ }
+ }
+ }
+ }
+
+ $compiler->raw(")");
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TransDefaultDomainNode extends \Twig_Node
+{
+ public function __construct(\Twig_Node_Expression $expr, $lineno = 0, $tag = null)
+ {
+ parent::__construct(array('expr' => $expr), array(), $lineno, $tag);
+ }
+
+ /**
+ * Compiles the node to PHP.
+ *
+ * @param \Twig_Compiler $compiler A Twig_Compiler instance
+ */
+ public function compile(\Twig_Compiler $compiler)
+ {
+ // noop as this node is just a marker for TranslationDefaultDomainNodeVisitor
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Node;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TransNode extends \Twig_Node
+{
+ public function __construct(\Twig_NodeInterface $body, \Twig_NodeInterface $domain = null, \Twig_Node_Expression $count = null, \Twig_Node_Expression $vars = null, \Twig_Node_Expression $locale = null, $lineno = 0, $tag = null)
+ {
+ parent::__construct(array('count' => $count, 'body' => $body, 'domain' => $domain, 'vars' => $vars, 'locale' => $locale), array(), $lineno, $tag);
+ }
+
+ /**
+ * Compiles the node to PHP.
+ *
+ * @param \Twig_Compiler $compiler A Twig_Compiler instance
+ */
+ public function compile(\Twig_Compiler $compiler)
+ {
+ $compiler->addDebugInfo($this);
+
+ $vars = $this->getNode('vars');
+ $defaults = new \Twig_Node_Expression_Array(array(), -1);
+ if ($vars instanceof \Twig_Node_Expression_Array) {
+ $defaults = $this->getNode('vars');
+ $vars = null;
+ }
+ list($msg, $defaults) = $this->compileString($this->getNode('body'), $defaults);
+
+ $method = null === $this->getNode('count') ? 'trans' : 'transChoice';
+
+ $compiler
+ ->write('echo $this->env->getExtension(\'translator\')->getTranslator()->'.$method.'(')
+ ->subcompile($msg)
+ ;
+
+ $compiler->raw(', ');
+
+ if (null !== $this->getNode('count')) {
+ $compiler
+ ->subcompile($this->getNode('count'))
+ ->raw(', ')
+ ;
+ }
+
+ if (null !== $vars) {
+ $compiler
+ ->raw('array_merge(')
+ ->subcompile($defaults)
+ ->raw(', ')
+ ->subcompile($this->getNode('vars'))
+ ->raw(')')
+ ;
+ } else {
+ $compiler->subcompile($defaults);
+ }
+
+ $compiler->raw(', ');
+
+ if (null === $this->getNode('domain')) {
+ $compiler->repr('messages');
+ } else {
+ $compiler->subcompile($this->getNode('domain'));
+ }
+
+ if (null !== $this->getNode('locale')) {
+ $compiler
+ ->raw(', ')
+ ->subcompile($this->getNode('locale'))
+ ;
+ }
+ $compiler->raw(");\n");
+ }
+
+ protected function compileString(\Twig_NodeInterface $body, \Twig_Node_Expression_Array $vars)
+ {
+ if ($body instanceof \Twig_Node_Expression_Constant) {
+ $msg = $body->getAttribute('value');
+ } elseif ($body instanceof \Twig_Node_Text) {
+ $msg = $body->getAttribute('data');
+ } else {
+ return array($body, $vars);
+ }
+
+ preg_match_all('/(?<!%)%([^%]+)%/', $msg, $matches);
+
+ if (version_compare(\Twig_Environment::VERSION, '1.5', '>=')) {
+ foreach ($matches[1] as $var) {
+ $key = new \Twig_Node_Expression_Constant('%'.$var.'%', $body->getLine());
+ if (!$vars->hasElement($key)) {
+ $vars->addElement(new \Twig_Node_Expression_Name($var, $body->getLine()), $key);
+ }
+ }
+ } else {
+ $current = array();
+ foreach ($vars as $name => $var) {
+ $current[$name] = true;
+ }
+ foreach ($matches[1] as $var) {
+ if (!isset($current['%'.$var.'%'])) {
+ $vars->setNode('%'.$var.'%', new \Twig_Node_Expression_Name($var, $body->getLine()));
+ }
+ }
+ }
+
+ return array(new \Twig_Node_Expression_Constant(str_replace('%%', '%', trim($msg)), $body->getLine()), $vars);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\NodeVisitor;
+
+/**
+ * @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
+ */
+class Scope
+{
+ /**
+ * @var Scope|null
+ */
+ private $parent;
+
+ /**
+ * @var Scope[]
+ */
+ private $children;
+
+ /**
+ * @var array
+ */
+ private $data;
+
+ /**
+ * @var boolean
+ */
+ private $left;
+
+ /**
+ * @param Scope $parent
+ */
+ public function __construct(Scope $parent = null)
+ {
+ $this->parent = $parent;
+ $this->left = false;
+ $this->data = array();
+ }
+
+ /**
+ * Opens a new child scope.
+ *
+ * @return Scope
+ */
+ public function enter()
+ {
+ $child = new self($this);
+ $this->children[] = $child;
+
+ return $child;
+ }
+
+ /**
+ * Closes current scope and returns parent one.
+ *
+ * @return Scope|null
+ */
+ public function leave()
+ {
+ $this->left = true;
+
+ return $this->parent;
+ }
+
+ /**
+ * Stores data into current scope.
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @throws \LogicException
+ *
+ * @return Scope Current scope
+ */
+ public function set($key, $value)
+ {
+ if ($this->left) {
+ throw new \LogicException('Left scope is not mutable.');
+ }
+
+ $this->data[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Tests if a data is visible from current scope.
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function has($key)
+ {
+ if (array_key_exists($key, $this->data)) {
+ return true;
+ }
+
+ if (null === $this->parent) {
+ return false;
+ }
+
+ return $this->parent->has($key);
+ }
+
+ /**
+ * Returns data visible from current scope.
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ if (array_key_exists($key, $this->data)) {
+ return $this->data[$key];
+ }
+
+ if (null === $this->parent) {
+ return $default;
+ }
+
+ return $this->parent->get($key, $default);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\NodeVisitor;
+
+use Symfony\Bridge\Twig\Node\TransNode;
+use Symfony\Bridge\Twig\Node\TransDefaultDomainNode;
+
+/**
+ * TranslationDefaultDomainNodeVisitor.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TranslationDefaultDomainNodeVisitor implements \Twig_NodeVisitorInterface
+{
+ /**
+ * @var Scope
+ */
+ private $scope;
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ $this->scope = new Scope();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env)
+ {
+ if ($node instanceof \Twig_Node_Block || $node instanceof \Twig_Node_Module) {
+ $this->scope = $this->scope->enter();
+ }
+
+ if ($node instanceof TransDefaultDomainNode) {
+ if ($node->getNode('expr') instanceof \Twig_Node_Expression_Constant) {
+ $this->scope->set('domain', $node->getNode('expr'));
+
+ return $node;
+ } else {
+ $var = $env->getParser()->getVarName();
+ $name = new \Twig_Node_Expression_AssignName($var, $node->getLine());
+ $this->scope->set('domain', new \Twig_Node_Expression_Name($var, $node->getLine()));
+
+ return new \Twig_Node_Set(false, new \Twig_Node(array($name)), new \Twig_Node(array($node->getNode('expr'))), $node->getLine());
+ }
+ }
+
+ if (!$this->scope->has('domain')) {
+ return $node;
+ }
+
+ if ($node instanceof \Twig_Node_Expression_Filter && in_array($node->getNode('filter')->getAttribute('value'), array('trans', 'transchoice'))) {
+ $ind = 'trans' === $node->getNode('filter')->getAttribute('value') ? 1 : 2;
+ $arguments = $node->getNode('arguments');
+ if (!$arguments->hasNode($ind)) {
+ if (!$arguments->hasNode($ind - 1)) {
+ $arguments->setNode($ind - 1, new \Twig_Node_Expression_Array(array(), $node->getLine()));
+ }
+
+ $arguments->setNode($ind, $this->scope->get('domain'));
+ }
+ } elseif ($node instanceof TransNode) {
+ if (null === $node->getNode('domain')) {
+ $node->setNode('domain', $this->scope->get('domain'));
+ }
+ }
+
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env)
+ {
+ if ($node instanceof TransDefaultDomainNode) {
+ return false;
+ }
+
+ if ($node instanceof \Twig_Node_Block || $node instanceof \Twig_Node_Module) {
+ $this->scope = $this->scope->leave();
+ }
+
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPriority()
+ {
+ return -10;
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\NodeVisitor;
+
+use Symfony\Bridge\Twig\Node\TransNode;
+
+/**
+ * TranslationNodeVisitor extracts translation messages.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TranslationNodeVisitor implements \Twig_NodeVisitorInterface
+{
+ const UNDEFINED_DOMAIN = '_undefined';
+
+ private $enabled = false;
+ private $messages = array();
+
+ public function enable()
+ {
+ $this->enabled = true;
+ $this->messages = array();
+ }
+
+ public function disable()
+ {
+ $this->enabled = false;
+ $this->messages = array();
+ }
+
+ public function getMessages()
+ {
+ return $this->messages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env)
+ {
+ if (!$this->enabled) {
+ return $node;
+ }
+
+ if (
+ $node instanceof \Twig_Node_Expression_Filter &&
+ 'trans' === $node->getNode('filter')->getAttribute('value') &&
+ $node->getNode('node') instanceof \Twig_Node_Expression_Constant
+ ) {
+ // extract constant nodes with a trans filter
+ $this->messages[] = array(
+ $node->getNode('node')->getAttribute('value'),
+ $this->getReadDomainFromArguments($node->getNode('arguments'), 1),
+ );
+ } elseif (
+ $node instanceof \Twig_Node_Expression_Filter &&
+ 'transchoice' === $node->getNode('filter')->getAttribute('value') &&
+ $node->getNode('node') instanceof \Twig_Node_Expression_Constant
+ ) {
+ // extract constant nodes with a trans filter
+ $this->messages[] = array(
+ $node->getNode('node')->getAttribute('value'),
+ $this->getReadDomainFromArguments($node->getNode('arguments'), 2),
+ );
+ } elseif ($node instanceof TransNode) {
+ // extract trans nodes
+ $this->messages[] = array(
+ $node->getNode('body')->getAttribute('data'),
+ $this->getReadDomainFromNode($node->getNode('domain')),
+ );
+ }
+
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env)
+ {
+ return $node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPriority()
+ {
+ return 0;
+ }
+
+ /**
+ * @param \Twig_Node $arguments
+ * @param int $index
+ *
+ * @return string|null
+ */
+ private function getReadDomainFromArguments(\Twig_Node $arguments, $index)
+ {
+ if ($arguments->hasNode('domain')) {
+ $argument = $arguments->getNode('domain');
+ } elseif ($arguments->hasNode($index)) {
+ $argument = $arguments->getNode($index);
+ } else {
+ return null;
+ }
+
+ return $this->getReadDomainFromNode($argument);
+ }
+
+ /**
+ * @param \Twig_Node $node
+ *
+ * @return string|null
+ */
+ private function getReadDomainFromNode(\Twig_Node $node = null)
+ {
+ if (null === $node) {
+ return null;
+ }
+
+ if ($node instanceof \Twig_Node_Expression_Constant) {
+ return $node->getAttribute('value');
+ }
+
+ return self::UNDEFINED_DOMAIN;
+ }
+}
--- /dev/null
+Twig Bridge
+===========
+
+Provides integration for [Twig](http://twig.sensiolabs.org/) with various
+Symfony2 components.
+
+Resources
+---------
+
+If you want to run the unit tests, install dev dependencies before
+running PHPUnit:
+
+ $ cd path/to/Symfony/Bridge/Twig/
+ $ composer.phar install --dev
+ $ phpunit
--- /dev/null
+{# Widgets #}
+
+{% block form_widget %}
+{% spaceless %}
+ {% if compound %}
+ {{ block('form_widget_compound') }}
+ {% else %}
+ {{ block('form_widget_simple') }}
+ {% endif %}
+{% endspaceless %}
+{% endblock form_widget %}
+
+{% block form_widget_simple %}
+{% spaceless %}
+ {% set type = type|default('text') %}
+ <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
+{% endspaceless %}
+{% endblock form_widget_simple %}
+
+{% block form_widget_compound %}
+{% spaceless %}
+ <div {{ block('widget_container_attributes') }}>
+ {% if form.parent is empty %}
+ {{ form_errors(form) }}
+ {% endif %}
+ {{ block('form_rows') }}
+ {{ form_rest(form) }}
+ </div>
+{% endspaceless %}
+{% endblock form_widget_compound %}
+
+{% block collection_widget %}
+{% spaceless %}
+ {% if prototype is defined %}
+ {% set attr = attr|merge({'data-prototype': form_row(prototype) }) %}
+ {% endif %}
+ {{ block('form_widget') }}
+{% endspaceless %}
+{% endblock collection_widget %}
+
+{% block textarea_widget %}
+{% spaceless %}
+ <textarea {{ block('widget_attributes') }}>{{ value }}</textarea>
+{% endspaceless %}
+{% endblock textarea_widget %}
+
+{% block choice_widget %}
+{% spaceless %}
+ {% if expanded %}
+ {{ block('choice_widget_expanded') }}
+ {% else %}
+ {{ block('choice_widget_collapsed') }}
+ {% endif %}
+{% endspaceless %}
+{% endblock choice_widget %}
+
+{% block choice_widget_expanded %}
+{% spaceless %}
+ <div {{ block('widget_container_attributes') }}>
+ {% for child in form %}
+ {{ form_widget(child) }}
+ {{ form_label(child) }}
+ {% endfor %}
+ </div>
+{% endspaceless %}
+{% endblock choice_widget_expanded %}
+
+{% block choice_widget_collapsed %}
+{% spaceless %}
+ <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
+ {% if empty_value is not none %}
+ <option {% if required %} disabled="disabled"{% if value is empty %} selected="selected"{% endif %}{% else %} value=""{% endif %}>{{ empty_value|trans({}, translation_domain) }}</option>
+ {% endif %}
+ {% if preferred_choices|length > 0 %}
+ {% set options = preferred_choices %}
+ {{ block('choice_widget_options') }}
+ {% if choices|length > 0 and separator is not none %}
+ <option disabled="disabled">{{ separator }}</option>
+ {% endif %}
+ {% endif %}
+ {% set options = choices %}
+ {{ block('choice_widget_options') }}
+ </select>
+{% endspaceless %}
+{% endblock choice_widget_collapsed %}
+
+{% block choice_widget_options %}
+{% spaceless %}
+ {% for group_label, choice in options %}
+ {% if choice is iterable %}
+ <optgroup label="{{ group_label|trans({}, translation_domain) }}">
+ {% set options = choice %}
+ {{ block('choice_widget_options') }}
+ </optgroup>
+ {% else %}
+ <option value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
+ {% endif %}
+ {% endfor %}
+{% endspaceless %}
+{% endblock choice_widget_options %}
+
+{% block checkbox_widget %}
+{% spaceless %}
+ <input type="checkbox" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
+{% endspaceless %}
+{% endblock checkbox_widget %}
+
+{% block radio_widget %}
+{% spaceless %}
+ <input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
+{% endspaceless %}
+{% endblock radio_widget %}
+
+{% block datetime_widget %}
+{% spaceless %}
+ {% if widget == 'single_text' %}
+ {{ block('form_widget_simple') }}
+ {% else %}
+ <div {{ block('widget_container_attributes') }}>
+ {{ form_errors(form.date) }}
+ {{ form_errors(form.time) }}
+ {{ form_widget(form.date) }}
+ {{ form_widget(form.time) }}
+ </div>
+ {% endif %}
+{% endspaceless %}
+{% endblock datetime_widget %}
+
+{% block date_widget %}
+{% spaceless %}
+ {% if widget == 'single_text' %}
+ {{ block('form_widget_simple') }}
+ {% else %}
+ <div {{ block('widget_container_attributes') }}>
+ {{ date_pattern|replace({
+ '{{ year }}': form_widget(form.year),
+ '{{ month }}': form_widget(form.month),
+ '{{ day }}': form_widget(form.day),
+ })|raw }}
+ </div>
+ {% endif %}
+{% endspaceless %}
+{% endblock date_widget %}
+
+{% block time_widget %}
+{% spaceless %}
+ {% if widget == 'single_text' %}
+ {{ block('form_widget_simple') }}
+ {% else %}
+ {% set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} %}
+ <div {{ block('widget_container_attributes') }}>
+ {{ form_widget(form.hour, vars) }}{% if with_minutes %}:{{ form_widget(form.minute, vars) }}{% endif %}{% if with_seconds %}:{{ form_widget(form.second, vars) }}{% endif %}
+ </div>
+ {% endif %}
+{% endspaceless %}
+{% endblock time_widget %}
+
+{% block number_widget %}
+{% spaceless %}
+ {# type="number" doesn't work with floats #}
+ {% set type = type|default('text') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock number_widget %}
+
+{% block integer_widget %}
+{% spaceless %}
+ {% set type = type|default('number') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock integer_widget %}
+
+{% block money_widget %}
+{% spaceless %}
+ {{ money_pattern|replace({ '{{ widget }}': block('form_widget_simple') })|raw }}
+{% endspaceless %}
+{% endblock money_widget %}
+
+{% block url_widget %}
+{% spaceless %}
+ {% set type = type|default('url') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock url_widget %}
+
+{% block search_widget %}
+{% spaceless %}
+ {% set type = type|default('search') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock search_widget %}
+
+{% block percent_widget %}
+{% spaceless %}
+ {% set type = type|default('text') %}
+ {{ block('form_widget_simple') }} %
+{% endspaceless %}
+{% endblock percent_widget %}
+
+{% block password_widget %}
+{% spaceless %}
+ {% set type = type|default('password') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock password_widget %}
+
+{% block hidden_widget %}
+{% spaceless %}
+ {% set type = type|default('hidden') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock hidden_widget %}
+
+{% block email_widget %}
+{% spaceless %}
+ {% set type = type|default('email') %}
+ {{ block('form_widget_simple') }}
+{% endspaceless %}
+{% endblock email_widget %}
+
+{% block button_widget %}
+{% spaceless %}
+ {% if label is empty %}
+ {% set label = name|humanize %}
+ {% endif %}
+ <button type="{{ type|default('button') }}" {{ block('button_attributes') }}>{{ label|trans({}, translation_domain) }}</button>
+{% endspaceless %}
+{% endblock button_widget %}
+
+{% block submit_widget %}
+{% spaceless %}
+ {% set type = type|default('submit') %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock submit_widget %}
+
+{% block reset_widget %}
+{% spaceless %}
+ {% set type = type|default('reset') %}
+ {{ block('button_widget') }}
+{% endspaceless %}
+{% endblock reset_widget %}
+
+{# Labels #}
+
+{% block form_label %}
+{% spaceless %}
+ {% if label is not sameas(false) %}
+ {% if not compound %}
+ {% set label_attr = label_attr|merge({'for': id}) %}
+ {% endif %}
+ {% if required %}
+ {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
+ {% endif %}
+ {% if label is empty %}
+ {% set label = name|humanize %}
+ {% endif %}
+ <label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ label|trans({}, translation_domain) }}</label>
+ {% endif %}
+{% endspaceless %}
+{% endblock form_label %}
+
+{% block button_label %}{% endblock %}
+
+{# Rows #}
+
+{% block repeated_row %}
+{% spaceless %}
+ {#
+ No need to render the errors here, as all errors are mapped
+ to the first child (see RepeatedTypeValidatorExtension).
+ #}
+ {{ block('form_rows') }}
+{% endspaceless %}
+{% endblock repeated_row %}
+
+{% block form_row %}
+{% spaceless %}
+ <div>
+ {{ form_label(form) }}
+ {{ form_errors(form) }}
+ {{ form_widget(form) }}
+ </div>
+{% endspaceless %}
+{% endblock form_row %}
+
+{% block button_row %}
+{% spaceless %}
+ <div>
+ {{ form_widget(form) }}
+ </div>
+{% endspaceless %}
+{% endblock button_row %}
+
+{% block hidden_row %}
+ {{ form_widget(form) }}
+{% endblock hidden_row %}
+
+{# Misc #}
+
+{% block form %}
+{% spaceless %}
+ {{ form_start(form) }}
+ {{ form_widget(form) }}
+ {{ form_end(form) }}
+{% endspaceless %}
+{% endblock form %}
+
+{% block form_start %}
+{% spaceless %}
+ {% set method = method|upper %}
+ {% if method in ["GET", "POST"] %}
+ {% set form_method = method %}
+ {% else %}
+ {% set form_method = "POST" %}
+ {% endif %}
+ <form method="{{ form_method|lower }}" action="{{ action }}"{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}{% if multipart %} enctype="multipart/form-data"{% endif %}>
+ {% if form_method != method %}
+ <input type="hidden" name="_method" value="{{ method }}" />
+ {% endif %}
+{% endspaceless %}
+{% endblock form_start %}
+
+{% block form_end %}
+{% spaceless %}
+ {% if not render_rest is defined or render_rest %}
+ {{ form_rest(form) }}
+ {% endif %}
+ </form>
+{% endspaceless %}
+{% endblock form_end %}
+
+{% block form_enctype %}
+{% spaceless %}
+ {% if multipart %}enctype="multipart/form-data"{% endif %}
+{% endspaceless %}
+{% endblock form_enctype %}
+
+{% block form_errors %}
+{% spaceless %}
+ {% if errors|length > 0 %}
+ <ul>
+ {% for error in errors %}
+ <li>{{ error.message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endspaceless %}
+{% endblock form_errors %}
+
+{% block form_rest %}
+{% spaceless %}
+ {% for child in form %}
+ {% if not child.rendered %}
+ {{ form_row(child) }}
+ {% endif %}
+ {% endfor %}
+{% endspaceless %}
+{% endblock form_rest %}
+
+{# Support #}
+
+{% block form_rows %}
+{% spaceless %}
+ {% for child in form %}
+ {{ form_row(child) }}
+ {% endfor %}
+{% endspaceless %}
+{% endblock form_rows %}
+
+{% block widget_attributes %}
+{% spaceless %}
+ id="{{ id }}" name="{{ full_name }}"{% if read_only %} readonly="readonly"{% endif %}{% if disabled %} disabled="disabled"{% endif %}{% if required %} required="required"{% endif %}{% if max_length %} maxlength="{{ max_length }}"{% endif %}{% if pattern %} pattern="{{ pattern }}"{% endif %}
+ {% for attrname, attrvalue in attr %}{% if attrname in ['placeholder', 'title'] %}{{ attrname }}="{{ attrvalue|trans({}, translation_domain) }}" {% else %}{{ attrname }}="{{ attrvalue }}" {% endif %}{% endfor %}
+{% endspaceless %}
+{% endblock widget_attributes %}
+
+{% block widget_container_attributes %}
+{% spaceless %}
+ {% if id is not empty %}id="{{ id }}" {% endif %}
+ {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %}
+{% endspaceless %}
+{% endblock widget_container_attributes %}
+
+{% block button_attributes %}
+{% spaceless %}
+ id="{{ id }}" name="{{ full_name }}"{% if disabled %} disabled="disabled"{% endif %}
+ {% for attrname, attrvalue in attr %}{{ attrname }}="{{ attrvalue }}" {% endfor %}
+{% endspaceless %}
+{% endblock button_attributes %}
--- /dev/null
+{% use "form_div_layout.html.twig" %}
+
+{% block form_row %}
+{% spaceless %}
+ <tr>
+ <td>
+ {{ form_label(form) }}
+ </td>
+ <td>
+ {{ form_errors(form) }}
+ {{ form_widget(form) }}
+ </td>
+ </tr>
+{% endspaceless %}
+{% endblock form_row %}
+
+{% block button_row %}
+{% spaceless %}
+ <tr>
+ <td></td>
+ <td>
+ {{ form_widget(form) }}
+ </td>
+ </tr>
+{% endspaceless %}
+{% endblock button_row %}
+
+{% block hidden_row %}
+{% spaceless %}
+ <tr style="display: none">
+ <td colspan="2">
+ {{ form_widget(form) }}
+ </td>
+ </tr>
+{% endspaceless %}
+{% endblock hidden_row %}
+
+{% block form_widget_compound %}
+{% spaceless %}
+ <table {{ block('widget_container_attributes') }}>
+ {% if form.parent is empty and errors|length > 0 %}
+ <tr>
+ <td colspan="2">
+ {{ form_errors(form) }}
+ </td>
+ </tr>
+ {% endif %}
+ {{ block('form_rows') }}
+ {{ form_rest(form) }}
+ </table>
+{% endspaceless %}
+{% endblock form_widget_compound %}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Bridge\Twig\Extension\CodeExtension;
+
+class CodeExtensionTest extends \PHPUnit_Framework_TestCase
+{
+ protected $helper;
+
+ public function testFormatFile()
+ {
+ $expected = sprintf('<a href="txmt://open?url=file://%s&line=25" title="Click to open this file" class="file_link">%s at line 25</a>', __FILE__, __FILE__);
+ $this->assertEquals($expected, $this->getExtension()->formatFile(__FILE__, 25));
+ }
+
+ /**
+ * @dataProvider getClassNameProvider
+ */
+ public function testGettingClassAbbreviation($class, $abbr)
+ {
+ $this->assertEquals($this->getExtension()->abbrClass($class), $abbr);
+ }
+
+ /**
+ * @dataProvider getMethodNameProvider
+ */
+ public function testGettingMethodAbbreviation($method, $abbr)
+ {
+ $this->assertEquals($this->getExtension()->abbrMethod($method), $abbr);
+ }
+
+ public function getClassNameProvider()
+ {
+ return array(
+ array('F\Q\N\Foo', '<abbr title="F\Q\N\Foo">Foo</abbr>'),
+ array('Bare', '<abbr title="Bare">Bare</abbr>'),
+ );
+ }
+
+ public function getMethodNameProvider()
+ {
+ return array(
+ array('F\Q\N\Foo::Method', '<abbr title="F\Q\N\Foo">Foo</abbr>::Method()'),
+ array('Bare::Method', '<abbr title="Bare">Bare</abbr>::Method()'),
+ array('Closure', '<abbr title="Closure">Closure</abbr>'),
+ array('Method', '<abbr title="Method">Method</abbr>()')
+ );
+ }
+
+ public function testGetName()
+ {
+ $this->assertEquals('code', $this->getExtension()->getName());
+ }
+
+ protected function getExtension()
+ {
+ return new CodeExtension('txmt://open?url=file://%f&line=%l', '/root', 'UTF-8');
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension\Fixtures;
+
+// Preventing autoloader throwing E_FATAL when Twig is now available
+if (!class_exists('Twig_Environment')) {
+ class StubFilesystemLoader
+ {
+ }
+} else {
+ class StubFilesystemLoader extends \Twig_Loader_Filesystem
+ {
+ protected function findTemplate($name)
+ {
+ // strip away bundle name
+ $parts = explode(':', $name);
+
+ return parent::findTemplate(end($parts));
+ }
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension\Fixtures;
+
+use Symfony\Component\Translation\TranslatorInterface;
+
+class StubTranslator implements TranslatorInterface
+{
+ public function trans($id, array $parameters = array(), $domain = null, $locale = null)
+ {
+ return '[trans]'.$id.'[/trans]';
+ }
+
+ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
+ {
+ return '[trans]'.$id.'[/trans]';
+ }
+
+ public function setLocale($locale)
+ {
+ }
+
+ public function getLocale()
+ {
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Bridge\Twig\Extension\FormExtension;
+use Symfony\Bridge\Twig\Form\TwigRenderer;
+use Symfony\Bridge\Twig\Form\TwigRendererEngine;
+use Symfony\Bridge\Twig\Extension\TranslationExtension;
+use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
+use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\Form\Extension\Core\View\ChoiceView;
+use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
+
+class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
+{
+ /**
+ * @var FormExtension
+ */
+ protected $extension;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Locale\Locale')) {
+ $this->markTestSkipped('The "Locale" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\Form\Form')) {
+ $this->markTestSkipped('The "Form" component is not available');
+ }
+
+ if (!class_exists('Twig_Environment')) {
+ $this->markTestSkipped('Twig is not available.');
+ }
+
+ parent::setUp();
+
+ $rendererEngine = new TwigRendererEngine(array(
+ 'form_div_layout.html.twig',
+ 'custom_widgets.html.twig',
+ ));
+ $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
+
+ $this->extension = new FormExtension($renderer);
+
+ $loader = new StubFilesystemLoader(array(
+ __DIR__.'/../../Resources/views/Form',
+ __DIR__,
+ ));
+
+ $environment = new \Twig_Environment($loader, array('strict_variables' => true));
+ $environment->addExtension(new TranslationExtension(new StubTranslator()));
+ $environment->addGlobal('global', '');
+ $environment->addExtension($this->extension);
+
+ $this->extension->initRuntime($environment);
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->extension = null;
+ }
+
+ public function testThemeBlockInheritanceUsingUse()
+ {
+ $view = $this->factory
+ ->createNamed('name', 'email')
+ ->createView()
+ ;
+
+ $this->setTheme($view, array('theme_use.html.twig'));
+
+ $this->assertMatchesXpath(
+ $this->renderWidget($view),
+ '/input[@type="email"][@rel="theme"]'
+ );
+ }
+
+ public function testThemeBlockInheritanceUsingExtend()
+ {
+ $view = $this->factory
+ ->createNamed('name', 'email')
+ ->createView()
+ ;
+
+ $this->setTheme($view, array('theme_extends.html.twig'));
+
+ $this->assertMatchesXpath(
+ $this->renderWidget($view),
+ '/input[@type="email"][@rel="theme"]'
+ );
+ }
+
+ public function isSelectedChoiceProvider()
+ {
+ // The commented cases should not be necessary anymore, because the
+ // choice lists should assure that both values passed here are always
+ // strings
+ return array(
+// array(true, 0, 0),
+ array(true, '0', '0'),
+ array(true, '1', '1'),
+// array(true, false, 0),
+// array(true, true, 1),
+ array(true, '', ''),
+// array(true, null, ''),
+ array(true, '1.23', '1.23'),
+ array(true, 'foo', 'foo'),
+ array(true, 'foo10', 'foo10'),
+ array(true, 'foo', array(1, 'foo', 'foo10')),
+
+ array(false, 10, array(1, 'foo', 'foo10')),
+ array(false, 0, array(1, 'foo', 'foo10')),
+ );
+ }
+
+ /**
+ * @dataProvider isSelectedChoiceProvider
+ */
+ public function testIsChoiceSelected($expected, $choice, $value)
+ {
+ $choice = new ChoiceView($choice, $choice, $choice.' label');
+
+ $this->assertSame($expected, $this->extension->isSelectedChoice($choice, $value));
+ }
+
+ protected function renderForm(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form', $vars);
+ }
+
+ protected function renderEnctype(FormView $view)
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype');
+ }
+
+ protected function renderLabel(FormView $view, $label = null, array $vars = array())
+ {
+ if ($label !== null) {
+ $vars += array('label' => $label);
+ }
+
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'label', $vars);
+ }
+
+ protected function renderErrors(FormView $view)
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'errors');
+ }
+
+ protected function renderWidget(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'widget', $vars);
+ }
+
+ protected function renderRow(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'row', $vars);
+ }
+
+ protected function renderRest(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars);
+ }
+
+ protected function renderStart(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars);
+ }
+
+ protected function renderEnd(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars);
+ }
+
+ protected function setTheme(FormView $view, array $themes)
+ {
+ $this->extension->renderer->setTheme($view, $themes);
+ }
+
+ public static function themeBlockInheritanceProvider()
+ {
+ return array(
+ array(array('theme.html.twig'))
+ );
+ }
+
+ public static function themeInheritanceProvider()
+ {
+ return array(
+ array(array('parent_label.html.twig'), array('child_label.html.twig'))
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Component\Form\FormView;
+use Symfony\Bridge\Twig\Form\TwigRenderer;
+use Symfony\Bridge\Twig\Form\TwigRendererEngine;
+use Symfony\Bridge\Twig\Extension\FormExtension;
+use Symfony\Bridge\Twig\Extension\TranslationExtension;
+use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
+use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
+use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
+
+class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
+{
+ /**
+ * @var FormExtension
+ */
+ protected $extension;
+
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Locale\Locale')) {
+ $this->markTestSkipped('The "Locale" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ if (!class_exists('Symfony\Component\Form\Form')) {
+ $this->markTestSkipped('The "Form" component is not available');
+ }
+
+ if (!class_exists('Twig_Environment')) {
+ $this->markTestSkipped('Twig is not available.');
+ }
+
+ parent::setUp();
+
+ $rendererEngine = new TwigRendererEngine(array(
+ 'form_table_layout.html.twig',
+ 'custom_widgets.html.twig',
+ ));
+ $renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
+
+ $this->extension = new FormExtension($renderer);
+
+ $loader = new StubFilesystemLoader(array(
+ __DIR__.'/../../Resources/views/Form',
+ __DIR__,
+ ));
+
+ $environment = new \Twig_Environment($loader, array('strict_variables' => true));
+ $environment->addExtension(new TranslationExtension(new StubTranslator()));
+ $environment->addGlobal('global', '');
+ $environment->addExtension($this->extension);
+
+ $this->extension->initRuntime($environment);
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+
+ $this->extension = null;
+ }
+
+ protected function renderForm(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form', $vars);
+ }
+
+ protected function renderEnctype(FormView $view)
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'enctype');
+ }
+
+ protected function renderLabel(FormView $view, $label = null, array $vars = array())
+ {
+ if ($label !== null) {
+ $vars += array('label' => $label);
+ }
+
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'label', $vars);
+ }
+
+ protected function renderErrors(FormView $view)
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'errors');
+ }
+
+ protected function renderWidget(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'widget', $vars);
+ }
+
+ protected function renderRow(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'row', $vars);
+ }
+
+ protected function renderRest(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->searchAndRenderBlock($view, 'rest', $vars);
+ }
+
+ protected function renderStart(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form_start', $vars);
+ }
+
+ protected function renderEnd(FormView $view, array $vars = array())
+ {
+ return (string) $this->extension->renderer->renderBlock($view, 'form_end', $vars);
+ }
+
+ protected function setTheme(FormView $view, array $themes)
+ {
+ $this->extension->renderer->setTheme($view, $themes);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Bridge\Twig\Extension\HttpKernelExtension;
+use Symfony\Bridge\Twig\Tests\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
+
+class HttpKernelExtensionTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Symfony\Component\HttpKernel\HttpKernel')) {
+ $this->markTestSkipped('The "HttpKernel" component is not available');
+ }
+
+ if (!class_exists('Twig_Environment')) {
+ $this->markTestSkipped('Twig is not available.');
+ }
+ }
+
+ /**
+ * @expectedException \Twig_Error_Runtime
+ */
+ public function testFragmentWithError()
+ {
+ $kernel = $this->getFragmentHandler($this->throwException(new \Exception('foo')));
+
+ $loader = new \Twig_Loader_Array(array('index' => '{{ fragment("foo") }}'));
+ $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false));
+ $twig->addExtension(new HttpKernelExtension($kernel));
+
+ $this->renderTemplate($kernel);
+ }
+
+ protected function getFragmentHandler($return)
+ {
+ $strategy = $this->getMock('Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface');
+ $strategy->expects($this->once())->method('getName')->will($this->returnValue('inline'));
+ $strategy->expects($this->once())->method('render')->will($return);
+
+ $renderer = new FragmentHandler(array($strategy));
+ $renderer->setRequest(Request::create('/'));
+
+ return $renderer;
+ }
+
+ protected function renderTemplate(FragmentHandler $renderer, $template = '{{ render("foo") }}')
+ {
+ $loader = new \Twig_Loader_Array(array('index' => $template));
+ $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false));
+ $twig->addExtension(new HttpKernelExtension($renderer));
+
+ return $twig->render('index');
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Bridge\Twig\Extension\RoutingExtension;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class RoutingExtensionTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Symfony\Component\Routing\Route')) {
+ $this->markTestSkipped('The "Routing" component is not available');
+ }
+ }
+
+ /**
+ * @dataProvider getEscapingTemplates
+ */
+ public function testEscaping($template, $mustBeEscaped)
+ {
+ $twig = new \Twig_Environment(null, array('debug' => true, 'cache' => false, 'autoescape' => true, 'optimizations' => 0));
+ $twig->addExtension(new RoutingExtension($this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface')));
+
+ $nodes = $twig->parse($twig->tokenize($template));
+
+ $this->assertSame($mustBeEscaped, $nodes->getNode('body')->getNode(0)->getNode('expr') instanceof \Twig_Node_Expression_Filter);
+ }
+
+ public function getEscapingTemplates()
+ {
+ return array(
+ array('{{ path("foo") }}', false),
+ array('{{ path("foo", {}) }}', false),
+ array('{{ path("foo", { foo: "foo" }) }}', false),
+ array('{{ path("foo", foo) }}', true),
+ array('{{ path("foo", { foo: foo }) }}', true),
+ array('{{ path("foo", { foo: ["foo", "bar"] }) }}', true),
+ array('{{ path("foo", { foo: "foo", bar: "bar" }) }}', true),
+
+ array('{{ path(name = "foo", parameters = {}) }}', false),
+ array('{{ path(name = "foo", parameters = { foo: "foo" }) }}', false),
+ array('{{ path(name = "foo", parameters = foo) }}', true),
+ array('{{ path(name = "foo", parameters = { foo: ["foo", "bar"] }) }}', true),
+ array('{{ path(name = "foo", parameters = { foo: foo }) }}', true),
+ array('{{ path(name = "foo", parameters = { foo: "foo", bar: "bar" }) }}', true),
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Extension;
+
+use Symfony\Bridge\Twig\Extension\TranslationExtension;
+use Symfony\Component\Translation\Translator;
+use Symfony\Component\Translation\MessageSelector;
+use Symfony\Component\Translation\Loader\ArrayLoader;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class TranslationExtensionTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (!class_exists('Symfony\Component\Translation\Translator')) {
+ $this->markTestSkipped('The "Translation" component is not available');
+ }
+
+ if (!class_exists('Twig_Environment')) {
+ $this->markTestSkipped('Twig is not available.');
+ }
+ }
+
+ public function testEscaping()
+ {
+ $output = $this->getTemplate('{% trans %}Percent: %value%%% (%msg%){% endtrans %}')->render(array('value' => 12, 'msg' => 'approx.'));
+
+ $this->assertEquals('Percent: 12% (approx.)', $output);
+ }
+
+ /**
+ * @dataProvider getTransTests
+ */
+ public function testTrans($template, $expected, array $variables = array())
+ {
+ if ($expected != $this->getTemplate($template)->render($variables)) {
+ print $template."\n";
+ $loader = new \Twig_Loader_Array(array('index' => $template));
+ $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false));
+ $twig->addExtension(new TranslationExtension(new Translator('en', new MessageSelector())));
+
+ echo $twig->compile($twig->parse($twig->tokenize($twig->getLoader()->getSource('index'), 'index')))."\n\n";
+ $this->assertEquals($expected, $this->getTemplate($template)->render($variables));
+ }
+
+ $this->assertEquals($expected, $this->getTemplate($template)->render($variables));
+ }
+
+ public function getTransTests()
+ {
+ return array(
+ // trans tag
+ array('{% trans %}Hello{% endtrans %}', 'Hello'),
+ array('{% trans %}%name%{% endtrans %}', 'Symfony2', array('name' => 'Symfony2')),
+
+ array('{% trans from elsewhere %}Hello{% endtrans %}', 'Hello'),
+
+ array('{% trans %}Hello %name%{% endtrans %}', 'Hello Symfony2', array('name' => 'Symfony2')),
+ array('{% trans with { \'%name%\': \'Symfony2\' } %}Hello %name%{% endtrans %}', 'Hello Symfony2'),
+ array('{% set vars = { \'%name%\': \'Symfony2\' } %}{% trans with vars %}Hello %name%{% endtrans %}', 'Hello Symfony2'),
+
+ array('{% trans into "fr"%}Hello{% endtrans %}', 'Hello'),
+
+ // transchoice
+ array('{% transchoice count from "messages" %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}',
+ 'There is no apples', array('count' => 0)),
+ array('{% transchoice count %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}',
+ 'There is 5 apples', array('count' => 5)),
+ array('{% transchoice count %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtranschoice %}',
+ 'There is 5 apples (Symfony2)', array('count' => 5, 'name' => 'Symfony2')),
+ array('{% transchoice count with { \'%name%\': \'Symfony2\' } %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtranschoice %}',
+ 'There is 5 apples (Symfony2)', array('count' => 5)),
+ array('{% transchoice count into "fr"%}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}',
+ 'There is no apples', array('count' => 0)),
+
+ // trans filter
+ array('{{ "Hello"|trans }}', 'Hello'),
+ array('{{ name|trans }}', 'Symfony2', array('name' => 'Symfony2')),
+ array('{{ hello|trans({ \'%name%\': \'Symfony2\' }) }}', 'Hello Symfony2', array('hello' => 'Hello %name%')),
+ array('{% set vars = { \'%name%\': \'Symfony2\' } %}{{ hello|trans(vars) }}', 'Hello Symfony2', array('hello' => 'Hello %name%')),
+ array('{{ "Hello"|trans({}, "messages", "fr") }}', 'Hello'),
+
+ // transchoice filter
+ array('{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|transchoice(count) }}', 'There is 5 apples', array('count' => 5)),
+ array('{{ text|transchoice(5, {\'%name%\': \'Symfony2\'}) }}', 'There is 5 apples (Symfony2)', array('text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)')),
+ array('{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|transchoice(count, {}, "messages", "fr") }}', 'There is 5 apples', array('count' => 5)),
+ );
+ }
+
+ public function testDefaultTranslationDomain()
+ {
+ $templates = array(
+ 'index' => '
+ {%- extends "base" %}
+
+ {%- trans_default_domain "foo" %}
+
+ {%- block content %}
+ {%- trans %}foo{% endtrans %}
+ {%- trans from "custom" %}foo{% endtrans %}
+ {{- "foo"|trans }}
+ {{- "foo"|trans({}, "custom") }}
+ {{- "foo"|transchoice(1) }}
+ {{- "foo"|transchoice(1, {}, "custom") }}
+ {% endblock %}
+ ',
+
+ 'base' => '
+ {%- block content "" %}
+ ',
+ );
+
+ $translator = new Translator('en', new MessageSelector());
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', array('foo' => 'foo (messages)'), 'en');
+ $translator->addResource('array', array('foo' => 'foo (custom)'), 'en', 'custom');
+ $translator->addResource('array', array('foo' => 'foo (foo)'), 'en', 'foo');
+
+ $template = $this->getTemplate($templates, $translator);
+
+ $this->assertEquals('foo (foo)foo (custom)foo (foo)foo (custom)foo (foo)foo (custom)', trim($template->render(array())));
+ }
+
+ protected function getTemplate($template, $translator = null)
+ {
+ if (null === $translator) {
+ $translator = new Translator('en', new MessageSelector());
+ }
+
+ if (is_array($template)) {
+ $loader = new \Twig_Loader_Array($template);
+ } else {
+ $loader = new \Twig_Loader_Array(array('index' => $template));
+ }
+ $twig = new \Twig_Environment($loader, array('debug' => true, 'cache' => false));
+ $twig->addExtension(new TranslationExtension($translator));
+
+ return $twig->loadTemplate('index');
+ }
+}
--- /dev/null
+{% block form_label %}
+ <label>{{ global }}child</label>
+{% endblock form_label %}
--- /dev/null
+{% block _text_id_widget %}
+{% spaceless %}
+ <div id="container">
+ {{ form_widget(form) }}
+ </div>
+{% endspaceless %}
+{% endblock _text_id_widget %}
+
+{% block _name_entry_label %}
+{% spaceless %}
+ {% if label is empty %}
+ {% set label = name|humanize %}
+ {% endif %}
+ <label>Custom label: {{ label|trans({}, translation_domain) }}</label>
+{% endspaceless %}
+{% endblock _name_entry_label %}
--- /dev/null
+{% block form_label %}
+ <label>parent</label>
+{% endblock form_label %}
--- /dev/null
+{% block form_widget_simple %}
+{% spaceless %}
+ {% set type = type|default('text') %}
+ <input type="{{ type }}" {{ block('widget_attributes') }} value="{{ value }}" rel="theme" />
+{% endspaceless %}
+{% endblock form_widget_simple %}
--- /dev/null
+{% extends 'form_div_layout.html.twig' %}
+
+{% block form_widget_simple %}
+{% spaceless %}
+ {% set type = type|default('text') %}
+ <input type="{{ type }}" {{ block('widget_attributes') }} value="{{ value }}" rel="theme" />
+{% endspaceless %}
+{% endblock form_widget_simple %}
--- /dev/null
+{% use 'form_div_layout.html.twig' %}
+
+{% block form_widget_simple %}
+{% spaceless %}
+ {% set type = type|default('text') %}
+ <input type="{{ type }}" {{ block('widget_attributes') }} value="{{ value }}" rel="theme" />
+{% endspaceless %}
+{% endblock form_widget_simple %}
--- /dev/null
+<?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\Bridge\Twig\Tests\Node;
+
+use Symfony\Bridge\Twig\Tests\TestCase;
+use Symfony\Bridge\Twig\Node\FormThemeNode;
+
+class FormThemeTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (version_compare(\Twig_Environment::VERSION, '1.5.0', '<')) {
+ $this->markTestSkipped('Requires Twig version to be at least 1.5.0.');
+ }
+ }
+
+ public function testConstructor()
+ {
+ $form = new \Twig_Node_Expression_Name('form', 0);
+ $resources = new \Twig_Node(array(
+ new \Twig_Node_Expression_Constant('tpl1', 0),
+ new \Twig_Node_Expression_Constant('tpl2', 0)
+ ));
+
+ $node = new FormThemeNode($form, $resources, 0);
+
+ $this->assertEquals($form, $node->getNode('form'));
+ $this->assertEquals($resources, $node->getNode('resources'));
+ }
+
+ public function testCompile()
+ {
+ $form = new \Twig_Node_Expression_Name('form', 0);
+ $resources = new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant(0, 0),
+ new \Twig_Node_Expression_Constant('tpl1', 0),
+ new \Twig_Node_Expression_Constant(1, 0),
+ new \Twig_Node_Expression_Constant('tpl2', 0)
+ ), 0);
+
+ $node = new FormThemeNode($form, $resources, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->setTheme(%s, array(0 => "tpl1", 1 => "tpl2"));',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+
+ $resources = new \Twig_Node_Expression_Constant('tpl1', 0);
+
+ $node = new FormThemeNode($form, $resources, 0);
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->setTheme(%s, "tpl1");',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ protected function getVariableGetter($name)
+ {
+ if (version_compare(phpversion(), '5.4.0RC1', '>=')) {
+ return sprintf('(isset($context["%s"]) ? $context["%s"] : null)', $name, $name);
+ }
+
+ return sprintf('$this->getContext($context, "%s")', $name);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Node;
+
+use Symfony\Bridge\Twig\Tests\TestCase;
+use Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode;
+
+class SearchAndRenderBlockNodeTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (version_compare(\Twig_Environment::VERSION, '1.5.0', '<')) {
+ $this->markTestSkipped('Requires Twig version to be at least 1.5.0.');
+ }
+ }
+
+ public function testCompileWidget()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_widget', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'widget\')',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileWidgetWithVariables()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant('foo', 0),
+ new \Twig_Node_Expression_Constant('bar', 0),
+ ), 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_widget', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'widget\', array("foo" => "bar"))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithLabel()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Constant('my label', 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\', array("label" => "my label"))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithNullLabel()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Constant(null, 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ // "label" => null must not be included in the output!
+ // Otherwise the default label is overwritten with null.
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\')',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithEmptyStringLabel()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Constant('', 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ // "label" => null must not be included in the output!
+ // Otherwise the default label is overwritten with null.
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\')',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithDefaultLabel()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\')',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithAttributes()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Constant(null, 0),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant('foo', 0),
+ new \Twig_Node_Expression_Constant('bar', 0),
+ ), 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ // "label" => null must not be included in the output!
+ // Otherwise the default label is overwritten with null.
+ // https://github.com/symfony/symfony/issues/5029
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\', array("foo" => "bar"))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithLabelAndAttributes()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Constant('value in argument', 0),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant('foo', 0),
+ new \Twig_Node_Expression_Constant('bar', 0),
+ new \Twig_Node_Expression_Constant('label', 0),
+ new \Twig_Node_Expression_Constant('value in attributes', 0),
+ ), 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\', array("foo" => "bar", "label" => "value in argument"))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithLabelThatEvaluatesToNull()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Conditional(
+ // if
+ new \Twig_Node_Expression_Constant(true, 0),
+ // then
+ new \Twig_Node_Expression_Constant(null, 0),
+ // else
+ new \Twig_Node_Expression_Constant(null, 0),
+ 0
+ ),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ // "label" => null must not be included in the output!
+ // Otherwise the default label is overwritten with null.
+ // https://github.com/symfony/symfony/issues/5029
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\', (twig_test_empty($_label_ = ((true) ? (null) : (null))) ? array() : array("label" => $_label_)))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes()
+ {
+ $arguments = new \Twig_Node(array(
+ new \Twig_Node_Expression_Name('form', 0),
+ new \Twig_Node_Expression_Conditional(
+ // if
+ new \Twig_Node_Expression_Constant(true, 0),
+ // then
+ new \Twig_Node_Expression_Constant(null, 0),
+ // else
+ new \Twig_Node_Expression_Constant(null, 0),
+ 0
+ ),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant('foo', 0),
+ new \Twig_Node_Expression_Constant('bar', 0),
+ new \Twig_Node_Expression_Constant('label', 0),
+ new \Twig_Node_Expression_Constant('value in attributes', 0),
+ ), 0),
+ ));
+
+ $node = new SearchAndRenderBlockNode('form_label', $arguments, 0);
+
+ $compiler = new \Twig_Compiler(new \Twig_Environment());
+
+ // "label" => null must not be included in the output!
+ // Otherwise the default label is overwritten with null.
+ // https://github.com/symfony/symfony/issues/5029
+ $this->assertEquals(
+ sprintf(
+ '$this->env->getExtension(\'form\')->renderer->searchAndRenderBlock(%s, \'label\', array("foo" => "bar", "label" => "value in attributes") + (twig_test_empty($_label_ = ((true) ? (null) : (null))) ? array() : array("label" => $_label_)))',
+ $this->getVariableGetter('form')
+ ),
+ trim($compiler->compile($node)->getSource())
+ );
+ }
+
+ protected function getVariableGetter($name)
+ {
+ if (version_compare(phpversion(), '5.4.0RC1', '>=')) {
+ return sprintf('(isset($context["%s"]) ? $context["%s"] : null)', $name, $name);
+ }
+
+ return sprintf('$this->getContext($context, "%s")', $name);
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\NodeVisitor;
+
+use Symfony\Bridge\Twig\NodeVisitor\Scope;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class ScopeTest extends TestCase
+{
+ public function testScopeInitiation()
+ {
+ $scope = new Scope();
+ $scope->enter();
+ $this->assertNull($scope->get('test'));
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\NodeVisitor;
+
+use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor;
+use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class TranslationDefaultDomainNodeVisitorTest extends TestCase
+{
+ private static $message = 'message';
+ private static $domain = 'domain';
+
+ /** @dataProvider getDefaultDomainAssignmentTestData */
+ public function testDefaultDomainAssignment(\Twig_Node $node)
+ {
+ $env = new \Twig_Environment(new \Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $visitor = new TranslationDefaultDomainNodeVisitor();
+
+ // visit trans_default_domain tag
+ $defaultDomain = TwigNodeProvider::getTransDefaultDomainTag(self::$domain);
+ $visitor->enterNode($defaultDomain, $env);
+ $visitor->leaveNode($defaultDomain, $env);
+
+ // visit tested node
+ $enteredNode = $visitor->enterNode($node, $env);
+ $leavedNode = $visitor->leaveNode($node, $env);
+ $this->assertSame($node, $enteredNode);
+ $this->assertSame($node, $leavedNode);
+
+ // extracting tested node messages
+ $visitor = new TranslationNodeVisitor();
+ $visitor->enable();
+ $visitor->enterNode($node, $env);
+ $visitor->leaveNode($node, $env);
+
+ $this->assertEquals(array(array(self::$message, self::$domain)), $visitor->getMessages());
+ }
+
+ /** @dataProvider getDefaultDomainAssignmentTestData */
+ public function testNewModuleWithoutDefaultDomainTag(\Twig_Node $node)
+ {
+ $env = new \Twig_Environment(new \Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $visitor = new TranslationDefaultDomainNodeVisitor();
+
+ // visit trans_default_domain tag
+ $newModule = TwigNodeProvider::getModule('test');
+ $visitor->enterNode($newModule, $env);
+ $visitor->leaveNode($newModule, $env);
+
+ // visit tested node
+ $enteredNode = $visitor->enterNode($node, $env);
+ $leavedNode = $visitor->leaveNode($node, $env);
+ $this->assertSame($node, $enteredNode);
+ $this->assertSame($node, $leavedNode);
+
+ // extracting tested node messages
+ $visitor = new TranslationNodeVisitor();
+ $visitor->enable();
+ $visitor->enterNode($node, $env);
+ $visitor->leaveNode($node, $env);
+
+ $this->assertEquals(array(array(self::$message, null)), $visitor->getMessages());
+ }
+
+ public function getDefaultDomainAssignmentTestData()
+ {
+ return array(
+ array(TwigNodeProvider::getTransFilter(self::$message)),
+ array(TwigNodeProvider::getTransChoiceFilter(self::$message)),
+ array(TwigNodeProvider::getTransTag(self::$message)),
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\NodeVisitor;
+
+use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class TranslationNodeVisitorTest extends TestCase
+{
+ /** @dataProvider getMessagesExtractionTestData */
+ public function testMessagesExtraction(\Twig_Node $node, array $expectedMessages)
+ {
+ $env = new \Twig_Environment(new \Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $visitor = new TranslationNodeVisitor();
+ $visitor->enable();
+ $visitor->enterNode($node, $env);
+ $visitor->leaveNode($node, $env);
+ $this->assertEquals($expectedMessages, $visitor->getMessages());
+ }
+
+ public function testMessageExtractionWithInvalidDomainNode()
+ {
+ $message = 'new key';
+
+ $node = new \Twig_Node_Expression_Filter(
+ new \Twig_Node_Expression_Constant($message, 0),
+ new \Twig_Node_Expression_Constant('trans', 0),
+ new \Twig_Node(array(
+ new \Twig_Node_Expression_Array(array(), 0),
+ new \Twig_Node_Expression_Name('variable', 0),
+ )),
+ 0
+ );
+
+ $this->testMessagesExtraction($node, array(array($message, TranslationNodeVisitor::UNDEFINED_DOMAIN)));
+ }
+
+ public function getMessagesExtractionTestData()
+ {
+ $message = 'new key';
+ $domain = 'domain';
+
+ return array(
+ array(TwigNodeProvider::getTransFilter($message), array(array($message, null))),
+ array(TwigNodeProvider::getTransChoiceFilter($message), array(array($message, null))),
+ array(TwigNodeProvider::getTransTag($message), array(array($message, null))),
+ array(TwigNodeProvider::getTransFilter($message, $domain), array(array($message, $domain))),
+ array(TwigNodeProvider::getTransChoiceFilter($message, $domain), array(array($message, $domain))),
+ array(TwigNodeProvider::getTransTag($message, $domain), array(array($message, $domain))),
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\NodeVisitor;
+
+use Symfony\Bridge\Twig\Node\TransDefaultDomainNode;
+use Symfony\Bridge\Twig\Node\TransNode;
+
+class TwigNodeProvider
+{
+ public static function getModule($content)
+ {
+ return new \Twig_Node_Module(
+ new \Twig_Node_Expression_Constant($content, 0),
+ null,
+ new \Twig_Node_Expression_Array(array(), 0),
+ new \Twig_Node_Expression_Array(array(), 0),
+ new \Twig_Node_Expression_Array(array(), 0),
+ null,
+ null
+ );
+ }
+
+ public static function getTransFilter($message, $domain = null)
+ {
+ $arguments = $domain ? array(
+ new \Twig_Node_Expression_Array(array(), 0),
+ new \Twig_Node_Expression_Constant($domain, 0),
+ ) : array();
+
+ return new \Twig_Node_Expression_Filter(
+ new \Twig_Node_Expression_Constant($message, 0),
+ new \Twig_Node_Expression_Constant('trans', 0),
+ new \Twig_Node($arguments),
+ 0
+ );
+ }
+
+ public static function getTransChoiceFilter($message, $domain = null)
+ {
+ $arguments = $domain ? array(
+ new \Twig_Node_Expression_Constant(0, 0),
+ new \Twig_Node_Expression_Array(array(), 0),
+ new \Twig_Node_Expression_Constant($domain, 0),
+ ) : array();
+
+ return new \Twig_Node_Expression_Filter(
+ new \Twig_Node_Expression_Constant($message, 0),
+ new \Twig_Node_Expression_Constant('transchoice', 0),
+ new \Twig_Node($arguments),
+ 0
+ );
+ }
+
+ public static function getTransTag($message, $domain = null)
+ {
+ return new TransNode(
+ new \Twig_Node_Body(array(), array('data' => $message)),
+ $domain ? new \Twig_Node_Expression_Constant($domain, 0) : null
+ );
+ }
+
+ public static function getTransDefaultDomainTag($domain)
+ {
+ return new TransDefaultDomainNode(
+ new \Twig_Node_Expression_Constant($domain, 0)
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests;
+
+abstract class TestCase extends \PHPUnit_Framework_TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Twig_Environment')) {
+ $this->markTestSkipped('Twig is not available.');
+ }
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Node;
+
+use Symfony\Bridge\Twig\Tests\TestCase;
+use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
+use Symfony\Bridge\Twig\Node\FormThemeNode;
+
+class FormThemeTokenParserTest extends TestCase
+{
+ protected function setUp()
+ {
+ parent::setUp();
+
+ if (version_compare(\Twig_Environment::VERSION, '1.5.0', '<')) {
+ $this->markTestSkipped('Requires Twig version to be at least 1.5.0.');
+ }
+ }
+
+ /**
+ * @dataProvider getTestsForFormTheme
+ */
+ public function testCompile($source, $expected)
+ {
+ $env = new \Twig_Environment(new \Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $env->addTokenParser(new FormThemeTokenParser());
+ $stream = $env->tokenize($source);
+ $parser = new \Twig_Parser($env);
+
+ $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0));
+ }
+
+ public function getTestsForFormTheme()
+ {
+ return array(
+ array(
+ '{% form_theme form "tpl1" %}',
+ new FormThemeNode(
+ new \Twig_Node_Expression_Name('form', 1),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant(0, 1),
+ new \Twig_Node_Expression_Constant('tpl1', 1),
+ ), 1),
+ 1,
+ 'form_theme'
+ )
+ ),
+ array(
+ '{% form_theme form "tpl1" "tpl2" %}',
+ new FormThemeNode(
+ new \Twig_Node_Expression_Name('form', 1),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant(0, 1),
+ new \Twig_Node_Expression_Constant('tpl1', 1),
+ new \Twig_Node_Expression_Constant(1, 1),
+ new \Twig_Node_Expression_Constant('tpl2', 1)
+ ), 1),
+ 1,
+ 'form_theme'
+ )
+ ),
+ array(
+ '{% form_theme form with "tpl1" %}',
+ new FormThemeNode(
+ new \Twig_Node_Expression_Name('form', 1),
+ new \Twig_Node_Expression_Constant('tpl1', 1),
+ 1,
+ 'form_theme'
+ )
+ ),
+ array(
+ '{% form_theme form with ["tpl1"] %}',
+ new FormThemeNode(
+ new \Twig_Node_Expression_Name('form', 1),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant(0, 1),
+ new \Twig_Node_Expression_Constant('tpl1', 1),
+ ), 1),
+ 1,
+ 'form_theme'
+ )
+ ),
+ array(
+ '{% form_theme form with ["tpl1", "tpl2"] %}',
+ new FormThemeNode(
+ new \Twig_Node_Expression_Name('form', 1),
+ new \Twig_Node_Expression_Array(array(
+ new \Twig_Node_Expression_Constant(0, 1),
+ new \Twig_Node_Expression_Constant('tpl1', 1),
+ new \Twig_Node_Expression_Constant(1, 1),
+ new \Twig_Node_Expression_Constant('tpl2', 1)
+ ), 1),
+ 1,
+ 'form_theme'
+ )
+ ),
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Tests\Translation;
+
+use Symfony\Bridge\Twig\Extension\TranslationExtension;
+use Symfony\Bridge\Twig\Translation\TwigExtractor;
+use Symfony\Component\Translation\MessageCatalogue;
+use Symfony\Bridge\Twig\Tests\TestCase;
+
+class TwigExtractorTest extends TestCase
+{
+ protected function setUp()
+ {
+ if (!class_exists('Symfony\Component\Translation\Translator')) {
+ $this->markTestSkipped('The "Translation" component is not available');
+ }
+ }
+
+ /**
+ * @dataProvider getExtractData
+ */
+ public function testExtract($template, $messages)
+ {
+ $loader = new \Twig_Loader_Array(array());
+ $twig = new \Twig_Environment($loader, array(
+ 'strict_variables' => true,
+ 'debug' => true,
+ 'cache' => false,
+ 'autoescape' => false,
+ ));
+ $twig->addExtension(new TranslationExtension($this->getMock('Symfony\Component\Translation\TranslatorInterface')));
+
+ $extractor = new TwigExtractor($twig);
+ $extractor->setPrefix('prefix');
+ $catalogue = new MessageCatalogue('en');
+
+ $m = new \ReflectionMethod($extractor, 'extractTemplate');
+ $m->setAccessible(true);
+ $m->invoke($extractor, $template, $catalogue);
+
+ foreach ($messages as $key => $domain) {
+ $this->assertTrue($catalogue->has($key, $domain));
+ $this->assertEquals('prefix'.$key, $catalogue->get($key, $domain));
+ }
+ }
+
+ public function getExtractData()
+ {
+ return array(
+ array('{{ "new key" | trans() }}', array('new key' => 'messages')),
+ array('{{ "new key" | trans() | upper }}', array('new key' => 'messages')),
+ array('{{ "new key" | trans({}, "domain") }}', array('new key' => 'domain')),
+ array('{{ "new key" | transchoice(1) }}', array('new key' => 'messages')),
+ array('{{ "new key" | transchoice(1) | upper }}', array('new key' => 'messages')),
+ array('{{ "new key" | transchoice(1, {}, "domain") }}', array('new key' => 'domain')),
+ array('{% trans %}new key{% endtrans %}', array('new key' => 'messages')),
+ array('{% trans %} new key {% endtrans %}', array('new key' => 'messages')),
+ array('{% trans from "domain" %}new key{% endtrans %}', array('new key' => 'domain')),
+ array('{% set foo = "new key" | trans %}', array('new key' => 'messages')),
+ array('{{ 1 ? "new key" | trans : "another key" | trans }}', array('new key' => 'messages', 'another key' => 'messages')),
+
+ // make sure 'trans_default_domain' tag is supported
+ array('{% trans_default_domain "domain" %}{{ "new key"|trans }}', array('new key' => 'domain')),
+ array('{% trans_default_domain "domain" %}{{ "new key"|transchoice }}', array('new key' => 'domain')),
+ array('{% trans_default_domain "domain" %}{% trans %}new key{% endtrans %}', array('new key' => 'domain')),
+
+ // make sure this works with twig's named arguments
+ array('{{ "new key" | trans(domain="domain") }}', array('new key' => 'domain')),
+ array('{{ "new key" | transchoice(domain="domain", count=1) }}', array('new key' => 'domain')),
+ );
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\TokenParser;
+
+use Symfony\Bridge\Twig\Node\FormThemeNode;
+
+/**
+ * Token Parser for the 'form_theme' tag.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class FormThemeTokenParser extends \Twig_TokenParser
+{
+ /**
+ * Parses a token and returns a node.
+ *
+ * @param \Twig_Token $token A Twig_Token instance
+ *
+ * @return \Twig_NodeInterface A Twig_NodeInterface instance
+ */
+ public function parse(\Twig_Token $token)
+ {
+ $lineno = $token->getLine();
+ $stream = $this->parser->getStream();
+
+ $form = $this->parser->getExpressionParser()->parseExpression();
+
+ if ($this->parser->getStream()->test(\Twig_Token::NAME_TYPE, 'with')) {
+ $this->parser->getStream()->next();
+ $resources = $this->parser->getExpressionParser()->parseExpression();
+ } else {
+ $resources = new \Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
+ do {
+ $resources->addElement($this->parser->getExpressionParser()->parseExpression());
+ } while (!$stream->test(\Twig_Token::BLOCK_END_TYPE));
+ }
+
+ $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+
+ return new FormThemeNode($form, $resources, $lineno, $this->getTag());
+ }
+
+ /**
+ * Gets the tag name associated with this token parser.
+ *
+ * @return string The tag name
+ */
+ public function getTag()
+ {
+ return 'form_theme';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\TokenParser;
+
+use Symfony\Bridge\Twig\Node\TransNode;
+
+/**
+ * Token Parser for the 'transchoice' tag.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TransChoiceTokenParser extends TransTokenParser
+{
+ /**
+ * Parses a token and returns a node.
+ *
+ * @param \Twig_Token $token A Twig_Token instance
+ *
+ * @return \Twig_NodeInterface A Twig_NodeInterface instance
+ *
+ * @throws \Twig_Error_Syntax
+ */
+ public function parse(\Twig_Token $token)
+ {
+ $lineno = $token->getLine();
+ $stream = $this->parser->getStream();
+
+ $vars = new \Twig_Node_Expression_Array(array(), $lineno);
+
+ $count = $this->parser->getExpressionParser()->parseExpression();
+
+ $domain = null;
+ $locale = null;
+
+ if ($stream->test('with')) {
+ // {% transchoice count with vars %}
+ $stream->next();
+ $vars = $this->parser->getExpressionParser()->parseExpression();
+ }
+
+ if ($stream->test('from')) {
+ // {% transchoice count from "messages" %}
+ $stream->next();
+ $domain = $this->parser->getExpressionParser()->parseExpression();
+ }
+
+ if ($stream->test('into')) {
+ // {% transchoice count into "fr" %}
+ $stream->next();
+ $locale = $this->parser->getExpressionParser()->parseExpression();
+ }
+
+ $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+
+ $body = $this->parser->subparse(array($this, 'decideTransChoiceFork'), true);
+
+ if (!$body instanceof \Twig_Node_Text && !$body instanceof \Twig_Node_Expression) {
+ throw new \Twig_Error_Syntax('A message must be a simple text.');
+ }
+
+ $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+
+ return new TransNode($body, $domain, $count, $vars, $locale, $lineno, $this->getTag());
+ }
+
+ public function decideTransChoiceFork($token)
+ {
+ return $token->test(array('endtranschoice'));
+ }
+
+ /**
+ * Gets the tag name associated with this token parser.
+ *
+ * @return string The tag name
+ */
+ public function getTag()
+ {
+ return 'transchoice';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\TokenParser;
+
+use Symfony\Bridge\Twig\Node\TransDefaultDomainNode;
+
+/**
+ * Token Parser for the 'trans_default_domain' tag.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TransDefaultDomainTokenParser extends \Twig_TokenParser
+{
+ /**
+ * Parses a token and returns a node.
+ *
+ * @param \Twig_Token $token A Twig_Token instance
+ *
+ * @return \Twig_NodeInterface A Twig_NodeInterface instance
+ */
+ public function parse(\Twig_Token $token)
+ {
+ $expr = $this->parser->getExpressionParser()->parseExpression();
+
+ $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);
+
+ return new TransDefaultDomainNode($expr, $token->getLine(), $this->getTag());
+ }
+
+ /**
+ * Gets the tag name associated with this token parser.
+ *
+ * @return string The tag name
+ */
+ public function getTag()
+ {
+ return 'trans_default_domain';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\TokenParser;
+
+use Symfony\Bridge\Twig\Node\TransNode;
+
+/**
+ * Token Parser for the 'trans' tag.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TransTokenParser extends \Twig_TokenParser
+{
+ /**
+ * Parses a token and returns a node.
+ *
+ * @param \Twig_Token $token A Twig_Token instance
+ *
+ * @return \Twig_NodeInterface A Twig_NodeInterface instance
+ *
+ * @throws \Twig_Error_Syntax
+ */
+ public function parse(\Twig_Token $token)
+ {
+ $lineno = $token->getLine();
+ $stream = $this->parser->getStream();
+
+ $vars = new \Twig_Node_Expression_Array(array(), $lineno);
+ $domain = null;
+ $locale = null;
+ if (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) {
+ if ($stream->test('with')) {
+ // {% trans with vars %}
+ $stream->next();
+ $vars = $this->parser->getExpressionParser()->parseExpression();
+ }
+
+ if ($stream->test('from')) {
+ // {% trans from "messages" %}
+ $stream->next();
+ $domain = $this->parser->getExpressionParser()->parseExpression();
+ }
+
+ if ($stream->test('into')) {
+ // {% trans into "fr" %}
+ $stream->next();
+ $locale = $this->parser->getExpressionParser()->parseExpression();
+ } elseif (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) {
+ throw new \Twig_Error_Syntax('Unexpected token. Twig was looking for the "with" or "from" keyword.');
+ }
+ }
+
+ // {% trans %}message{% endtrans %}
+ $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+ $body = $this->parser->subparse(array($this, 'decideTransFork'), true);
+
+ if (!$body instanceof \Twig_Node_Text && !$body instanceof \Twig_Node_Expression) {
+ throw new \Twig_Error_Syntax('A message inside a trans tag must be a simple text');
+ }
+
+ $stream->expect(\Twig_Token::BLOCK_END_TYPE);
+
+ return new TransNode($body, $domain, null, $vars, $locale, $lineno, $this->getTag());
+ }
+
+ public function decideTransFork($token)
+ {
+ return $token->test(array('endtrans'));
+ }
+
+ /**
+ * Gets the tag name associated with this token parser.
+ *
+ * @return string The tag name
+ */
+ public function getTag()
+ {
+ return 'trans';
+ }
+}
--- /dev/null
+<?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\Bridge\Twig\Translation;
+
+use Symfony\Component\Finder\Finder;
+use Symfony\Component\Translation\Extractor\ExtractorInterface;
+use Symfony\Component\Translation\MessageCatalogue;
+
+/**
+ * TwigExtractor extracts translation messages from a twig template.
+ *
+ * @author Michel Salib <michelsalib@hotmail.com>
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TwigExtractor implements ExtractorInterface
+{
+ /**
+ * Default domain for found messages.
+ *
+ * @var string
+ */
+ private $defaultDomain = 'messages';
+
+ /**
+ * Prefix for found message.
+ *
+ * @var string
+ */
+ private $prefix = '';
+
+ /**
+ * The twig environment.
+ *
+ * @var \Twig_Environment
+ */
+ private $twig;
+
+ public function __construct(\Twig_Environment $twig)
+ {
+ $this->twig = $twig;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function extract($directory, MessageCatalogue $catalogue)
+ {
+ // load any existing translation files
+ $finder = new Finder();
+ $files = $finder->files()->name('*.twig')->in($directory);
+ foreach ($files as $file) {
+ $this->extractTemplate(file_get_contents($file->getPathname()), $catalogue);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setPrefix($prefix)
+ {
+ $this->prefix = $prefix;
+ }
+
+ protected function extractTemplate($template, MessageCatalogue $catalogue)
+ {
+ $visitor = $this->twig->getExtension('translator')->getTranslationNodeVisitor();
+ $visitor->enable();
+
+ $this->twig->parse($this->twig->tokenize($template));
+
+ foreach ($visitor->getMessages() as $message) {
+ $catalogue->set(trim($message[0]), $this->prefix.trim($message[0]), $message[1] ? $message[1] : $this->defaultDomain);
+ }
+
+ $visitor->disable();
+ }
+}
--- /dev/null
+<?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\Bridge\Twig;
+
+use Symfony\Component\Templating\EngineInterface;
+use Symfony\Component\Templating\StreamingEngineInterface;
+use Symfony\Component\Templating\TemplateNameParserInterface;
+
+/**
+ * This engine knows how to render Twig templates.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class TwigEngine implements EngineInterface, StreamingEngineInterface
+{
+ protected $environment;
+ protected $parser;
+
+ /**
+ * Constructor.
+ *
+ * @param \Twig_Environment $environment A \Twig_Environment instance
+ * @param TemplateNameParserInterface $parser A TemplateNameParserInterface instance
+ */
+ public function __construct(\Twig_Environment $environment, TemplateNameParserInterface $parser)
+ {
+ $this->environment = $environment;
+ $this->parser = $parser;
+ }
+
+ /**
+ * Renders a template.
+ *
+ * @param mixed $name A template name
+ * @param array $parameters An array of parameters to pass to the template
+ *
+ * @return string The evaluated template as a string
+ *
+ * @throws \InvalidArgumentException if the template does not exist
+ * @throws \RuntimeException if the template cannot be rendered
+ */
+ public function render($name, array $parameters = array())
+ {
+ return $this->load($name)->render($parameters);
+ }
+
+ /**
+ * Streams a template.
+ *
+ * @param mixed $name A template name or a TemplateReferenceInterface instance
+ * @param array $parameters An array of parameters to pass to the template
+ *
+ * @throws \RuntimeException if the template cannot be rendered
+ */
+ public function stream($name, array $parameters = array())
+ {
+ $this->load($name)->display($parameters);
+ }
+
+ /**
+ * Returns true if the template exists.
+ *
+ * @param mixed $name A template name
+ *
+ * @return Boolean true if the template exists, false otherwise
+ */
+ public function exists($name)
+ {
+ try {
+ $this->load($name);
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if this class is able to render the given template.
+ *
+ * @param string $name A template name
+ *
+ * @return Boolean True if this class supports the given resource, false otherwise
+ */
+ public function supports($name)
+ {
+ if ($name instanceof \Twig_Template) {
+ return true;
+ }
+
+ $template = $this->parser->parse($name);
+
+ return 'twig' === $template->get('engine');
+ }
+
+ /**
+ * Loads the given template.
+ *
+ * @param mixed $name A template name or an instance of Twig_Template
+ *
+ * @return \Twig_TemplateInterface A \Twig_TemplateInterface instance
+ *
+ * @throws \InvalidArgumentException if the template does not exist
+ */
+ protected function load($name)
+ {
+ if ($name instanceof \Twig_Template) {
+ return $name;
+ }
+
+ try {
+ return $this->environment->loadTemplate($name);
+ } catch (\Twig_Error_Loader $e) {
+ throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+}
--- /dev/null
+{
+ "name": "symfony/twig-bridge",
+ "type": "symfony-bridge",
+ "description": "Symfony Twig Bridge",
+ "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",
+ "twig/twig": "~1.11"
+ },
+ "require-dev": {
+ "symfony/form": "2.2.*",
+ "symfony/http-kernel": "~2.2",
+ "symfony/routing": "~2.2",
+ "symfony/templating": "~2.1",
+ "symfony/translation": "~2.2",
+ "symfony/yaml": "~2.0",
+ "symfony/security": "~2.0"
+ },
+ "suggest": {
+ "symfony/form": "",
+ "symfony/http-kernel": "",
+ "symfony/routing": "",
+ "symfony/templating": "",
+ "symfony/translation": "",
+ "symfony/yaml": "",
+ "symfony/security": ""
+ },
+ "autoload": {
+ "psr-0": { "Symfony\\Bridge\\Twig\\": "" }
+ },
+ "target-dir": "Symfony/Bridge/Twig",
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
--- /dev/null
+<?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 Twig Bridge Test Suite">
+ <directory>./Tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./</directory>
+ <exclude>
+ <directory>./Resources</directory>
+ <directory>./Tests</directory>
+ <directory>./vendor</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+Subproject commit f5b0c84f3699e494c84ee627d7d583e115d2c4a2
--- /dev/null
+; top-most EditorConfig file
+root = true
+
+; Unix-style newlines
+[*]
+end_of_line = LF
+
+[*.php]
+indent_style = space
+indent_size = 4
+
+[*.test]
+indent_style = space
+indent_size = 4
+
+[*.rst]
+indent_style = space
+indent_size = 4
--- /dev/null
+/ext/twig/autom4te.cache/
+
--- /dev/null
+language: php
+
+php:
+ - 5.2
+ - 5.3
+ - 5.4
+ - 5.5
+
+env:
+ - TWIG_EXT=no
+ - TWIG_EXT=yes
+
+before_script:
+ - if [ "$TWIG_EXT" == "yes" ]; then sh -c "cd ext/twig && phpize && ./configure --enable-twig && make && sudo make install"; fi
+ - if [ "$TWIG_EXT" == "yes" ]; then echo "extension=twig.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`; fi
--- /dev/null
+Twig is written and maintained by the Twig Team:
+
+Lead Developer:
+
+- Fabien Potencier <fabien.potencier@symfony-project.org>
+
+Project Founder:
+
+- Armin Ronacher <armin.ronacher@active-4.com>
--- /dev/null
+* 1.13.2 (2013-08-03)
+
+ * fixed the error line number for an error occurs in and embedded template
+ * fixed crashes of the C extension on some edge cases
+
+* 1.13.1 (2013-06-06)
+
+ * added the possibility to ignore the filesystem constructor argument in Twig_Loader_Filesystem
+ * fixed Twig_Loader_Chain::exists() for a loader which implements Twig_ExistsLoaderInterface
+ * adjusted backtrace call to reduce memory usage when an error occurs
+ * added support for object instances as the second argument of the constant test
+ * fixed the include function when used in an assignment
+
+* 1.13.0 (2013-05-10)
+
+ * fixed getting a numeric-like item on a variable ('09' for instance)
+ * fixed getting a boolean or float key on an array, so it is consistent with PHP's array access:
+ `{{ array[false] }}` behaves the same as `echo $array[false];` (equals `$array[0]`)
+ * made the escape filter 20% faster for happy path (escaping string for html with UTF-8)
+ * changed ☃ to § in tests
+ * enforced usage of named arguments after positional ones
+
+* 1.12.3 (2013-04-08)
+
+ * fixed a security issue in the filesystem loader where it was possible to include a template one
+ level above the configured path
+ * fixed fatal error that should be an exception when adding a filter/function/test too late
+ * added a batch filter
+ * added support for encoding an array as query string in the url_encode filter
+
+* 1.12.2 (2013-02-09)
+
+ * fixed the timezone used by the date filter and function when the given date contains a timezone (like 2010-01-28T15:00:00+02:00)
+ * fixed globals when getGlobals is called early on
+ * added the first and last filter
+
+* 1.12.1 (2013-01-15)
+
+ * added support for object instances as the second argument of the constant function
+ * relaxed globals management to avoid a BC break
+ * added support for {{ some_string[:2] }}
+
+* 1.12.0 (2013-01-08)
+
+ * added verbatim as an alias for the raw tag to avoid confusion with the raw filter
+ * fixed registration of tests and functions as anonymous functions
+ * fixed globals management
+
+* 1.12.0-RC1 (2012-12-29)
+
+ * added an include function (does the same as the include tag but in a more flexible way)
+ * added the ability to use any PHP callable to define filters, functions, and tests
+ * added a syntax error when using a loop variable that is not defined
+ * added the ability to set default values for macro arguments
+ * added support for named arguments for filters, tests, and functions
+ * moved filters/functions/tests syntax errors to the parser
+ * added support for extended ternary operator syntaxes
+
+* 1.11.1 (2012-11-11)
+
+ * fixed debug info line numbering (was off by 2)
+ * fixed escaping when calling a macro inside another one (regression introduced in 1.9.1)
+ * optimized variable access on PHP 5.4
+ * fixed a crash of the C extension when an exception was thrown from a macro called without being imported (using _self.XXX)
+
+* 1.11.0 (2012-11-07)
+
+ * fixed macro compilation when a variable name is a PHP reserved keyword
+ * changed the date filter behavior to always apply the default timezone, except if false is passed as the timezone
+ * fixed bitwise operator precedences
+ * added the template_from_string function
+ * fixed default timezone usage for the date function
+ * optimized the way Twig exceptions are managed (to make them faster)
+ * added Twig_ExistsLoaderInterface (implementing this interface in your loader make the chain loader much faster)
+
+* 1.10.3 (2012-10-19)
+
+ * fixed wrong template location in some error messages
+ * reverted a BC break introduced in 1.10.2
+ * added a split filter
+
+* 1.10.2 (2012-10-15)
+
+ * fixed macro calls on PHP 5.4
+
+* 1.10.1 (2012-10-15)
+
+ * made a speed optimization to macro calls when imported via the "import" tag
+ * fixed C extension compilation on Windows
+ * fixed a segfault in the C extension when using DateTime objects
+
+* 1.10.0 (2012-09-28)
+
+ * extracted functional tests framework to make it reusable for third-party extensions
+ * added namespaced templates support in Twig_Loader_Filesystem
+ * added Twig_Loader_Filesystem::prependPath()
+ * fixed an error when a token parser pass a closure as a test to the subparse() method
+
+* 1.9.2 (2012-08-25)
+
+ * fixed the in operator for objects that contain circular references
+ * fixed the C extension when accessing a public property of an object implementing the \ArrayAccess interface
+
+* 1.9.1 (2012-07-22)
+
+ * optimized macro calls when auto-escaping is on
+ * fixed wrong parent class for Twig_Function_Node
+ * made Twig_Loader_Chain more explicit about problems
+
+* 1.9.0 (2012-07-13)
+
+ * made the parsing independent of the template loaders
+ * fixed exception trace when an error occurs when rendering a child template
+ * added escaping strategies for CSS, URL, and HTML attributes
+ * fixed nested embed tag calls
+ * added the date_modify filter
+
+* 1.8.3 (2012-06-17)
+
+ * fixed paths in the filesystem loader when passing a path that ends with a slash or a backslash
+ * fixed escaping when a project defines a function named html or js
+ * fixed chmod mode to apply the umask correctly
+
+* 1.8.2 (2012-05-30)
+
+ * added the abs filter
+ * fixed a regression when using a number in template attributes
+ * fixed compiler when mbstring.func_overload is set to 2
+ * fixed DateTimeZone support in date filter
+
+* 1.8.1 (2012-05-17)
+
+ * fixed a regression when dealing with SimpleXMLElement instances in templates
+ * fixed "is_safe" value for the "dump" function when "html_errors" is not defined in php.ini
+ * switched to use mbstring whenever possible instead of iconv (you might need to update your encoding as mbstring and iconv encoding names sometimes differ)
+
+* 1.8.0 (2012-05-08)
+
+ * enforced interface when adding tests, filters, functions, and node visitors from extensions
+ * fixed a side-effect of the date filter where the timezone might be changed
+ * simplified usage of the autoescape tag; the only (optional) argument is now the escaping strategy or false (with a BC layer)
+ * added a way to dynamically change the auto-escaping strategy according to the template "filename"
+ * changed the autoescape option to also accept a supported escaping strategy (for BC, true is equivalent to html)
+ * added an embed tag
+
+* 1.7.0 (2012-04-24)
+
+ * fixed a PHP warning when using CIFS
+ * fixed template line number in some exceptions
+ * added an iterable test
+ * added an error when defining two blocks with the same name in a template
+ * added the preserves_safety option for filters
+ * fixed a PHP notice when trying to access a key on a non-object/array variable
+ * enhanced error reporting when the template file is an instance of SplFileInfo
+ * added Twig_Environment::mergeGlobals()
+ * added compilation checks to avoid misuses of the sandbox tag
+ * fixed filesystem loader freshness logic for high traffic websites
+ * fixed random function when charset is null
+
+* 1.6.5 (2012-04-11)
+
+ * fixed a regression when a template only extends another one without defining any blocks
+
+* 1.6.4 (2012-04-02)
+
+ * fixed PHP notice in Twig_Error::guessTemplateLine() introduced in 1.6.3
+ * fixed performance when compiling large files
+ * optimized parent template creation when the template does not use dynamic inheritance
+
+* 1.6.3 (2012-03-22)
+
+ * fixed usage of Z_ADDREF_P for PHP 5.2 in the C extension
+ * fixed compilation of numeric values used in templates when using a locale where the decimal separator is not a dot
+ * made the strategy used to guess the real template file name and line number in exception messages much faster and more accurate
+
+* 1.6.2 (2012-03-18)
+
+ * fixed sandbox mode when used with inheritance
+ * added preserveKeys support for the slice filter
+ * fixed the date filter when a DateTime instance is passed with a specific timezone
+ * added a trim filter
+
+* 1.6.1 (2012-02-29)
+
+ * fixed Twig C extension
+ * removed the creation of Twig_Markup instances when not needed
+ * added a way to set the default global timezone for dates
+ * fixed the slice filter on strings when the length is not specified
+ * fixed the creation of the cache directory in case of a race condition
+
+* 1.6.0 (2012-02-04)
+
+ * fixed raw blocks when used with the whitespace trim option
+ * made a speed optimization to macro calls when imported via the "from" tag
+ * fixed globals, parsers, visitors, filters, tests, and functions management in Twig_Environment when a new one or new extension is added
+ * fixed the attribute function when passing arguments
+ * added slice notation support for the [] operator (syntactic sugar for the slice operator)
+ * added a slice filter
+ * added string support for the reverse filter
+ * fixed the empty test and the length filter for Twig_Markup instances
+ * added a date function to ease date comparison
+ * fixed unary operators precedence
+ * added recursive parsing support in the parser
+ * added string and integer handling for the random function
+
+* 1.5.1 (2012-01-05)
+
+ * fixed a regression when parsing strings
+
+* 1.5.0 (2012-01-04)
+
+ * added Traversable objects support for the join filter
+
+* 1.5.0-RC2 (2011-12-30)
+
+ * added a way to set the default global date interval format
+ * fixed the date filter for DateInterval instances (setTimezone() does not exist for them)
+ * refactored Twig_Template::display() to ease its extension
+ * added a number_format filter
+
+* 1.5.0-RC1 (2011-12-26)
+
+ * removed the need to quote hash keys
+ * allowed hash keys to be any expression
+ * added a do tag
+ * added a flush tag
+ * added support for dynamically named filters and functions
+ * added a dump function to help debugging templates
+ * added a nl2br filter
+ * added a random function
+ * added a way to change the default format for the date filter
+ * fixed the lexer when an operator ending with a letter ends a line
+ * added string interpolation support
+ * enhanced exceptions for unknown filters, functions, tests, and tags
+
+* 1.4.0 (2011-12-07)
+
+ * fixed lexer when using big numbers (> PHP_INT_MAX)
+ * added missing preserveKeys argument to the reverse filter
+ * fixed macros containing filter tag calls
+
+* 1.4.0-RC2 (2011-11-27)
+
+ * removed usage of Reflection in Twig_Template::getAttribute()
+ * added a C extension that can optionally replace Twig_Template::getAttribute()
+ * added negative timestamp support to the date filter
+
+* 1.4.0-RC1 (2011-11-20)
+
+ * optimized variable access when using PHP 5.4
+ * changed the precedence of the .. operator to be more consistent with languages that implements such a feature like Ruby
+ * added an Exception to Twig_Loader_Array::isFresh() method when the template does not exist to be consistent with other loaders
+ * added Twig_Function_Node to allow more complex functions to have their own Node class
+ * added Twig_Filter_Node to allow more complex filters to have their own Node class
+ * added Twig_Test_Node to allow more complex tests to have their own Node class
+ * added a better error message when a template is empty but contain a BOM
+ * fixed "in" operator for empty strings
+ * fixed the "defined" test and the "default" filter (now works with more than one call (foo.bar.foo) and for both values of the strict_variables option)
+ * changed the way extensions are loaded (addFilter/addFunction/addGlobal/addTest/addNodeVisitor/addTokenParser/addExtension can now be called in any order)
+ * added Twig_Environment::display()
+ * made the escape filter smarter when the encoding is not supported by PHP
+ * added a convert_encoding filter
+ * moved all node manipulations outside the compile() Node method
+ * made several speed optimizations
+
+* 1.3.0 (2011-10-08)
+
+no changes
+
+* 1.3.0-RC1 (2011-10-04)
+
+ * added an optimization for the parent() function
+ * added cache reloading when auto_reload is true and an extension has been modified
+ * added the possibility to force the escaping of a string already marked as safe (instance of Twig_Markup)
+ * allowed empty templates to be used as traits
+ * added traits support for the "parent" function
+
+* 1.2.0 (2011-09-13)
+
+no changes
+
+* 1.2.0-RC1 (2011-09-10)
+
+ * enhanced the exception when a tag remains unclosed
+ * added support for empty Countable objects for the "empty" test
+ * fixed algorithm that determines if a template using inheritance is valid (no output between block definitions)
+ * added better support for encoding problems when escaping a string (available as of PHP 5.4)
+ * added a way to ignore a missing template when using the "include" tag ({% include "foo" ignore missing %})
+ * added support for an array of templates to the "include" and "extends" tags ({% include ['foo', 'bar'] %})
+ * added support for bitwise operators in expressions
+ * added the "attribute" function to allow getting dynamic attributes on variables
+ * added Twig_Loader_Chain
+ * added Twig_Loader_Array::setTemplate()
+ * added an optimization for the set tag when used to capture a large chunk of static text
+ * changed name regex to match PHP one "[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*" (works for blocks, tags, functions, filters, and macros)
+ * removed the possibility to use the "extends" tag from a block
+ * added "if" modifier support to "for" loops
+
+* 1.1.2 (2011-07-30)
+
+ * fixed json_encode filter on PHP 5.2
+ * fixed regression introduced in 1.1.1 ({{ block(foo|lower) }})
+ * fixed inheritance when using conditional parents
+ * fixed compilation of templates when the body of a child template is not empty
+ * fixed output when a macro throws an exception
+ * fixed a parsing problem when a large chunk of text is enclosed in a comment tag
+ * added PHPDoc for all Token parsers and Core extension functions
+
+* 1.1.1 (2011-07-17)
+
+ * added a performance optimization in the Optimizer (also helps to lower the number of nested level calls)
+ * made some performance improvement for some edge cases
+
+* 1.1.0 (2011-06-28)
+
+ * fixed json_encode filter
+
+* 1.1.0-RC3 (2011-06-24)
+
+ * fixed method case-sensitivity when using the sandbox mode
+ * added timezone support for the date filter
+ * fixed possible security problems with NUL bytes
+
+* 1.1.0-RC2 (2011-06-16)
+
+ * added an exception when the template passed to "use" is not a string
+ * made 'a.b is defined' not throw an exception if a is not defined (in strict mode)
+ * added {% line \d+ %} directive
+
+* 1.1.0-RC1 (2011-05-28)
+
+Flush your cache after upgrading.
+
+ * fixed date filter when using a timestamp
+ * fixed the defined test for some cases
+ * fixed a parsing problem when a large chunk of text is enclosed in a raw tag
+ * added support for horizontal reuse of template blocks (see docs for more information)
+ * added whitespace control modifier to all tags (see docs for more information)
+ * added null as an alias for none (the null test is also an alias for the none test now)
+ * made TRUE, FALSE, NONE equivalent to their lowercase counterparts
+ * wrapped all compilation and runtime exceptions with Twig_Error_Runtime and added logic to guess the template name and line
+ * moved display() method to Twig_Template (generated templates should now use doDisplay() instead)
+
+* 1.0.0 (2011-03-27)
+
+ * fixed output when using mbstring
+ * fixed duplicate call of methods when using the sandbox
+ * made the charset configurable for the escape filter
+
+* 1.0.0-RC2 (2011-02-21)
+
+ * changed the way {% set %} works when capturing (the content is now marked as safe)
+ * added support for macro name in the endmacro tag
+ * make Twig_Error compatible with PHP 5.3.0 >
+ * fixed an infinite loop on some Windows configurations
+ * fixed the "length" filter for numbers
+ * fixed Template::getAttribute() as properties in PHP are case sensitive
+ * removed coupling between Twig_Node and Twig_Template
+ * fixed the ternary operator precedence rule
+
+* 1.0.0-RC1 (2011-01-09)
+
+Backward incompatibilities:
+
+ * the "items" filter, which has been deprecated for quite a long time now, has been removed
+ * the "range" filter has been converted to a function: 0|range(10) -> range(0, 10)
+ * the "constant" filter has been converted to a function: {{ some_date|date('DATE_W3C'|constant) }} -> {{ some_date|date(constant('DATE_W3C')) }}
+ * the "cycle" filter has been converted to a function: {{ ['odd', 'even']|cycle(i) }} -> {{ cycle(['odd', 'even'], i) }}
+ * the "for" tag does not support "joined by" anymore
+ * the "autoescape" first argument is now "true"/"false" (instead of "on"/"off")
+ * the "parent" tag has been replaced by a "parent" function ({{ parent() }} instead of {% parent %})
+ * the "display" tag has been replaced by a "block" function ({{ block('title') }} instead of {% display title %})
+ * removed the grammar and simple token parser (moved to the Twig Extensions repository)
+
+Changes:
+
+ * added "needs_context" option for filters and functions (the context is then passed as a first argument)
+ * added global variables support
+ * made macros return their value instead of echoing directly (fixes calling a macro in sandbox mode)
+ * added the "from" tag to import macros as functions
+ * added support for functions (a function is just syntactic sugar for a getAttribute() call)
+ * made macros callable when sandbox mode is enabled
+ * added an exception when a macro uses a reserved name
+ * the "default" filter now uses the "empty" test instead of just checking for null
+ * added the "empty" test
+
+* 0.9.10 (2010-12-16)
+
+Backward incompatibilities:
+
+ * The Escaper extension is enabled by default, which means that all displayed
+ variables are now automatically escaped. You can revert to the previous
+ behavior by removing the extension via $env->removeExtension('escaper')
+ or just set the 'autoescape' option to 'false'.
+ * removed the "without loop" attribute for the "for" tag (not needed anymore
+ as the Optimizer take care of that for most cases)
+ * arrays and hashes have now a different syntax
+ * arrays keep the same syntax with square brackets: [1, 2]
+ * hashes now use curly braces (["a": "b"] should now be written as {"a": "b"})
+ * support for "arrays with keys" and "hashes without keys" is not supported anymore ([1, "foo": "bar"] or {"foo": "bar", 1})
+ * the i18n extension is now part of the Twig Extensions repository
+
+Changes:
+
+ * added the merge filter
+ * removed 'is_escaper' option for filters (a left over from the previous version) -- you must use 'is_safe' now instead
+ * fixed usage of operators as method names (like is, in, and not)
+ * changed the order of execution for node visitors
+ * fixed default() filter behavior when used with strict_variables set to on
+ * fixed filesystem loader compatibility with PHAR files
+ * enhanced error messages when an unexpected token is parsed in an expression
+ * fixed filename not being added to syntax error messages
+ * added the autoescape option to enable/disable autoescaping
+ * removed the newline after a comment (mimics PHP behavior)
+ * added a syntax error exception when parent block is used on a template that does not extend another one
+ * made the Escaper extension enabled by default
+ * fixed sandbox extension when used with auto output escaping
+ * fixed escaper when wrapping a Twig_Node_Print (the original class must be preserved)
+ * added an Optimizer extension (enabled by default; optimizes "for" loops and "raw" filters)
+ * added priority to node visitors
+
+* 0.9.9 (2010-11-28)
+
+Backward incompatibilities:
+ * the self special variable has been renamed to _self
+ * the odd and even filters are now tests:
+ {{ foo|odd }} must now be written {{ foo is odd }}
+ * the "safe" filter has been renamed to "raw"
+ * in Node classes,
+ sub-nodes are now accessed via getNode() (instead of property access)
+ attributes via getAttribute() (instead of array access)
+ * the urlencode filter had been renamed to url_encode
+ * the include tag now merges the passed variables with the current context by default
+ (the old behavior is still possible by adding the "only" keyword)
+ * moved Exceptions to Twig_Error_* (Twig_SyntaxError/Twig_RuntimeError are now Twig_Error_Syntax/Twig_Error_Runtime)
+ * removed support for {{ 1 < i < 3 }} (use {{ i > 1 and i < 3 }} instead)
+ * the "in" filter has been removed ({{ a|in(b) }} should now be written {{ a in b }})
+
+Changes:
+ * added file and line to Twig_Error_Runtime exceptions thrown from Twig_Template
+ * changed trans tag to accept any variable for the plural count
+ * fixed sandbox mode (__toString() method check was not enforced if called implicitly from complex statements)
+ * added the ** (power) operator
+ * changed the algorithm used for parsing expressions
+ * added the spaceless tag
+ * removed trim_blocks option
+ * added support for is*() methods for attributes (foo.bar now looks for foo->getBar() or foo->isBar())
+ * changed all exceptions to extend Twig_Error
+ * fixed unary expressions ({{ not(1 or 0) }})
+ * fixed child templates (with an extend tag) that uses one or more imports
+ * added support for {{ 1 not in [2, 3] }} (more readable than the current {{ not (1 in [2, 3]) }})
+ * escaping has been rewritten
+ * the implementation of template inheritance has been rewritten
+ (blocks can now be called individually and still work with inheritance)
+ * fixed error handling for if tag when a syntax error occurs within a subparse process
+ * added a way to implement custom logic for resolving token parsers given a tag name
+ * fixed js escaper to be stricter (now uses a whilelist-based js escaper)
+ * added the following filers: "constant", "trans", "replace", "json_encode"
+ * added a "constant" test
+ * fixed objects with __toString() not being autoescaped
+ * fixed subscript expressions when calling __call() (methods now keep the case)
+ * added "test" feature (accessible via the "is" operator)
+ * removed the debug tag (should be done in an extension)
+ * fixed trans tag when no vars are used in plural form
+ * fixed race condition when writing template cache
+ * added the special _charset variable to reference the current charset
+ * added the special _context variable to reference the current context
+ * renamed self to _self (to avoid conflict)
+ * fixed Twig_Template::getAttribute() for protected properties
+
+* 0.9.8 (2010-06-28)
+
+Backward incompatibilities:
+ * the trans tag plural count is now attached to the plural tag:
+ old: `{% trans count %}...{% plural %}...{% endtrans %}`
+ new: `{% trans %}...{% plural count %}...{% endtrans %}`
+
+ * added a way to translate strings coming from a variable ({% trans var %})
+ * fixed trans tag when used with the Escaper extension
+ * fixed default cache umask
+ * removed Twig_Template instances from the debug tag output
+ * fixed objects with __isset() defined
+ * fixed set tag when used with a capture
+ * fixed type hinting for Twig_Environment::addFilter() method
+
+* 0.9.7 (2010-06-12)
+
+Backward incompatibilities:
+ * changed 'as' to '=' for the set tag ({% set title as "Title" %} must now be {% set title = "Title" %})
+ * removed the sandboxed attribute of the include tag (use the new sandbox tag instead)
+ * refactored the Node system (if you have custom nodes, you will have to update them to use the new API)
+
+ * added self as a special variable that refers to the current template (useful for importing macros from the current template)
+ * added Twig_Template instance support to the include tag
+ * added support for dynamic and conditional inheritance ({% extends some_var %} and {% extends standalone ? "minimum" : "base" %})
+ * added a grammar sub-framework to ease the creation of custom tags
+ * fixed the for tag for large arrays (some loop variables are now only available for arrays and objects that implement the Countable interface)
+ * removed the Twig_Resource::resolveMissingFilter() method
+ * fixed the filter tag which did not apply filtering to included files
+ * added a bunch of unit tests
+ * added a bunch of phpdoc
+ * added a sandbox tag in the sandbox extension
+ * changed the date filter to support any date format supported by DateTime
+ * added strict_variable setting to throw an exception when an invalid variable is used in a template (disabled by default)
+ * added the lexer, parser, and compiler as arguments to the Twig_Environment constructor
+ * changed the cache option to only accepts an explicit path to a cache directory or false
+ * added a way to add token parsers, filters, and visitors without creating an extension
+ * added three interfaces: Twig_NodeInterface, Twig_TokenParserInterface, and Twig_FilterInterface
+ * changed the generated code to match the new coding standards
+ * fixed sandbox mode (__toString() method check was not enforced if called implicitly from a simple statement like {{ article }})
+ * added an exception when a child template has a non-empty body (as it is always ignored when rendering)
+
+* 0.9.6 (2010-05-12)
+
+ * fixed variables defined outside a loop and for which the value changes in a for loop
+ * fixed the test suite for PHP 5.2 and older versions of PHPUnit
+ * added support for __call() in expression resolution
+ * fixed node visiting for macros (macros are now visited by visitors as any other node)
+ * fixed nested block definitions with a parent call (rarely useful but nonetheless supported now)
+ * added the cycle filter
+ * fixed the Lexer when mbstring.func_overload is used with an mbstring.internal_encoding different from ASCII
+ * added a long-syntax for the set tag ({% set foo %}...{% endset %})
+ * unit tests are now powered by PHPUnit
+ * added support for gettext via the `i18n` extension
+ * fixed twig_capitalize_string_filter() and fixed twig_length_filter() when used with UTF-8 values
+ * added a more useful exception if an if tag is not closed properly
+ * added support for escaping strategy in the autoescape tag
+ * fixed lexer when a template has a big chunk of text between/in a block
+
+* 0.9.5 (2010-01-20)
+
+As for any new release, don't forget to remove all cached templates after
+upgrading.
+
+If you have defined custom filters, you MUST upgrade them for this release. To
+upgrade, replace "array" with "new Twig_Filter_Function", and replace the
+environment constant by the "needs_environment" option:
+
+ // before
+ 'even' => array('twig_is_even_filter', false),
+ 'escape' => array('twig_escape_filter', true),
+
+ // after
+ 'even' => new Twig_Filter_Function('twig_is_even_filter'),
+ 'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true)),
+
+If you have created NodeTransformer classes, you will need to upgrade them to
+the new interface (please note that the interface is not yet considered
+stable).
+
+ * fixed list nodes that did not extend the Twig_NodeListInterface
+ * added the "without loop" option to the for tag (it disables the generation of the loop variable)
+ * refactored node transformers to node visitors
+ * fixed automatic-escaping for blocks
+ * added a way to specify variables to pass to an included template
+ * changed the automatic-escaping rules to be more sensible and more configurable in custom filters (the documentation lists all the rules)
+ * improved the filter system to allow object methods to be used as filters
+ * changed the Array and String loaders to actually make use of the cache mechanism
+ * included the default filter function definitions in the extension class files directly (Core, Escaper)
+ * added the // operator (like the floor() PHP function)
+ * added the .. operator (as a syntactic sugar for the range filter when the step is 1)
+ * added the in operator (as a syntactic sugar for the in filter)
+ * added the following filters in the Core extension: in, range
+ * added support for arrays (same behavior as in PHP, a mix between lists and dictionaries, arrays and hashes)
+ * enhanced some error messages to provide better feedback in case of parsing errors
+
+* 0.9.4 (2009-12-02)
+
+If you have custom loaders, you MUST upgrade them for this release: The
+Twig_Loader base class has been removed, and the Twig_LoaderInterface has also
+been changed (see the source code for more information or the documentation).
+
+ * added support for DateTime instances for the date filter
+ * fixed loop.last when the array only has one item
+ * made it possible to insert newlines in tag and variable blocks
+ * fixed a bug when a literal '\n' were present in a template text
+ * fixed bug when the filename of a template contains */
+ * refactored loaders
+
+* 0.9.3 (2009-11-11)
+
+This release is NOT backward compatible with the previous releases.
+
+ The loaders do not take the cache and autoReload arguments anymore. Instead,
+ the Twig_Environment class has two new options: cache and auto_reload.
+ Upgrading your code means changing this kind of code:
+
+ $loader = new Twig_Loader_Filesystem('/path/to/templates', '/path/to/compilation_cache', true);
+ $twig = new Twig_Environment($loader);
+
+ to something like this:
+
+ $loader = new Twig_Loader_Filesystem('/path/to/templates');
+ $twig = new Twig_Environment($loader, array(
+ 'cache' => '/path/to/compilation_cache',
+ 'auto_reload' => true,
+ ));
+
+ * deprecated the "items" filter as it is not needed anymore
+ * made cache and auto_reload options of Twig_Environment instead of arguments of Twig_Loader
+ * optimized template loading speed
+ * removed output when an error occurs in a template and render() is used
+ * made major speed improvements for loops (up to 300% on even the smallest loops)
+ * added properties as part of the sandbox mode
+ * added public properties support (obj.item can now be the item property on the obj object)
+ * extended set tag to support expression as value ({% set foo as 'foo' ~ 'bar' %} )
+ * fixed bug when \ was used in HTML
+
+* 0.9.2 (2009-10-29)
+
+ * made some speed optimizations
+ * changed the cache extension to .php
+ * added a js escaping strategy
+ * added support for short block tag
+ * changed the filter tag to allow chained filters
+ * made lexer more flexible as you can now change the default delimiters
+ * added set tag
+ * changed default directory permission when cache dir does not exist (more secure)
+ * added macro support
+ * changed filters first optional argument to be a Twig_Environment instance instead of a Twig_Template instance
+ * made Twig_Autoloader::autoload() a static method
+ * avoid writing template file if an error occurs
+ * added $ escaping when outputting raw strings
+ * enhanced some error messages to ease debugging
+ * fixed empty cache files when the template contains an error
+
+* 0.9.1 (2009-10-14)
+
+ * fixed a bug in PHP 5.2.6
+ * fixed numbers with one than one decimal
+ * added support for method calls with arguments ({{ foo.bar('a', 43) }})
+ * made small speed optimizations
+ * made minor tweaks to allow better extensibility and flexibility
+
+* 0.9.0 (2009-10-12)
+
+ * Initial release
--- /dev/null
+Copyright (c) 2009-2013 by the Twig Team, see AUTHORS for more details.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+Twig, the flexible, fast, and secure template language for PHP
+==============================================================
+
+[![Build Status](https://secure.travis-ci.org/fabpot/Twig.png?branch=master)](http://travis-ci.org/fabpot/Twig)
+
+Twig is a template language for PHP, released under the new BSD license (code
+and documentation).
+
+Twig uses a syntax similar to the Django and Jinja template languages which
+inspired the Twig runtime environment.
+
+More Information
+----------------
+
+Read the [documentation][1] for more information.
+
+[1]: http://twig.sensiolabs.org/documentation
--- /dev/null
+{
+ "name": "twig/twig",
+ "type": "library",
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "keywords": ["templating"],
+ "homepage": "http://twig.sensiolabs.org",
+ "license": "BSD-3-Clause",
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.2.4"
+ },
+ "autoload": {
+ "psr-0" : {
+ "Twig_" : "lib/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.13-dev"
+ }
+ }
+}
--- /dev/null
+Extending Twig
+==============
+
+.. caution::
+
+ This section describes how to extend Twig as of **Twig 1.12**. If you are
+ using an older version, read the :doc:`legacy<advanced_legacy>` chapter
+ instead.
+
+Twig can be extended in many ways; you can add extra tags, filters, tests,
+operators, global variables, and functions. You can even extend the parser
+itself with node visitors.
+
+.. note::
+
+ The first section of this chapter describes how to extend Twig easily. If
+ you want to reuse your changes in different projects or if you want to
+ share them with others, you should then create an extension as described
+ in the following section.
+
+.. caution::
+
+ When extending Twig without creating an extension, Twig won't be able to
+ recompile your templates when the PHP code is updated. To see your changes
+ in real-time, either disable template caching or package your code into an
+ extension (see the next section of this chapter).
+
+Before extending Twig, you must understand the differences between all the
+different possible extension points and when to use them.
+
+First, remember that Twig has two main language constructs:
+
+* ``{{ }}``: used to print the result of an expression evaluation;
+
+* ``{% %}``: used to execute statements.
+
+To understand why Twig exposes so many extension points, let's see how to
+implement a *Lorem ipsum* generator (it needs to know the number of words to
+generate).
+
+You can use a ``lipsum`` *tag*:
+
+.. code-block:: jinja
+
+ {% lipsum 40 %}
+
+That works, but using a tag for ``lipsum`` is not a good idea for at least
+three main reasons:
+
+* ``lipsum`` is not a language construct;
+* The tag outputs something;
+* The tag is not flexible as you cannot use it in an expression:
+
+ .. code-block:: jinja
+
+ {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
+
+In fact, you rarely need to create tags; and that's good news because tags are
+the most complex extension point of Twig.
+
+Now, let's use a ``lipsum`` *filter*:
+
+.. code-block:: jinja
+
+ {{ 40|lipsum }}
+
+Again, it works, but it looks weird. A filter transforms the passed value to
+something else but here we use the value to indicate the number of words to
+generate (so, ``40`` is an argument of the filter, not the value we want to
+transform).
+
+Next, let's use a ``lipsum`` *function*:
+
+.. code-block:: jinja
+
+ {{ lipsum(40) }}
+
+Here we go. For this specific example, the creation of a function is the
+extension point to use. And you can use it anywhere an expression is accepted:
+
+.. code-block:: jinja
+
+ {{ 'some text' ~ lipsum(40) ~ 'some more text' }}
+
+ {% set lipsum = lipsum(40) %}
+
+Last but not the least, you can also use a *global* object with a method able
+to generate lorem ipsum text:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+As a rule of thumb, use functions for frequently used features and global
+objects for everything else.
+
+Keep in mind the following when you want to extend Twig:
+
+========== ========================== ========== =========================
+What? Implementation difficulty? How often? When?
+========== ========================== ========== =========================
+*macro* trivial frequent Content generation
+*global* trivial frequent Helper object
+*function* trivial frequent Content generation
+*filter* trivial frequent Value transformation
+*tag* complex rare DSL language construct
+*test* trivial rare Boolean decision
+*operator* trivial rare Values transformation
+========== ========================== ========== =========================
+
+Globals
+-------
+
+A global variable is like any other template variable, except that it's
+available in all templates and macros::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addGlobal('text', new Text());
+
+You can then use the ``text`` variable anywhere in a template:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+Filters
+-------
+
+Creating a filter is as simple as associating a name with a PHP callable::
+
+ // an anonymous function
+ $filter = new Twig_SimpleFilter('rot13', function ($string) {
+ return str_rot13($string);
+ });
+
+ // or a simple PHP function
+ $filter = new Twig_SimpleFilter('rot13', 'str_rot13');
+
+ // or a class method
+ $filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));
+
+The first argument passed to the ``Twig_SimpleFilter`` constructor is the name
+of the filter you will use in templates and the second one is the PHP callable
+to associate with it.
+
+Then, add the filter to your Twig environment::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFilter($filter);
+
+And here is how to use it in a template:
+
+.. code-block:: jinja
+
+ {{ 'Twig'|rot13 }}
+
+ {# will output Gjvt #}
+
+When called by Twig, the PHP callable receives the left side of the filter
+(before the pipe ``|``) as the first argument and the extra arguments passed
+to the filter (within parentheses ``()``) as extra arguments.
+
+For instance, the following code:
+
+.. code-block:: jinja
+
+ {{ 'TWIG'|lower }}
+ {{ now|date('d/m/Y') }}
+
+is compiled to something like the following::
+
+ <?php echo strtolower('TWIG') ?>
+ <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
+
+The ``Twig_SimpleFilter`` class takes an array of options as its last
+argument::
+
+ $filter = new Twig_SimpleFilter('rot13', 'str_rot13', $options);
+
+Environment aware Filters
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want to access the current environment instance in your filter, set the
+``needs_environment`` option to ``true``; Twig will pass the current
+environment as the first argument to the filter call::
+
+ $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $string) {
+ // get the current charset for instance
+ $charset = $env->getCharset();
+
+ return str_rot13($string);
+ }, array('needs_environment' => true));
+
+Context aware Filters
+~~~~~~~~~~~~~~~~~~~~~
+
+If you want to access the current context in your filter, set the
+``needs_context`` option to ``true``; Twig will pass the current context as
+the first argument to the filter call (or the second one if
+``needs_environment`` is also set to ``true``)::
+
+ $filter = new Twig_SimpleFilter('rot13', function ($context, $string) {
+ // ...
+ }, array('needs_context' => true));
+
+ $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $context, $string) {
+ // ...
+ }, array('needs_context' => true, 'needs_environment' => true));
+
+Automatic Escaping
+~~~~~~~~~~~~~~~~~~
+
+If automatic escaping is enabled, the output of the filter may be escaped
+before printing. If your filter acts as an escaper (or explicitly outputs html
+or JavaScript code), you will want the raw output to be printed. In such a
+case, set the ``is_safe`` option::
+
+ $filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html')));
+
+Some filters may need to work on input that is already escaped or safe, for
+example when adding (safe) html tags to originally unsafe output. In such a
+case, set the ``pre_escape`` option to escape the input data before it is run
+through your filter::
+
+ $filter = new Twig_SimpleFilter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
+
+Dynamic Filters
+~~~~~~~~~~~~~~~
+
+A filter name containing the special ``*`` character is a dynamic filter as
+the ``*`` can be any string::
+
+ $filter = new Twig_SimpleFilter('*_path', function ($name, $arguments) {
+ // ...
+ });
+
+The following filters will be matched by the above defined dynamic filter:
+
+* ``product_path``
+* ``category_path``
+
+A dynamic filter can define more than one dynamic parts::
+
+ $filter = new Twig_SimpleFilter('*_path_*', function ($name, $suffix, $arguments) {
+ // ...
+ });
+
+The filter will receive all dynamic part values before the normal filter
+arguments, but after the environment and the context. For instance, a call to
+``'foo'|a_path_b()`` will result in the following arguments to be passed to
+the filter: ``('a', 'b', 'foo')``.
+
+Functions
+---------
+
+Functions are defined in the exact same way as filters, but you need to create
+an instance of ``Twig_SimpleFunction``::
+
+ $twig = new Twig_Environment($loader);
+ $function = new Twig_SimpleFunction('function_name', function () {
+ // ...
+ });
+ $twig->addFunction($function);
+
+Functions support the same features as filters, except for the ``pre_escape``
+and ``preserves_safety`` options.
+
+Tests
+-----
+
+Tests are defined in the exact same way as filters and functions, but you need
+to create an instance of ``Twig_SimpleTest``::
+
+ $twig = new Twig_Environment($loader);
+ $test = new Twig_SimpleTest('test_name', function () {
+ // ...
+ });
+ $twig->addTest($test);
+
+Tests allow you to create custom application specific logic for evaluating
+boolean conditions. As a simple, example let's create a Twig test that checks if
+objects are 'red'::
+
+ $twig = new Twig_Environment($loader)
+ $test = new Twig_SimpleTest('red', function ($value) {
+ if (isset($value->color) && $value->color == 'red') {
+ return true;
+ }
+ if (isset($value->paint) && $value->paint == 'red') {
+ return true;
+ }
+ return false;
+ });
+ $twig->addTest($test);
+
+Test functions should always return true/false.
+
+When creating tests you can use the ``node_class`` option to provide custom test
+compilation. This is useful if your test can be compiled into PHP primitives.
+This is used by many of the tests built into Twig::
+
+ $twig = new Twig_Environment($loader)
+ $test = new Twig_SimpleTest(
+ 'odd',
+ null,
+ array('node_class' => 'Twig_Node_Expression_Test_Odd'));
+ $twig->addTest($test);
+
+ class Twig_Node_Expression_Test_Odd extends Twig_Node_Expression_Test
+ {
+ public function compile(Twig_Compiler $compiler)
+ {
+ $compiler
+ ->raw('(')
+ ->subcompile($this->getNode('node'))
+ ->raw(' % 2 == 1')
+ ->raw(')')
+ ;
+ }
+ }
+
+The above example, shows how you can create tests that use a node class. The
+node class has access to one sub-node called 'node'. This sub-node contains the
+value that is being tested. When the ``odd`` filter is used in code like:
+
+.. code-block:: jinja
+
+ {% if my_value is odd %}
+
+The ``node`` sub-node will contain an expression of ``my_value``. Node based
+tests also have access to the ``arguments`` node. This node will contain the
+various other arguments that have been provided to your test.
+
+Tags
+----
+
+One of the most exciting feature of a template engine like Twig is the
+possibility to define new language constructs. This is also the most complex
+feature as you need to understand how Twig's internals work.
+
+Let's create a simple ``set`` tag that allows the definition of simple
+variables from within a template. The tag can be used like follows:
+
+.. code-block:: jinja
+
+ {% set name = "value" %}
+
+ {{ name }}
+
+ {# should output value #}
+
+.. note::
+
+ The ``set`` tag is part of the Core extension and as such is always
+ available. The built-in version is slightly more powerful and supports
+ multiple assignments by default (cf. the template designers chapter for
+ more information).
+
+Three steps are needed to define a new tag:
+
+* Defining a Token Parser class (responsible for parsing the template code);
+
+* Defining a Node class (responsible for converting the parsed code to PHP);
+
+* Registering the tag.
+
+Registering a new tag
+~~~~~~~~~~~~~~~~~~~~~
+
+Adding a tag is as simple as calling the ``addTokenParser`` method on the
+``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addTokenParser(new Project_Set_TokenParser());
+
+Defining a Token Parser
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Now, let's see the actual code of this class::
+
+ class Project_Set_TokenParser extends Twig_TokenParser
+ {
+ public function parse(Twig_Token $token)
+ {
+ $parser = $this->parser;
+ $stream = $parser->getStream();
+
+ $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
+ $stream->expect(Twig_Token::OPERATOR_TYPE, '=');
+ $value = $parser->getExpressionParser()->parseExpression();
+ $stream->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
+ }
+
+ public function getTag()
+ {
+ return 'set';
+ }
+ }
+
+The ``getTag()`` method must return the tag we want to parse, here ``set``.
+
+The ``parse()`` method is invoked whenever the parser encounters a ``set``
+tag. It should return a ``Twig_Node`` instance that represents the node (the
+``Project_Set_Node`` calls creating is explained in the next section).
+
+The parsing process is simplified thanks to a bunch of methods you can call
+from the token stream (``$this->parser->getStream()``):
+
+* ``getCurrent()``: Gets the current token in the stream.
+
+* ``next()``: Moves to the next token in the stream, *but returns the old one*.
+
+* ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether
+ the current token is of a particular type or value (or both). The value may be an
+ array of several possible values.
+
+* ``expect($type[, $value[, $message]])``: If the current token isn't of the given
+ type/value a syntax error is thrown. Otherwise, if the type and value are correct,
+ the token is returned and the stream moves to the next token.
+
+* ``look()``: Looks a the next token without consuming it.
+
+Parsing expressions is done by calling the ``parseExpression()`` like we did for
+the ``set`` tag.
+
+.. tip::
+
+ Reading the existing ``TokenParser`` classes is the best way to learn all
+ the nitty-gritty details of the parsing process.
+
+Defining a Node
+~~~~~~~~~~~~~~~
+
+The ``Project_Set_Node`` class itself is rather simple::
+
+ class Project_Set_Node extends Twig_Node
+ {
+ public function __construct($name, Twig_Node_Expression $value, $line, $tag = null)
+ {
+ parent::__construct(array('value' => $value), array('name' => $name), $line, $tag);
+ }
+
+ public function compile(Twig_Compiler $compiler)
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$context[\''.$this->getAttribute('name').'\'] = ')
+ ->subcompile($this->getNode('value'))
+ ->raw(";\n")
+ ;
+ }
+ }
+
+The compiler implements a fluid interface and provides methods that helps the
+developer generate beautiful and readable PHP code:
+
+* ``subcompile()``: Compiles a node.
+
+* ``raw()``: Writes the given string as is.
+
+* ``write()``: Writes the given string by adding indentation at the beginning
+ of each line.
+
+* ``string()``: Writes a quoted string.
+
+* ``repr()``: Writes a PHP representation of a given value (see
+ ``Twig_Node_For`` for a usage example).
+
+* ``addDebugInfo()``: Adds the line of the original template file related to
+ the current node as a comment.
+
+* ``indent()``: Indents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+* ``outdent()``: Outdents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+.. _creating_extensions:
+
+Creating an Extension
+---------------------
+
+The main motivation for writing an extension is to move often used code into a
+reusable class like adding support for internationalization. An extension can
+define tags, filters, tests, operators, global variables, functions, and node
+visitors.
+
+Creating an extension also makes for a better separation of code that is
+executed at compilation time and code needed at runtime. As such, it makes
+your code faster.
+
+Most of the time, it is useful to create a single extension for your project,
+to host all the specific tags and filters you want to add to Twig.
+
+.. tip::
+
+ When packaging your code into an extension, Twig is smart enough to
+ recompile your templates whenever you make a change to it (when
+ ``auto_reload`` is enabled).
+
+.. note::
+
+ Before writing your own extensions, have a look at the Twig official
+ extension repository: http://github.com/fabpot/Twig-extensions.
+
+An extension is a class that implements the following interface::
+
+ interface Twig_ExtensionInterface
+ {
+ /**
+ * Initializes the runtime environment.
+ *
+ * This is where you can load some file that contains filter functions for instance.
+ *
+ * @param Twig_Environment $environment The current Twig_Environment instance
+ */
+ function initRuntime(Twig_Environment $environment);
+
+ /**
+ * Returns the token parser instances to add to the existing list.
+ *
+ * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
+ */
+ function getTokenParsers();
+
+ /**
+ * Returns the node visitor instances to add to the existing list.
+ *
+ * @return array An array of Twig_NodeVisitorInterface instances
+ */
+ function getNodeVisitors();
+
+ /**
+ * Returns a list of filters to add to the existing list.
+ *
+ * @return array An array of filters
+ */
+ function getFilters();
+
+ /**
+ * Returns a list of tests to add to the existing list.
+ *
+ * @return array An array of tests
+ */
+ function getTests();
+
+ /**
+ * Returns a list of functions to add to the existing list.
+ *
+ * @return array An array of functions
+ */
+ function getFunctions();
+
+ /**
+ * Returns a list of operators to add to the existing list.
+ *
+ * @return array An array of operators
+ */
+ function getOperators();
+
+ /**
+ * Returns a list of global variables to add to the existing list.
+ *
+ * @return array An array of global variables
+ */
+ function getGlobals();
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ function getName();
+ }
+
+To keep your extension class clean and lean, it can inherit from the built-in
+``Twig_Extension`` class instead of implementing the whole interface. That
+way, you just need to implement the ``getName()`` method as the
+``Twig_Extension`` provides empty implementations for all other methods.
+
+The ``getName()`` method must return a unique identifier for your extension.
+
+Now, with this information in mind, let's create the most basic extension
+possible::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+.. note::
+
+ Of course, this extension does nothing for now. We will customize it in
+ the next sections.
+
+Twig does not care where you save your extension on the filesystem, as all
+extensions must be registered explicitly to be available in your templates.
+
+You can register an extension by using the ``addExtension()`` method on your
+main ``Environment`` object::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new Project_Twig_Extension());
+
+Of course, you need to first load the extension file by either using
+``require_once()`` or by using an autoloader (see `spl_autoload_register()`_).
+
+.. tip::
+
+ The bundled extensions are great examples of how extensions work.
+
+Globals
+~~~~~~~
+
+Global variables can be registered in an extension via the ``getGlobals()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getGlobals()
+ {
+ return array(
+ 'text' => new Text(),
+ );
+ }
+
+ // ...
+ }
+
+Functions
+~~~~~~~~~
+
+Functions can be registered in an extension via the ``getFunctions()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFunctions()
+ {
+ return array(
+ new Twig_SimpleFunction('lipsum', 'generate_lipsum'),
+ );
+ }
+
+ // ...
+ }
+
+Filters
+~~~~~~~
+
+To add a filter to an extension, you need to override the ``getFilters()``
+method. This method must return an array of filters to add to the Twig
+environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ new Twig_SimpleFilter('rot13', 'str_rot13'),
+ );
+ }
+
+ // ...
+ }
+
+Tags
+~~~~
+
+Adding a tag in an extension can be done by overriding the
+``getTokenParsers()`` method. This method must return an array of tags to add
+to the Twig environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTokenParsers()
+ {
+ return array(new Project_Set_TokenParser());
+ }
+
+ // ...
+ }
+
+In the above code, we have added a single new tag, defined by the
+``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is
+responsible for parsing the tag and compiling it to PHP.
+
+Operators
+~~~~~~~~~
+
+The ``getOperators()`` methods allows to add new operators. Here is how to add
+``!``, ``||``, and ``&&`` operators::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getOperators()
+ {
+ return array(
+ array(
+ '!' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
+ ),
+ array(
+ '||' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ '&&' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ ),
+ );
+ }
+
+ // ...
+ }
+
+Tests
+~~~~~
+
+The ``getTests()`` methods allows to add new test functions::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTests()
+ {
+ return array(
+ new Twig_SimpleTest('even', 'twig_test_even'),
+ );
+ }
+
+ // ...
+ }
+
+Overloading
+-----------
+
+To overload an already defined filter, test, operator, global variable, or
+function, define it again **as late as possible**::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFilter(new Twig_SimpleFilter('date', function ($timestamp, $format = 'F j, Y H:i') {
+ // do something different from the built-in date filter
+ }));
+
+Here, we have overloaded the built-in ``date`` filter with a custom one.
+
+That also works with an extension::
+
+ class MyCoreExtension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ new Twig_SimpleFilter('date', array($this, 'dateFilter')),
+ );
+ }
+
+ public function dateFilter($timestamp, $format = 'F j, Y H:i')
+ {
+ // do something different from the built-in date filter
+ }
+
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new MyCoreExtension());
+
+.. caution::
+
+ Note that overloading the built-in Twig elements is not recommended as it
+ might be confusing.
+
+Testing an Extension
+--------------------
+
+Functional Tests
+~~~~~~~~~~~~~~~~
+
+You can create functional tests for extensions simply by creating the
+following file structure in your test directory::
+
+ Fixtures/
+ filters/
+ foo.test
+ bar.test
+ functions/
+ foo.test
+ bar.test
+ tags/
+ foo.test
+ bar.test
+ IntegrationTest.php
+
+The ``IntegrationTest.php`` file should look like this::
+
+ class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
+ {
+ public function getExtensions()
+ {
+ return array(
+ new Project_Twig_Extension1(),
+ new Project_Twig_Extension2(),
+ );
+ }
+
+ public function getFixturesDir()
+ {
+ return dirname(__FILE__).'/Fixtures/';
+ }
+ }
+
+Fixtures examples can be found within the Twig repository
+`tests/Twig/Fixtures`_ directory.
+
+Node Tests
+~~~~~~~~~~
+
+Testing the node visitors can be complex, so extend your test cases from
+``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository
+`tests/Twig/Node`_ directory.
+
+.. _`spl_autoload_register()`: http://www.php.net/spl_autoload_register
+.. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php
+.. _`tests/Twig/Fixtures`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Fixtures
+.. _`tests/Twig/Node`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Node
--- /dev/null
+Extending Twig
+==============
+
+.. caution::
+
+ This section describes how to extends Twig for versions **older than
+ 1.12**. If you are using a newer version, read the :doc:`newer<advanced>`
+ chapter instead.
+
+Twig can be extended in many ways; you can add extra tags, filters, tests,
+operators, global variables, and functions. You can even extend the parser
+itself with node visitors.
+
+.. note::
+
+ The first section of this chapter describes how to extend Twig easily. If
+ you want to reuse your changes in different projects or if you want to
+ share them with others, you should then create an extension as described
+ in the following section.
+
+.. caution::
+
+ When extending Twig by calling methods on the Twig environment instance,
+ Twig won't be able to recompile your templates when the PHP code is
+ updated. To see your changes in real-time, either disable template caching
+ or package your code into an extension (see the next section of this
+ chapter).
+
+Before extending Twig, you must understand the differences between all the
+different possible extension points and when to use them.
+
+First, remember that Twig has two main language constructs:
+
+* ``{{ }}``: used to print the result of an expression evaluation;
+
+* ``{% %}``: used to execute statements.
+
+To understand why Twig exposes so many extension points, let's see how to
+implement a *Lorem ipsum* generator (it needs to know the number of words to
+generate).
+
+You can use a ``lipsum`` *tag*:
+
+.. code-block:: jinja
+
+ {% lipsum 40 %}
+
+That works, but using a tag for ``lipsum`` is not a good idea for at least
+three main reasons:
+
+* ``lipsum`` is not a language construct;
+* The tag outputs something;
+* The tag is not flexible as you cannot use it in an expression:
+
+ .. code-block:: jinja
+
+ {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
+
+In fact, you rarely need to create tags; and that's good news because tags are
+the most complex extension point of Twig.
+
+Now, let's use a ``lipsum`` *filter*:
+
+.. code-block:: jinja
+
+ {{ 40|lipsum }}
+
+Again, it works, but it looks weird. A filter transforms the passed value to
+something else but here we use the value to indicate the number of words to
+generate (so, ``40`` is an argument of the filter, not the value we want to
+transform).
+
+Next, let's use a ``lipsum`` *function*:
+
+.. code-block:: jinja
+
+ {{ lipsum(40) }}
+
+Here we go. For this specific example, the creation of a function is the
+extension point to use. And you can use it anywhere an expression is accepted:
+
+.. code-block:: jinja
+
+ {{ 'some text' ~ ipsum(40) ~ 'some more text' }}
+
+ {% set ipsum = ipsum(40) %}
+
+Last but not the least, you can also use a *global* object with a method able
+to generate lorem ipsum text:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+As a rule of thumb, use functions for frequently used features and global
+objects for everything else.
+
+Keep in mind the following when you want to extend Twig:
+
+========== ========================== ========== =========================
+What? Implementation difficulty? How often? When?
+========== ========================== ========== =========================
+*macro* trivial frequent Content generation
+*global* trivial frequent Helper object
+*function* trivial frequent Content generation
+*filter* trivial frequent Value transformation
+*tag* complex rare DSL language construct
+*test* trivial rare Boolean decision
+*operator* trivial rare Values transformation
+========== ========================== ========== =========================
+
+Globals
+-------
+
+A global variable is like any other template variable, except that it's
+available in all templates and macros::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addGlobal('text', new Text());
+
+You can then use the ``text`` variable anywhere in a template:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+Filters
+-------
+
+A filter is a regular PHP function or an object method that takes the left
+side of the filter (before the pipe ``|``) as first argument and the extra
+arguments passed to the filter (within parentheses ``()``) as extra arguments.
+
+Defining a filter is as easy as associating the filter name with a PHP
+callable. For instance, let's say you have the following code in a template:
+
+.. code-block:: jinja
+
+ {{ 'TWIG'|lower }}
+
+When compiling this template to PHP, Twig looks for the PHP callable
+associated with the ``lower`` filter. The ``lower`` filter is a built-in Twig
+filter, and it is simply mapped to the PHP ``strtolower()`` function. After
+compilation, the generated PHP code is roughly equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo strtolower('TWIG') ?>
+
+As you can see, the ``'TWIG'`` string is passed as a first argument to the PHP
+function.
+
+A filter can also take extra arguments like in the following example:
+
+.. code-block:: jinja
+
+ {{ now|date('d/m/Y') }}
+
+In this case, the extra arguments are passed to the function after the main
+argument, and the compiled code is equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
+
+Let's see how to create a new filter.
+
+In this section, we will create a ``rot13`` filter, which should return the
+`rot13`_ transformation of a string. Here is an example of its usage and the
+expected output:
+
+.. code-block:: jinja
+
+ {{ "Twig"|rot13 }}
+
+ {# should displays Gjvt #}
+
+Adding a filter is as simple as calling the ``addFilter()`` method on the
+``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFilter('rot13', new Twig_Filter_Function('str_rot13'));
+
+The second argument of ``addFilter()`` is an instance of ``Twig_Filter``.
+Here, we use ``Twig_Filter_Function`` as the filter is a PHP function. The
+first argument passed to the ``Twig_Filter_Function`` constructor is the name
+of the PHP function to call, here ``str_rot13``, a native PHP function.
+
+Let's say I now want to be able to add a prefix before the converted string:
+
+.. code-block:: jinja
+
+ {{ "Twig"|rot13('prefix_') }}
+
+ {# should displays prefix_Gjvt #}
+
+As the PHP ``str_rot13()`` function does not support this requirement, let's
+create a new PHP function::
+
+ function project_compute_rot13($string, $prefix = '')
+ {
+ return $prefix.str_rot13($string);
+ }
+
+As you can see, the ``prefix`` argument of the filter is passed as an extra
+argument to the ``project_compute_rot13()`` function.
+
+Adding this filter is as easy as before::
+
+ $twig->addFilter('rot13', new Twig_Filter_Function('project_compute_rot13'));
+
+For better encapsulation, a filter can also be defined as a static method of a
+class. The ``Twig_Filter_Function`` class can also be used to register such
+static methods as filters::
+
+ $twig->addFilter('rot13', new Twig_Filter_Function('SomeClass::rot13Filter'));
+
+.. tip::
+
+ In an extension, you can also define a filter as a static method of the
+ extension class.
+
+Environment aware Filters
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``Twig_Filter`` classes take options as their last argument. For instance,
+if you want access to the current environment instance in your filter, set the
+``needs_environment`` option to ``true``::
+
+ $filter = new Twig_Filter_Function('str_rot13', array('needs_environment' => true));
+
+Twig will then pass the current environment as the first argument to the
+filter call::
+
+ function twig_compute_rot13(Twig_Environment $env, $string)
+ {
+ // get the current charset for instance
+ $charset = $env->getCharset();
+
+ return str_rot13($string);
+ }
+
+Automatic Escaping
+~~~~~~~~~~~~~~~~~~
+
+If automatic escaping is enabled, the output of the filter may be escaped
+before printing. If your filter acts as an escaper (or explicitly outputs html
+or javascript code), you will want the raw output to be printed. In such a
+case, set the ``is_safe`` option::
+
+ $filter = new Twig_Filter_Function('nl2br', array('is_safe' => array('html')));
+
+Some filters may need to work on input that is already escaped or safe, for
+example when adding (safe) html tags to originally unsafe output. In such a
+case, set the ``pre_escape`` option to escape the input data before it is run
+through your filter::
+
+ $filter = new Twig_Filter_Function('somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
+
+Dynamic Filters
+~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.5
+ Dynamic filters support was added in Twig 1.5.
+
+A filter name containing the special ``*`` character is a dynamic filter as
+the ``*`` can be any string::
+
+ $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $arguments)
+ {
+ // ...
+ }
+
+The following filters will be matched by the above defined dynamic filter:
+
+* ``product_path``
+* ``category_path``
+
+A dynamic filter can define more than one dynamic parts::
+
+ $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $suffix, $arguments)
+ {
+ // ...
+ }
+
+The filter will receive all dynamic part values before the normal filters
+arguments. For instance, a call to ``'foo'|a_path_b()`` will result in the
+following PHP call: ``twig_path('a', 'b', 'foo')``.
+
+Functions
+---------
+
+A function is a regular PHP function or an object method that can be called from
+templates.
+
+.. code-block:: jinja
+
+ {{ constant("DATE_W3C") }}
+
+When compiling this template to PHP, Twig looks for the PHP callable
+associated with the ``constant`` function. The ``constant`` function is a built-in Twig
+function, and it is simply mapped to the PHP ``constant()`` function. After
+compilation, the generated PHP code is roughly equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo constant('DATE_W3C') ?>
+
+Adding a function is similar to adding a filter. This can be done by calling the
+``addFunction()`` method on the ``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFunction('functionName', new Twig_Function_Function('someFunction'));
+
+You can also expose extension methods as functions in your templates::
+
+ // $this is an object that implements Twig_ExtensionInterface.
+ $twig = new Twig_Environment($loader);
+ $twig->addFunction('otherFunction', new Twig_Function_Method($this, 'someMethod'));
+
+Functions also support ``needs_environment`` and ``is_safe`` parameters.
+
+Dynamic Functions
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.5
+ Dynamic functions support was added in Twig 1.5.
+
+A function name containing the special ``*`` character is a dynamic function
+as the ``*`` can be any string::
+
+ $twig->addFunction('*_path', new Twig_Function_Function('twig_path'));
+
+ function twig_path($name, $arguments)
+ {
+ // ...
+ }
+
+The following functions will be matched by the above defined dynamic function:
+
+* ``product_path``
+* ``category_path``
+
+A dynamic function can define more than one dynamic parts::
+
+ $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $suffix, $arguments)
+ {
+ // ...
+ }
+
+The function will receive all dynamic part values before the normal functions
+arguments. For instance, a call to ``a_path_b('foo')`` will result in the
+following PHP call: ``twig_path('a', 'b', 'foo')``.
+
+Tags
+----
+
+One of the most exciting feature of a template engine like Twig is the
+possibility to define new language constructs. This is also the most complex
+feature as you need to understand how Twig's internals work.
+
+Let's create a simple ``set`` tag that allows the definition of simple
+variables from within a template. The tag can be used like follows:
+
+.. code-block:: jinja
+
+ {% set name = "value" %}
+
+ {{ name }}
+
+ {# should output value #}
+
+.. note::
+
+ The ``set`` tag is part of the Core extension and as such is always
+ available. The built-in version is slightly more powerful and supports
+ multiple assignments by default (cf. the template designers chapter for
+ more information).
+
+Three steps are needed to define a new tag:
+
+* Defining a Token Parser class (responsible for parsing the template code);
+
+* Defining a Node class (responsible for converting the parsed code to PHP);
+
+* Registering the tag.
+
+Registering a new tag
+~~~~~~~~~~~~~~~~~~~~~
+
+Adding a tag is as simple as calling the ``addTokenParser`` method on the
+``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addTokenParser(new Project_Set_TokenParser());
+
+Defining a Token Parser
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Now, let's see the actual code of this class::
+
+ class Project_Set_TokenParser extends Twig_TokenParser
+ {
+ public function parse(Twig_Token $token)
+ {
+ $lineno = $token->getLine();
+ $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+ $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, '=');
+ $value = $this->parser->getExpressionParser()->parseExpression();
+
+ $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Project_Set_Node($name, $value, $lineno, $this->getTag());
+ }
+
+ public function getTag()
+ {
+ return 'set';
+ }
+ }
+
+The ``getTag()`` method must return the tag we want to parse, here ``set``.
+
+The ``parse()`` method is invoked whenever the parser encounters a ``set``
+tag. It should return a ``Twig_Node`` instance that represents the node (the
+``Project_Set_Node`` calls creating is explained in the next section).
+
+The parsing process is simplified thanks to a bunch of methods you can call
+from the token stream (``$this->parser->getStream()``):
+
+* ``getCurrent()``: Gets the current token in the stream.
+
+* ``next()``: Moves to the next token in the stream, *but returns the old one*.
+
+* ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether
+ the current token is of a particular type or value (or both). The value may be an
+ array of several possible values.
+
+* ``expect($type[, $value[, $message]])``: If the current token isn't of the given
+ type/value a syntax error is thrown. Otherwise, if the type and value are correct,
+ the token is returned and the stream moves to the next token.
+
+* ``look()``: Looks a the next token without consuming it.
+
+Parsing expressions is done by calling the ``parseExpression()`` like we did for
+the ``set`` tag.
+
+.. tip::
+
+ Reading the existing ``TokenParser`` classes is the best way to learn all
+ the nitty-gritty details of the parsing process.
+
+Defining a Node
+~~~~~~~~~~~~~~~
+
+The ``Project_Set_Node`` class itself is rather simple::
+
+ class Project_Set_Node extends Twig_Node
+ {
+ public function __construct($name, Twig_Node_Expression $value, $lineno, $tag = null)
+ {
+ parent::__construct(array('value' => $value), array('name' => $name), $lineno, $tag);
+ }
+
+ public function compile(Twig_Compiler $compiler)
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$context[\''.$this->getAttribute('name').'\'] = ')
+ ->subcompile($this->getNode('value'))
+ ->raw(";\n")
+ ;
+ }
+ }
+
+The compiler implements a fluid interface and provides methods that helps the
+developer generate beautiful and readable PHP code:
+
+* ``subcompile()``: Compiles a node.
+
+* ``raw()``: Writes the given string as is.
+
+* ``write()``: Writes the given string by adding indentation at the beginning
+ of each line.
+
+* ``string()``: Writes a quoted string.
+
+* ``repr()``: Writes a PHP representation of a given value (see
+ ``Twig_Node_For`` for a usage example).
+
+* ``addDebugInfo()``: Adds the line of the original template file related to
+ the current node as a comment.
+
+* ``indent()``: Indents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+* ``outdent()``: Outdents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+.. _creating_extensions:
+
+Creating an Extension
+---------------------
+
+The main motivation for writing an extension is to move often used code into a
+reusable class like adding support for internationalization. An extension can
+define tags, filters, tests, operators, global variables, functions, and node
+visitors.
+
+Creating an extension also makes for a better separation of code that is
+executed at compilation time and code needed at runtime. As such, it makes
+your code faster.
+
+Most of the time, it is useful to create a single extension for your project,
+to host all the specific tags and filters you want to add to Twig.
+
+.. tip::
+
+ When packaging your code into an extension, Twig is smart enough to
+ recompile your templates whenever you make a change to it (when the
+ ``auto_reload`` is enabled).
+
+.. note::
+
+ Before writing your own extensions, have a look at the Twig official
+ extension repository: http://github.com/fabpot/Twig-extensions.
+
+An extension is a class that implements the following interface::
+
+ interface Twig_ExtensionInterface
+ {
+ /**
+ * Initializes the runtime environment.
+ *
+ * This is where you can load some file that contains filter functions for instance.
+ *
+ * @param Twig_Environment $environment The current Twig_Environment instance
+ */
+ function initRuntime(Twig_Environment $environment);
+
+ /**
+ * Returns the token parser instances to add to the existing list.
+ *
+ * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
+ */
+ function getTokenParsers();
+
+ /**
+ * Returns the node visitor instances to add to the existing list.
+ *
+ * @return array An array of Twig_NodeVisitorInterface instances
+ */
+ function getNodeVisitors();
+
+ /**
+ * Returns a list of filters to add to the existing list.
+ *
+ * @return array An array of filters
+ */
+ function getFilters();
+
+ /**
+ * Returns a list of tests to add to the existing list.
+ *
+ * @return array An array of tests
+ */
+ function getTests();
+
+ /**
+ * Returns a list of functions to add to the existing list.
+ *
+ * @return array An array of functions
+ */
+ function getFunctions();
+
+ /**
+ * Returns a list of operators to add to the existing list.
+ *
+ * @return array An array of operators
+ */
+ function getOperators();
+
+ /**
+ * Returns a list of global variables to add to the existing list.
+ *
+ * @return array An array of global variables
+ */
+ function getGlobals();
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ function getName();
+ }
+
+To keep your extension class clean and lean, it can inherit from the built-in
+``Twig_Extension`` class instead of implementing the whole interface. That
+way, you just need to implement the ``getName()`` method as the
+``Twig_Extension`` provides empty implementations for all other methods.
+
+The ``getName()`` method must return a unique identifier for your extension.
+
+Now, with this information in mind, let's create the most basic extension
+possible::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+.. note::
+
+ Of course, this extension does nothing for now. We will customize it in
+ the next sections.
+
+Twig does not care where you save your extension on the filesystem, as all
+extensions must be registered explicitly to be available in your templates.
+
+You can register an extension by using the ``addExtension()`` method on your
+main ``Environment`` object::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new Project_Twig_Extension());
+
+Of course, you need to first load the extension file by either using
+``require_once()`` or by using an autoloader (see `spl_autoload_register()`_).
+
+.. tip::
+
+ The bundled extensions are great examples of how extensions work.
+
+Globals
+~~~~~~~
+
+Global variables can be registered in an extension via the ``getGlobals()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getGlobals()
+ {
+ return array(
+ 'text' => new Text(),
+ );
+ }
+
+ // ...
+ }
+
+Functions
+~~~~~~~~~
+
+Functions can be registered in an extension via the ``getFunctions()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFunctions()
+ {
+ return array(
+ 'lipsum' => new Twig_Function_Function('generate_lipsum'),
+ );
+ }
+
+ // ...
+ }
+
+Filters
+~~~~~~~
+
+To add a filter to an extension, you need to override the ``getFilters()``
+method. This method must return an array of filters to add to the Twig
+environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'rot13' => new Twig_Filter_Function('str_rot13'),
+ );
+ }
+
+ // ...
+ }
+
+As you can see in the above code, the ``getFilters()`` method returns an array
+where keys are the name of the filters (``rot13``) and the values the
+definition of the filter (``new Twig_Filter_Function('str_rot13')``).
+
+As seen in the previous chapter, you can also define filters as static methods
+on the extension class::
+
+$twig->addFilter('rot13', new Twig_Filter_Function('Project_Twig_Extension::rot13Filter'));
+
+You can also use ``Twig_Filter_Method`` instead of ``Twig_Filter_Function``
+when defining a filter to use a method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'rot13' => new Twig_Filter_Method($this, 'rot13Filter'),
+ );
+ }
+
+ public function rot13Filter($string)
+ {
+ return str_rot13($string);
+ }
+
+ // ...
+ }
+
+The first argument of the ``Twig_Filter_Method`` constructor is always
+``$this``, the current extension object. The second one is the name of the
+method to call.
+
+Using methods for filters is a great way to package your filter without
+polluting the global namespace. This also gives the developer more flexibility
+at the cost of a small overhead.
+
+Overriding default Filters
+..........................
+
+If some default core filters do not suit your needs, you can easily override
+them by creating your own extension. Just use the same names as the one you
+want to override::
+
+ class MyCoreExtension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'date' => new Twig_Filter_Method($this, 'dateFilter'),
+ // ...
+ );
+ }
+
+ public function dateFilter($timestamp, $format = 'F j, Y H:i')
+ {
+ return '...'.twig_date_format_filter($timestamp, $format);
+ }
+
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+Here, we override the ``date`` filter with a custom one. Using this extension
+is as simple as registering the ``MyCoreExtension`` extension by calling the
+``addExtension()`` method on the environment instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new MyCoreExtension());
+
+Tags
+~~~~
+
+Adding a tag in an extension can be done by overriding the
+``getTokenParsers()`` method. This method must return an array of tags to add
+to the Twig environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTokenParsers()
+ {
+ return array(new Project_Set_TokenParser());
+ }
+
+ // ...
+ }
+
+In the above code, we have added a single new tag, defined by the
+``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is
+responsible for parsing the tag and compiling it to PHP.
+
+Operators
+~~~~~~~~~
+
+The ``getOperators()`` methods allows to add new operators. Here is how to add
+``!``, ``||``, and ``&&`` operators::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getOperators()
+ {
+ return array(
+ array(
+ '!' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
+ ),
+ array(
+ '||' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ '&&' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ ),
+ );
+ }
+
+ // ...
+ }
+
+Tests
+~~~~~
+
+The ``getTests()`` methods allows to add new test functions::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTests()
+ {
+ return array(
+ 'even' => new Twig_Test_Function('twig_test_even'),
+ );
+ }
+
+ // ...
+ }
+
+Testing an Extension
+--------------------
+
+.. versionadded:: 1.10
+ Support for functional tests was added in Twig 1.10.
+
+Functional Tests
+~~~~~~~~~~~~~~~~
+
+You can create functional tests for extensions simply by creating the
+following file structure in your test directory::
+
+ Fixtures/
+ filters/
+ foo.test
+ bar.test
+ functions/
+ foo.test
+ bar.test
+ tags/
+ foo.test
+ bar.test
+ IntegrationTest.php
+
+The ``IntegrationTest.php`` file should look like this::
+
+ class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
+ {
+ public function getExtensions()
+ {
+ return array(
+ new Project_Twig_Extension1(),
+ new Project_Twig_Extension2(),
+ );
+ }
+
+ public function getFixturesDir()
+ {
+ return dirname(__FILE__).'/Fixtures/';
+ }
+ }
+
+Fixtures examples can be found within the Twig repository
+`tests/Twig/Fixtures`_ directory.
+
+Node Tests
+~~~~~~~~~~
+
+Testing the node visitors can be complex, so extend your test cases from
+``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository
+`tests/Twig/Node`_ directory.
+
+.. _`spl_autoload_register()`: http://www.php.net/spl_autoload_register
+.. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php
+.. _`tests/Twig/Fixtures`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Fixtures
+.. _`tests/Twig/Node`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Node
--- /dev/null
+Twig for Developers
+===================
+
+This chapter describes the API to Twig and not the template language. It will
+be most useful as reference to those implementing the template interface to
+the application and not those who are creating Twig templates.
+
+Basics
+------
+
+Twig uses a central object called the **environment** (of class
+``Twig_Environment``). Instances of this class are used to store the
+configuration and extensions, and are used to load templates from the file
+system or other locations.
+
+Most applications will create one ``Twig_Environment`` object on application
+initialization and use that to load templates. In some cases it's however
+useful to have multiple environments side by side, if different configurations
+are in use.
+
+The simplest way to configure Twig to load templates for your application
+looks roughly like this::
+
+ require_once '/path/to/lib/Twig/Autoloader.php';
+ Twig_Autoloader::register();
+
+ $loader = new Twig_Loader_Filesystem('/path/to/templates');
+ $twig = new Twig_Environment($loader, array(
+ 'cache' => '/path/to/compilation_cache',
+ ));
+
+This will create a template environment with the default settings and a loader
+that looks up the templates in the ``/path/to/templates/`` folder. Different
+loaders are available and you can also write your own if you want to load
+templates from a database or other resources.
+
+.. note::
+
+ Notice that the second argument of the environment is an array of options.
+ The ``cache`` option is a compilation cache directory, where Twig caches
+ the compiled templates to avoid the parsing phase for sub-sequent
+ requests. It is very different from the cache you might want to add for
+ the evaluated templates. For such a need, you can use any available PHP
+ cache library.
+
+To load a template from this environment you just have to call the
+``loadTemplate()`` method which then returns a ``Twig_Template`` instance::
+
+ $template = $twig->loadTemplate('index.html');
+
+To render the template with some variables, call the ``render()`` method::
+
+ echo $template->render(array('the' => 'variables', 'go' => 'here'));
+
+.. note::
+
+ The ``display()`` method is a shortcut to output the template directly.
+
+You can also load and render the template in one fell swoop::
+
+ echo $twig->render('index.html', array('the' => 'variables', 'go' => 'here'));
+
+.. _environment_options:
+
+Environment Options
+-------------------
+
+When creating a new ``Twig_Environment`` instance, you can pass an array of
+options as the constructor second argument::
+
+ $twig = new Twig_Environment($loader, array('debug' => true));
+
+The following options are available:
+
+* ``debug``: When set to ``true``, the generated templates have a
+ ``__toString()`` method that you can use to display the generated nodes
+ (default to ``false``).
+
+* ``charset``: The charset used by the templates (default to ``utf-8``).
+
+* ``base_template_class``: The base template class to use for generated
+ templates (default to ``Twig_Template``).
+
+* ``cache``: An absolute path where to store the compiled templates, or
+ ``false`` to disable caching (which is the default).
+
+* ``auto_reload``: When developing with Twig, it's useful to recompile the
+ template whenever the source code changes. If you don't provide a value for
+ the ``auto_reload`` option, it will be determined automatically based on the
+ ``debug`` value.
+
+* ``strict_variables``: If set to ``false``, Twig will silently ignore invalid
+ variables (variables and or attributes/methods that do not exist) and
+ replace them with a ``null`` value. When set to ``true``, Twig throws an
+ exception instead (default to ``false``).
+
+* ``autoescape``: If set to ``true``, auto-escaping will be enabled by default
+ for all templates (default to ``true``). As of Twig 1.8, you can set the
+ escaping strategy to use (``html``, ``js``, ``false`` to disable).
+ As of Twig 1.9, you can set the escaping strategy to use (``css``, ``url``,
+ ``html_attr``, or a PHP callback that takes the template "filename" and must
+ return the escaping strategy to use -- the callback cannot be a function name
+ to avoid collision with built-in escaping strategies).
+
+* ``optimizations``: A flag that indicates which optimizations to apply
+ (default to ``-1`` -- all optimizations are enabled; set it to ``0`` to
+ disable).
+
+Loaders
+-------
+
+Loaders are responsible for loading templates from a resource such as the file
+system.
+
+Compilation Cache
+~~~~~~~~~~~~~~~~~
+
+All template loaders can cache the compiled templates on the filesystem for
+future reuse. It speeds up Twig a lot as templates are only compiled once; and
+the performance boost is even larger if you use a PHP accelerator such as APC.
+See the ``cache`` and ``auto_reload`` options of ``Twig_Environment`` above
+for more information.
+
+Built-in Loaders
+~~~~~~~~~~~~~~~~
+
+Here is a list of the built-in loaders Twig provides:
+
+``Twig_Loader_Filesystem``
+..........................
+
+.. versionadded:: 1.10
+ The ``prependPath()`` and support for namespaces were added in Twig 1.10.
+
+``Twig_Loader_Filesystem`` loads templates from the file system. This loader
+can find templates in folders on the file system and is the preferred way to
+load them::
+
+ $loader = new Twig_Loader_Filesystem($templateDir);
+
+It can also look for templates in an array of directories::
+
+ $loader = new Twig_Loader_Filesystem(array($templateDir1, $templateDir2));
+
+With such a configuration, Twig will first look for templates in
+``$templateDir1`` and if they do not exist, it will fallback to look for them
+in the ``$templateDir2``.
+
+You can add or prepend paths via the ``addPath()`` and ``prependPath()``
+methods::
+
+ $loader->addPath($templateDir3);
+ $loader->prependPath($templateDir4);
+
+The filesystem loader also supports namespaced templates. This allows to group
+your templates under different namespaces which have their own template paths.
+
+When using the ``setPaths()``, ``addPath()``, and ``prependPath()`` methods,
+specify the namespace as the second argument (when not specified, these
+methods act on the "main" namespace)::
+
+ $loader->addPath($templateDir, 'admin');
+
+Namespaced templates can be accessed via the special
+``@namespace_name/template_path`` notation::
+
+ $twig->render('@admin/index.html', array());
+
+``Twig_Loader_String``
+......................
+
+``Twig_Loader_String`` loads templates from strings. It's a dummy loader as
+the template reference is the template source code::
+
+ $loader = new Twig_Loader_String();
+ $twig = new Twig_Environment($loader);
+
+ echo $twig->render('Hello {{ name }}!', array('name' => 'Fabien'));
+
+This loader should only be used for unit testing as it has severe limitations:
+several tags, like ``extends`` or ``include`` do not make sense to use as the
+reference to the template is the template source code itself.
+
+``Twig_Loader_Array``
+.....................
+
+``Twig_Loader_Array`` loads a template from a PHP array. It's passed an array
+of strings bound to template names::
+
+ $loader = new Twig_Loader_Array(array(
+ 'index.html' => 'Hello {{ name }}!',
+ ));
+ $twig = new Twig_Environment($loader);
+
+ echo $twig->render('index.html', array('name' => 'Fabien'));
+
+This loader is very useful for unit testing. It can also be used for small
+projects where storing all templates in a single PHP file might make sense.
+
+.. tip::
+
+ When using the ``Array`` or ``String`` loaders with a cache mechanism, you
+ should know that a new cache key is generated each time a template content
+ "changes" (the cache key being the source code of the template). If you
+ don't want to see your cache grows out of control, you need to take care
+ of clearing the old cache file by yourself.
+
+``Twig_Loader_Chain``
+.....................
+
+``Twig_Loader_Chain`` delegates the loading of templates to other loaders::
+
+ $loader1 = new Twig_Loader_Array(array(
+ 'base.html' => '{% block content %}{% endblock %}',
+ ));
+ $loader2 = new Twig_Loader_Array(array(
+ 'index.html' => '{% extends "base.twig" %}{% block content %}Hello {{ name }}{% endblock %}',
+ 'base.html' => 'Will never be loaded',
+ ));
+
+ $loader = new Twig_Loader_Chain(array($loader1, $loader2));
+
+ $twig = new Twig_Environment($loader);
+
+When looking for a template, Twig will try each loader in turn and it will
+return as soon as the template is found. When rendering the ``index.html``
+template from the above example, Twig will load it with ``$loader2`` but the
+``base.html`` template will be loaded from ``$loader1``.
+
+``Twig_Loader_Chain`` accepts any loader that implements
+``Twig_LoaderInterface``.
+
+.. note::
+
+ You can also add loaders via the ``addLoader()`` method.
+
+Create your own Loader
+~~~~~~~~~~~~~~~~~~~~~~
+
+All loaders implement the ``Twig_LoaderInterface``::
+
+ interface Twig_LoaderInterface
+ {
+ /**
+ * Gets the source code of a template, given its name.
+ *
+ * @param string $name string The name of the template to load
+ *
+ * @return string The template source code
+ */
+ function getSource($name);
+
+ /**
+ * Gets the cache key to use for the cache for a given template name.
+ *
+ * @param string $name string The name of the template to load
+ *
+ * @return string The cache key
+ */
+ function getCacheKey($name);
+
+ /**
+ * Returns true if the template is still fresh.
+ *
+ * @param string $name The template name
+ * @param timestamp $time The last modification time of the cached template
+ */
+ function isFresh($name, $time);
+ }
+
+As an example, here is how the built-in ``Twig_Loader_String`` reads::
+
+ class Twig_Loader_String implements Twig_LoaderInterface
+ {
+ public function getSource($name)
+ {
+ return $name;
+ }
+
+ public function getCacheKey($name)
+ {
+ return $name;
+ }
+
+ public function isFresh($name, $time)
+ {
+ return false;
+ }
+ }
+
+The ``isFresh()`` method must return ``true`` if the current cached template
+is still fresh, given the last modification time, or ``false`` otherwise.
+
+.. tip::
+
+ As of Twig 1.11.0, you can also implement ``Twig_ExistsLoaderInterface``
+ to make your loader faster when used with the chain loader.
+
+Using Extensions
+----------------
+
+Twig extensions are packages that add new features to Twig. Using an
+extension is as simple as using the ``addExtension()`` method::
+
+ $twig->addExtension(new Twig_Extension_Sandbox());
+
+Twig comes bundled with the following extensions:
+
+* *Twig_Extension_Core*: Defines all the core features of Twig.
+
+* *Twig_Extension_Escaper*: Adds automatic output-escaping and the possibility
+ to escape/unescape blocks of code.
+
+* *Twig_Extension_Sandbox*: Adds a sandbox mode to the default Twig
+ environment, making it safe to evaluate untrusted code.
+
+* *Twig_Extension_Optimizer*: Optimizes the node tree before compilation.
+
+The core, escaper, and optimizer extensions do not need to be added to the
+Twig environment, as they are registered by default.
+
+Built-in Extensions
+-------------------
+
+This section describes the features added by the built-in extensions.
+
+.. tip::
+
+ Read the chapter about extending Twig to learn how to create your own
+ extensions.
+
+Core Extension
+~~~~~~~~~~~~~~
+
+The ``core`` extension defines all the core features of Twig:
+
+* :doc:`Tags <tags/index>`;
+* :doc:`Filters <filters/index>`;
+* :doc:`Functions <functions/index>`;
+* :doc:`Tests <tests/index>`.
+
+Escaper Extension
+~~~~~~~~~~~~~~~~~
+
+The ``escaper`` extension adds automatic output escaping to Twig. It defines a
+tag, ``autoescape``, and a filter, ``raw``.
+
+When creating the escaper extension, you can switch on or off the global
+output escaping strategy::
+
+ $escaper = new Twig_Extension_Escaper('html');
+ $twig->addExtension($escaper);
+
+If set to ``html``, all variables in templates are escaped (using the ``html``
+escaping strategy), except those using the ``raw`` filter:
+
+.. code-block:: jinja
+
+ {{ article.to_html|raw }}
+
+You can also change the escaping mode locally by using the ``autoescape`` tag
+(see the :doc:`autoescape<tags/autoescape>` doc for the syntax used before
+Twig 1.8):
+
+.. code-block:: jinja
+
+ {% autoescape 'html' %}
+ {{ var }}
+ {{ var|raw }} {# var won't be escaped #}
+ {{ var|escape }} {# var won't be double-escaped #}
+ {% endautoescape %}
+
+.. warning::
+
+ The ``autoescape`` tag has no effect on included files.
+
+The escaping rules are implemented as follows:
+
+* Literals (integers, booleans, arrays, ...) used in the template directly as
+ variables or filter arguments are never automatically escaped:
+
+ .. code-block:: jinja
+
+ {{ "Twig<br />" }} {# won't be escaped #}
+
+ {% set text = "Twig<br />" %}
+ {{ text }} {# will be escaped #}
+
+* Expressions which the result is always a literal or a variable marked safe
+ are never automatically escaped:
+
+ .. code-block:: jinja
+
+ {{ foo ? "Twig<br />" : "<br />Twig" }} {# won't be escaped #}
+
+ {% set text = "Twig<br />" %}
+ {{ foo ? text : "<br />Twig" }} {# will be escaped #}
+
+ {% set text = "Twig<br />" %}
+ {{ foo ? text|raw : "<br />Twig" }} {# won't be escaped #}
+
+ {% set text = "Twig<br />" %}
+ {{ foo ? text|escape : "<br />Twig" }} {# the result of the expression won't be escaped #}
+
+* Escaping is applied before printing, after any other filter is applied:
+
+ .. code-block:: jinja
+
+ {{ var|upper }} {# is equivalent to {{ var|upper|escape }} #}
+
+* The `raw` filter should only be used at the end of the filter chain:
+
+ .. code-block:: jinja
+
+ {{ var|raw|upper }} {# will be escaped #}
+
+ {{ var|upper|raw }} {# won't be escaped #}
+
+* Automatic escaping is not applied if the last filter in the chain is marked
+ safe for the current context (e.g. ``html`` or ``js``). ``escaper`` and
+ ``escaper('html')`` are marked safe for html, ``escaper('js')`` is marked
+ safe for javascript, ``raw`` is marked safe for everything.
+
+ .. code-block:: jinja
+
+ {% autoescape 'js' %}
+ {{ var|escape('html') }} {# will be escaped for html and javascript #}
+ {{ var }} {# will be escaped for javascript #}
+ {{ var|escape('js') }} {# won't be double-escaped #}
+ {% endautoescape %}
+
+.. note::
+
+ Note that autoescaping has some limitations as escaping is applied on
+ expressions after evaluation. For instance, when working with
+ concatenation, ``{{ foo|raw ~ bar }}`` won't give the expected result as
+ escaping is applied on the result of the concatenation, not on the
+ individual variables (so, the ``raw`` filter won't have any effect here).
+
+Sandbox Extension
+~~~~~~~~~~~~~~~~~
+
+The ``sandbox`` extension can be used to evaluate untrusted code. Access to
+unsafe attributes and methods is prohibited. The sandbox security is managed
+by a policy instance. By default, Twig comes with one policy class:
+``Twig_Sandbox_SecurityPolicy``. This class allows you to white-list some
+tags, filters, properties, and methods::
+
+ $tags = array('if');
+ $filters = array('upper');
+ $methods = array(
+ 'Article' => array('getTitle', 'getBody'),
+ );
+ $properties = array(
+ 'Article' => array('title', 'body'),
+ );
+ $functions = array('range');
+ $policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods, $properties, $functions);
+
+With the previous configuration, the security policy will only allow usage of
+the ``if`` tag, and the ``upper`` filter. Moreover, the templates will only be
+able to call the ``getTitle()`` and ``getBody()`` methods on ``Article``
+objects, and the ``title`` and ``body`` public properties. Everything else
+won't be allowed and will generate a ``Twig_Sandbox_SecurityError`` exception.
+
+The policy object is the first argument of the sandbox constructor::
+
+ $sandbox = new Twig_Extension_Sandbox($policy);
+ $twig->addExtension($sandbox);
+
+By default, the sandbox mode is disabled and should be enabled when including
+untrusted template code by using the ``sandbox`` tag:
+
+.. code-block:: jinja
+
+ {% sandbox %}
+ {% include 'user.html' %}
+ {% endsandbox %}
+
+You can sandbox all templates by passing ``true`` as the second argument of
+the extension constructor::
+
+ $sandbox = new Twig_Extension_Sandbox($policy, true);
+
+Optimizer Extension
+~~~~~~~~~~~~~~~~~~~
+
+The ``optimizer`` extension optimizes the node tree before compilation::
+
+ $twig->addExtension(new Twig_Extension_Optimizer());
+
+By default, all optimizations are turned on. You can select the ones you want
+to enable by passing them to the constructor::
+
+ $optimizer = new Twig_Extension_Optimizer(Twig_NodeVisitor_Optimizer::OPTIMIZE_FOR);
+
+ $twig->addExtension($optimizer);
+
+Twig supports the following optimizations:
+
+* ``Twig_NodeVisitor_Optimizer::OPTIMIZE_ALL``, enables all optimizations
+ (this is the default value).
+* ``Twig_NodeVisitor_Optimizer::OPTIMIZE_NONE``, disables all optimizations.
+ This reduces the compilation time, but it can increase the execution time
+ and the consumed memory.
+* ``Twig_NodeVisitor_Optimizer::OPTIMIZE_FOR``, optimizes the ``for`` tag by
+ removing the ``loop`` variable creation whenever possible.
+* ``Twig_NodeVisitor_Optimizer::OPTIMIZE_RAW_FILTER``, removes the ``raw``
+ filter whenever possible.
+* ``Twig_NodeVisitor_Optimizer::OPTIMIZE_VAR_ACCESS``, simplifies the creation
+ and access of variables in the compiled templates whenever possible.
+
+Exceptions
+----------
+
+Twig can throw exceptions:
+
+* ``Twig_Error``: The base exception for all errors.
+
+* ``Twig_Error_Syntax``: Thrown to tell the user that there is a problem with
+ the template syntax.
+
+* ``Twig_Error_Runtime``: Thrown when an error occurs at runtime (when a filter
+ does not exist for instance).
+
+* ``Twig_Error_Loader``: Thrown when an error occurs during template loading.
+
+* ``Twig_Sandbox_SecurityError``: Thrown when an unallowed tag, filter, or
+ method is called in a sandboxed template.
--- /dev/null
+Coding Standards
+================
+
+When writing Twig templates, we recommend you to follow these official coding
+standards:
+
+* Put one (and only one) space after the start of a delimiter (``{{``, ``{%``,
+ and ``{#``) and before the end of a delimiter (``}}``, ``%}``, and ``#}``):
+
+ .. code-block:: jinja
+
+ {{ foo }}
+ {# comment #}
+ {% if foo %}{% endif %}
+
+ When using the whitespace control character, do not put any spaces between
+ it and the delimiter:
+
+ .. code-block:: jinja
+
+ {{- foo -}}
+ {#- comment -#}
+ {%- if foo -%}{%- endif -%}
+
+* Put one (and only one) space before and after the following operators:
+ comparison operators (``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``), math
+ operators (``+``, ``-``, ``/``, ``*``, ``%``, ``//``, ``**``), logic
+ operators (``not``, ``and``, ``or``), ``~``, ``is``, ``in``, and the ternary
+ operator (``?:``):
+
+ .. code-block:: jinja
+
+ {{ 1 + 2 }}
+ {{ foo ~ bar }}
+ {{ true ? true : false }}
+
+* Put one (and only one) space after the ``:`` sign in hashes and ``,`` in
+ arrays and hashes:
+
+ .. code-block:: jinja
+
+ {{ [1, 2, 3] }}
+ {{ {'foo': 'bar'} }}
+
+* Do not put any spaces after an opening parenthesis and before a closing
+ parenthesis in expressions:
+
+ .. code-block:: jinja
+
+ {{ 1 + (2 * 3) }}
+
+* Do not put any spaces before and after string delimiters:
+
+ .. code-block:: jinja
+
+ {{ 'foo' }}
+ {{ "foo" }}
+
+* Do not put any spaces before and after the following operators: ``|``,
+ ``.``, ``..``, ``[]``:
+
+ .. code-block:: jinja
+
+ {{ foo|upper|lower }}
+ {{ user.name }}
+ {{ user[name] }}
+ {% for i in 1..12 %}{% endfor %}
+
+* Do not put any spaces before and after the parenthesis used for filter and
+ function calls:
+
+ .. code-block:: jinja
+
+ {{ foo|default('foo') }}
+ {{ range(1..10) }}
+
+* Do not put any spaces before and after the opening and the closing of arrays
+ and hashes:
+
+ .. code-block:: jinja
+
+ {{ [1, 2, 3] }}
+ {{ {'foo': 'bar'} }}
+
+* Use lower cased and underscored variable names:
+
+ .. code-block:: jinja
+
+ {% set foo = 'foo' %}
+ {% set foo_bar = 'foo' %}
+
+* Indent your code inside tags (use the same indentation as the one used for
+ the main language of the file):
+
+ .. code-block:: jinja
+
+ {% block foo %}
+ {% if true %}
+ true
+ {% endif %}
+ {% endblock %}
--- /dev/null
+Deprecated Features
+===================
+
+This document lists all deprecated features in Twig. Deprecated features are
+kept for backward compatibility and removed in the next major release (a
+feature that was deprecated in Twig 1.x is removed in Twig 2.0).
+
+Token Parsers
+-------------
+
+* As of Twig 1.x, the token parser broker sub-system is deprecated. The
+ following class and interface will be removed in 2.0:
+
+ * ``Twig_TokenParserBrokerInterface``
+ * ``Twig_TokenParserBroker``
+
+Extensions
+----------
+
+* As of Twig 1.x, the ability to remove an extension is deprecated and the
+ ``Twig_Environment::removeExtension()`` method will be removed in 2.0.
+
+PEAR
+----
+
+PEAR support will be discontinued in Twig 2.0, and no PEAR packages will be
+provided. Use Composer instead.
+
+Filters
+-------
+
+* As of Twig 1.x, use ``Twig_SimpleFilter`` to add a filter. The following
+ classes and interfaces will be removed in 2.0:
+
+ * ``Twig_FilterInterface``
+ * ``Twig_FilterCallableInterface``
+ * ``Twig_Filter``
+ * ``Twig_Filter_Function``
+ * ``Twig_Filter_Method``
+ * ``Twig_Filter_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleFilter`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Filter`` instead). In Twig 2.x,
+ ``Twig_SimpleFilter`` is just an alias for ``Twig_Filter``.
+
+Functions
+---------
+
+* As of Twig 1.x, use ``Twig_SimpleFunction`` to add a function. The following
+ classes and interfaces will be removed in 2.0:
+
+ * ``Twig_FunctionInterface``
+ * ``Twig_FunctionCallableInterface``
+ * ``Twig_Function``
+ * ``Twig_Function_Function``
+ * ``Twig_Function_Method``
+ * ``Twig_Function_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleFunction`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Function`` instead). In Twig 2.x,
+ ``Twig_SimpleFunction`` is just an alias for ``Twig_Function``.
+
+Tests
+-----
+
+* As of Twig 1.x, use ``Twig_SimpleTest`` to add a test. The following classes
+ and interfaces will be removed in 2.0:
+
+ * ``Twig_TestInterface``
+ * ``Twig_TestCallableInterface``
+ * ``Twig_Test``
+ * ``Twig_Test_Function``
+ * ``Twig_Test_Method``
+ * ``Twig_Test_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleTest`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Test`` instead). In Twig 2.x,
+ ``Twig_SimpleTest`` is just an alias for ``Twig_Test``.
+
+Interfaces
+----------
+
+* As of Twig 2.x, the following interfaces are deprecated and empty (they will
+ be removed in Twig 3.0):
+
+* ``Twig_CompilerInterface`` (use ``Twig_Compiler`` instead)
+* ``Twig_LexerInterface`` (use ``Twig_Lexer`` instead)
+* ``Twig_NodeInterface`` (use ``Twig_Node`` instead)
+* ``Twig_ParserInterface`` (use ``Twig_Parser`` instead)
+* ``Twig_ExistsLoaderInterface`` (merged with ``Twig_LoaderInterface``)
+* ``Twig_TemplateInterface`` (use ``Twig_Template`` instead)
+
+Globals
+-------
+
+* As of Twig 2.x, the ability to register a global variable after the runtime
+ or the extensions have been initialized is not possible anymore (but
+ changing the value of an already registered global is possible).
--- /dev/null
+``abs``
+=======
+
+The ``abs`` filter returns the absolute value.
+
+.. code-block:: jinja
+
+ {# number = -5 #}
+
+ {{ number|abs }}
+
+ {# outputs 5 #}
+
+.. note::
+
+ Internally, Twig uses the PHP `abs`_ function.
+
+.. _`abs`: http://php.net/abs
--- /dev/null
+``batch``
+=========
+
+.. versionadded:: 1.12.3
+ The batch filter was added in Twig 1.12.3.
+
+The ``batch`` filter "batches" items by returning a list of lists with the
+given number of items. If you provide a second parameter, it is used to fill
+missing items:
+
+.. code-block:: jinja
+
+ {% set items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] %}
+
+ <table>
+ {% for row in items|batch(3, 'No item') %}
+ <tr>
+ {% for column in row %}
+ <td>{{ column }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </table>
+
+The above example will be rendered as:
+
+.. code-block:: jinja
+
+ <table>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td>e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>No item</td>
+ <td>No item</td>
+ </tr>
+ </table>
--- /dev/null
+``capitalize``
+==============
+
+The ``capitalize`` filter capitalizes a value. The first character will be
+uppercase, all others lowercase:
+
+.. code-block:: jinja
+
+ {{ 'my first car'|capitalize }}
+
+ {# outputs 'My first car' #}
--- /dev/null
+``convert_encoding``
+====================
+
+.. versionadded:: 1.4
+ The ``convert_encoding`` filter was added in Twig 1.4.
+
+The ``convert_encoding`` filter converts a string from one encoding to
+another. The first argument is the expected output charset and the second one
+is the input charset:
+
+.. code-block:: jinja
+
+ {{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}
+
+.. note::
+
+ This filter relies on the `iconv`_ or `mbstring`_ extension, so one of
+ them must be installed. In case both are installed, `mbstring`_ is used by
+ default (Twig before 1.8.1 uses `iconv`_ by default).
+
+Arguments
+---------
+
+ * ``from``: The input charset
+ * ``to``: The output charset
+
+.. _`iconv`: http://php.net/iconv
+.. _`mbstring`: http://php.net/mbstring
--- /dev/null
+``date``
+========
+
+.. versionadded:: 1.1
+ The timezone support has been added in Twig 1.1.
+
+.. versionadded:: 1.5
+ The default date format support has been added in Twig 1.5.
+
+.. versionadded:: 1.6.1
+ The default timezone support has been added in Twig 1.6.1.
+
+.. versionadded:: 1.11.0
+ The introduction of the false value for the timezone was introduced in Twig 1.11.0
+
+The ``date`` filter formats a date to a given format:
+
+.. code-block:: jinja
+
+ {{ post.published_at|date("m/d/Y") }}
+
+The ``date`` filter accepts strings (it must be in a format supported by the
+`strtotime`_ function), `DateTime`_ instances, or `DateInterval`_ instances. For
+instance, to display the current date, filter the word "now":
+
+.. code-block:: jinja
+
+ {{ "now"|date("m/d/Y") }}
+
+To escape words and characters in the date format use ``\\`` in front of each
+character:
+
+.. code-block:: jinja
+
+ {{ post.published_at|date("F jS \\a\\t g:ia") }}
+
+If the value passed to the ``date`` filter is ``null``, it will return the
+current date by default. If an empty string is desired instead of the current
+date, use a ternary operator:
+
+.. code-block:: jinja
+
+ {{ post.published_at is empty ? "" : post.published_at|date("m/d/Y") }}
+
+If no format is provided, Twig will use the default one: ``F j, Y H:i``. This
+default can be easily changed by calling the ``setDateFormat()`` method on the
+``core`` extension instance. The first argument is the default format for
+dates and the second one is the default format for date intervals:
+
+.. code-block:: php
+
+ $twig = new Twig_Environment($loader);
+ $twig->getExtension('core')->setDateFormat('d/m/Y', '%d days');
+
+Timezone
+--------
+
+By default, the date is displayed by applying the default timezone (the one
+specified in php.ini or declared in Twig -- see below), but you can override
+it by explicitly specifying a timezone:
+
+.. code-block:: jinja
+
+ {{ post.published_at|date("m/d/Y", "Europe/Paris") }}
+
+If the date is already a DateTime object, and if you want to keep its current
+timezone, pass ``false`` as the timezone value:
+
+.. code-block:: jinja
+
+ {{ post.published_at|date("m/d/Y", false) }}
+
+The default timezone can also be set globally by calling ``setTimezone()``:
+
+.. code-block:: php
+
+ $twig = new Twig_Environment($loader);
+ $twig->getExtension('core')->setTimezone('Europe/Paris');
+
+Arguments
+---------
+
+ * ``format``: The date format
+ * ``timezone``: The date timezone
+
+.. _`strtotime`: http://www.php.net/strtotime
+.. _`DateTime`: http://www.php.net/DateTime
+.. _`DateInterval`: http://www.php.net/DateInterval
--- /dev/null
+``date_modify``
+===============
+
+.. versionadded:: 1.9.0
+ The date_modify filter has been added in Twig 1.9.0.
+
+The ``date_modify`` filter modifies a date with a given modifier string:
+
+.. code-block:: jinja
+
+ {{ post.published_at|date_modify("+1 day")|date("m/d/Y") }}
+
+The ``date_modify`` filter accepts strings (it must be in a format supported
+by the `strtotime`_ function) or `DateTime`_ instances. You can easily combine
+it with the :doc:`date<date>` filter for formatting.
+
+Arguments
+---------
+
+ * ``modifier``: The modifier
+
+.. _`strtotime`: http://www.php.net/strtotime
+.. _`DateTime`: http://www.php.net/DateTime
--- /dev/null
+``default``
+===========
+
+The ``default`` filter returns the passed default value if the value is
+undefined or empty, otherwise the value of the variable:
+
+.. code-block:: jinja
+
+ {{ var|default('var is not defined') }}
+
+ {{ var.foo|default('foo item on var is not defined') }}
+
+ {{ var['foo']|default('foo item on var is not defined') }}
+
+ {{ ''|default('passed var is empty') }}
+
+When using the ``default`` filter on an expression that uses variables in some
+method calls, be sure to use the ``default`` filter whenever a variable can be
+undefined:
+
+.. code-block:: jinja
+
+ {{ var.method(foo|default('foo'))|default('foo') }}
+
+.. note::
+
+ Read the documentation for the :doc:`defined<../tests/defined>` and
+ :doc:`empty<../tests/empty>` tests to learn more about their semantics.
+
+Arguments
+---------
+
+ * ``default``: The default value
--- /dev/null
+``escape``
+==========
+
+.. versionadded:: 1.9.0
+ The ``css``, ``url``, and ``html_attr`` strategies were added in Twig
+ 1.9.0.
+
+The ``escape`` filter escapes a string for safe insertion into the final
+output. It supports different escaping strategies depending on the template
+context.
+
+By default, it uses the HTML escaping strategy:
+
+.. code-block:: jinja
+
+ {{ user.username|escape }}
+
+For convenience, the ``e`` filter is defined as an alias:
+
+.. code-block:: jinja
+
+ {{ user.username|e }}
+
+The ``escape`` filter can also be used in other contexts than HTML thanks to
+an optional argument which defines the escaping strategy to use:
+
+.. code-block:: jinja
+
+ {{ user.username|e }}
+ {# is equivalent to #}
+ {{ user.username|e('html') }}
+
+And here is how to escape variables included in JavaScript code:
+
+.. code-block:: jinja
+
+ {{ user.username|escape('js') }}
+ {{ user.username|e('js') }}
+
+The ``escape`` filter supports the following escaping strategies:
+
+* ``html``: escapes a string for the **HTML body** context.
+
+* ``js``: escapes a string for the **JavaScript context**.
+
+* ``css``: escapes a string for the **CSS context**. CSS escaping can be
+ applied to any string being inserted into CSS and escapes everything except
+ alphanumerics.
+
+* ``url``: escapes a string for the **URI or parameter contexts**. This should
+ not be used to escape an entire URI; only a subcomponent being inserted.
+
+* ``html_attr``: escapes a string for the **HTML attribute** context.
+
+.. note::
+
+ Internally, ``escape`` uses the PHP native `htmlspecialchars`_ function
+ for the HTML escaping strategy.
+
+.. caution::
+
+ When using automatic escaping, Twig tries to not double-escape a variable
+ when the automatic escaping strategy is the same as the one applied by the
+ escape filter; but that does not work when using a variable as the
+ escaping strategy:
+
+ .. code-block:: jinja
+
+ {% set strategy = 'html' %}
+
+ {% autoescape 'html' %}
+ {{ var|escape('html') }} {# won't be double-escaped #}
+ {{ var|escape(strategy) }} {# will be double-escaped #}
+ {% endautoescape %}
+
+ When using a variable as the escaping strategy, you should disable
+ automatic escaping:
+
+ .. code-block:: jinja
+
+ {% set strategy = 'html' %}
+
+ {% autoescape 'html' %}
+ {{ var|escape(strategy)|raw }} {# won't be double-escaped #}
+ {% endautoescape %}
+
+Arguments
+---------
+
+ * ``strategy``: The escaping strategy
+ * ``charset``: The string charset
+
+.. _`htmlspecialchars`: http://php.net/htmlspecialchars
--- /dev/null
+``first``
+=========
+
+.. versionadded:: 1.12.2
+ The first filter was added in Twig 1.12.2.
+
+The ``first`` filter returns the first "element" of a sequence, a mapping, or
+a string:
+
+.. code-block:: jinja
+
+ {{ [1, 2, 3, 4]|first }}
+ {# outputs 1 #}
+
+ {{ { a: 1, b: 2, c: 3, d: 4 }|first }}
+ {# outputs 1 #}
+
+ {{ '1234'|first }}
+ {# outputs 1 #}
+
+.. note::
+
+ It also works with objects implementing the `Traversable`_ interface.
+
+.. _`Traversable`: http://php.net/manual/en/class.traversable.php
--- /dev/null
+``format``
+==========
+
+The ``format`` filter formats a given string by replacing the placeholders
+(placeholders follows the `sprintf`_ notation):
+
+.. code-block:: jinja
+
+ {{ "I like %s and %s."|format(foo, "bar") }}
+
+ {# returns I like foo and bar
+ if the foo parameter equals to the foo string. #}
+
+.. _`sprintf`: http://www.php.net/sprintf
+
+.. seealso:: :doc:`replace<replace>`
--- /dev/null
+Filters
+=======
+
+.. toctree::
+ :maxdepth: 1
+
+ abs
+ batch
+ capitalize
+ convert_encoding
+ date
+ date_modify
+ default
+ escape
+ first
+ format
+ join
+ json_encode
+ keys
+ last
+ length
+ lower
+ nl2br
+ number_format
+ merge
+ upper
+ raw
+ replace
+ reverse
+ slice
+ sort
+ split
+ striptags
+ title
+ trim
+ url_encode
--- /dev/null
+``join``
+========
+
+The ``join`` filter returns a string which is the concatenation of the items
+of a sequence:
+
+.. code-block:: jinja
+
+ {{ [1, 2, 3]|join }}
+ {# returns 123 #}
+
+The separator between elements is an empty string per default, but you can
+define it with the optional first parameter:
+
+.. code-block:: jinja
+
+ {{ [1, 2, 3]|join('|') }}
+ {# returns 1|2|3 #}
+
+Arguments
+---------
+
+ * ``glue``: The separator
--- /dev/null
+``json_encode``
+===============
+
+The ``json_encode`` filter returns the JSON representation of a string:
+
+.. code-block:: jinja
+
+ {{ data|json_encode() }}
+
+.. note::
+
+ Internally, Twig uses the PHP `json_encode`_ function.
+
+Arguments
+---------
+
+ * ``options``: A bitmask of `json_encode options`_ (``{{
+ data|json_encode(constant(JSON_PRETTY_PRINT)) }}``)
+
+.. _`json_encode`: http://php.net/json_encode
+.. _`json_encode options`: http://www.php.net/manual/en/json.constants.php
--- /dev/null
+``keys``
+========
+
+The ``keys`` filter returns the keys of an array. It is useful when you want to
+iterate over the keys of an array:
+
+.. code-block:: jinja
+
+ {% for key in array|keys %}
+ ...
+ {% endfor %}
--- /dev/null
+``last``
+========
+
+.. versionadded:: 1.12.2
+ The last filter was added in Twig 1.12.2.
+
+The ``last`` filter returns the last "element" of a sequence, a mapping, or
+a string:
+
+.. code-block:: jinja
+
+ {{ [1, 2, 3, 4]|last }}
+ {# outputs 4 #}
+
+ {{ { a: 1, b: 2, c: 3, d: 4 }|last }}
+ {# outputs 4 #}
+
+ {{ '1234'|last }}
+ {# outputs 4 #}
+
+.. note::
+
+ It also works with objects implementing the `Traversable`_ interface.
+
+.. _`Traversable`: http://php.net/manual/en/class.traversable.php
--- /dev/null
+``length``
+==========
+
+The ``length`` filters returns the number of items of a sequence or mapping, or
+the length of a string:
+
+.. code-block:: jinja
+
+ {% if users|length > 10 %}
+ ...
+ {% endif %}
+
--- /dev/null
+``lower``
+=========
+
+The ``lower`` filter converts a value to lowercase:
+
+.. code-block:: jinja
+
+ {{ 'WELCOME'|lower }}
+
+ {# outputs 'welcome' #}
--- /dev/null
+``merge``
+=========
+
+The ``merge`` filter merges an array with another array:
+
+.. code-block:: jinja
+
+ {% set values = [1, 2] %}
+
+ {% set values = values|merge(['apple', 'orange']) %}
+
+ {# values now contains [1, 2, 'apple', 'orange'] #}
+
+New values are added at the end of the existing ones.
+
+The ``merge`` filter also works on hashes:
+
+.. code-block:: jinja
+
+ {% set items = { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'unknown' } %}
+
+ {% set items = items|merge({ 'peugeot': 'car', 'renault': 'car' }) %}
+
+ {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car' } #}
+
+For hashes, the merging process occurs on the keys: if the key does not
+already exist, it is added but if the key already exists, its value is
+overridden.
+
+.. tip::
+
+ If you want to ensure that some values are defined in an array (by given
+ default values), reverse the two elements in the call:
+
+ .. code-block:: jinja
+
+ {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
+
+ {% set items = { 'apple': 'unknown' }|merge(items) %}
+
+ {# items now contains { 'apple': 'fruit', 'orange': 'fruit' } #}
--- /dev/null
+``nl2br``
+=========
+
+.. versionadded:: 1.5
+ The nl2br filter was added in Twig 1.5.
+
+The ``nl2br`` filter inserts HTML line breaks before all newlines in a string:
+
+.. code-block:: jinja
+
+ {{ "I like Twig.\nYou will like it too."|nl2br }}
+ {# outputs
+
+ I like Twig.<br />
+ You will like it too.
+
+ #}
+
+.. note::
+
+ The ``nl2br`` filter pre-escapes the input before applying the
+ transformation.
--- /dev/null
+``number_format``
+=================
+
+.. versionadded:: 1.5
+ The number_format filter was added in Twig 1.5
+
+The ``number_format`` filter formats numbers. It is a wrapper around PHP's
+`number_format`_ function:
+
+.. code-block:: jinja
+
+ {{ 200.35|number_format }}
+
+You can control the number of decimal places, decimal point, and thousands
+separator using the additional arguments:
+
+.. code-block:: jinja
+
+ {{ 9800.333|number_format(2, '.', ',') }}
+
+If no formatting options are provided then Twig will use the default formatting
+options of:
+
+- 0 decimal places.
+- ``.`` as the decimal point.
+- ``,`` as the thousands separator.
+
+These defaults can be easily changed through the core extension:
+
+.. code-block:: php
+
+ $twig = new Twig_Environment($loader);
+ $twig->getExtension('core')->setNumberFormat(3, '.', ',');
+
+The defaults set for ``number_format`` can be over-ridden upon each call using the
+additional parameters.
+
+Arguments
+---------
+
+ * ``decimal``: The number of decimal points to display
+ * ``decimal_point``: The character(s) to use for the decimal point
+ * ``decimal_sep``: The character(s) to use for the thousands separator
+
+.. _`number_format`: http://php.net/number_format
--- /dev/null
+``raw``
+=======
+
+The ``raw`` filter marks the value as being "safe", which means that in an
+environment with automatic escaping enabled this variable will not be escaped
+if ``raw`` is the last filter applied to it:
+
+.. code-block:: jinja
+
+ {% autoescape true %}
+ {{ var|raw }} {# var won't be escaped #}
+ {% endautoescape %}
--- /dev/null
+``replace``
+===========
+
+The ``replace`` filter formats a given string by replacing the placeholders
+(placeholders are free-form):
+
+.. code-block:: jinja
+
+ {{ "I like %this% and %that%."|replace({'%this%': foo, '%that%': "bar"}) }}
+
+ {# returns I like foo and bar
+ if the foo parameter equals to the foo string. #}
+
+Arguments
+---------
+
+ * ``replace_pairs``: The placeholder values
+
+.. seealso:: :doc:`format<format>`
--- /dev/null
+``reverse``
+===========
+
+.. versionadded:: 1.6
+ Support for strings has been added in Twig 1.6.
+
+The ``reverse`` filter reverses a sequence, a mapping, or a string:
+
+.. code-block:: jinja
+
+ {% for user in users|reverse %}
+ ...
+ {% endfor %}
+
+ {{ '1234'|reverse }}
+
+ {# outputs 4321 #}
+
+.. tip::
+
+ For sequences and mappings, numeric keys are not preserved. To reverse
+ them as well, pass ``true`` as an argument to the ``reverse`` filter:
+
+ .. code-block:: jinja
+
+ {% for key, value in {1: "a", 2: "b", 3: "c"}|reverse %}
+ {{ key }}: {{ value }}
+ {%- endfor %}
+
+ {# output: 0: c 1: b 2: a #}
+
+ {% for key, value in {1: "a", 2: "b", 3: "c"}|reverse(true) %}
+ {{ key }}: {{ value }}
+ {%- endfor %}
+
+ {# output: 3: c 2: b 1: a #}
+
+.. note::
+
+ It also works with objects implementing the `Traversable`_ interface.
+
+Arguments
+---------
+
+ * ``preserve_keys``: Preserve keys when reversing a mapping or a sequence.
+
+.. _`Traversable`: http://php.net/Traversable
--- /dev/null
+``slice``
+===========
+
+.. versionadded:: 1.6
+ The slice filter was added in Twig 1.6.
+
+The ``slice`` filter extracts a slice of a sequence, a mapping, or a string:
+
+.. code-block:: jinja
+
+ {% for i in [1, 2, 3, 4, 5]|slice(1, 2) %}
+ {# will iterate over 2 and 3 #}
+ {% endfor %}
+
+ {{ '12345'|slice(1, 2) }}
+
+ {# outputs 23 #}
+
+You can use any valid expression for both the start and the length:
+
+.. code-block:: jinja
+
+ {% for i in [1, 2, 3, 4, 5]|slice(start, length) %}
+ {# ... #}
+ {% endfor %}
+
+As syntactic sugar, you can also use the ``[]`` notation:
+
+.. code-block:: jinja
+
+ {% for i in [1, 2, 3, 4, 5][start:length] %}
+ {# ... #}
+ {% endfor %}
+
+ {{ '12345'[1:2] }}
+
+ {# you can omit the first argument -- which is the same as 0 #}
+ {{ '12345'[:2] }} {# will display "12" #}
+
+ {# you can omit the last argument -- which will select everything till the end #}
+ {{ '12345'[2:] }} {# will display "345" #}
+
+The ``slice`` filter works as the `array_slice`_ PHP function for arrays and
+`substr`_ for strings.
+
+If the start is non-negative, the sequence will start at that start in the
+variable. If start is negative, the sequence will start that far from the end
+of the variable.
+
+If length is given and is positive, then the sequence will have up to that
+many elements in it. If the variable is shorter than the length, then only the
+available variable elements will be present. If length is given and is
+negative then the sequence will stop that many elements from the end of the
+variable. If it is omitted, then the sequence will have everything from offset
+up until the end of the variable.
+
+.. note::
+
+ It also works with objects implementing the `Traversable`_ interface.
+
+Arguments
+---------
+
+ * ``start``: The start of the slice
+ * ``length``: The size of the slice
+ * ``preserve_keys``: Whether to preserve key or not (when the input is an array)
+
+.. _`Traversable`: http://php.net/manual/en/class.traversable.php
+.. _`array_slice`: http://php.net/array_slice
+.. _`substr`: http://php.net/substr
--- /dev/null
+``sort``
+========
+
+The ``sort`` filter sorts an array:
+
+.. code-block:: jinja
+
+ {% for user in users|sort %}
+ ...
+ {% endfor %}
+
+.. note::
+
+ Internally, Twig uses the PHP `asort`_ function to maintain index
+ association.
+
+.. _`asort`: http://php.net/asort
--- /dev/null
+``split``
+=========
+
+.. versionadded:: 1.10.3
+ The split filter was added in Twig 1.10.3.
+
+The ``split`` filter splits a string by the given delimiter and returns a list
+of strings:
+
+.. code-block:: jinja
+
+ {{ "one,two,three"|split(',') }}
+ {# returns ['one', 'two', 'three'] #}
+
+You can also pass a ``limit`` argument:
+
+ * If ``limit`` is positive, the returned array will contain a maximum of
+ limit elements with the last element containing the rest of string;
+
+ * If ``limit`` is negative, all components except the last -limit are
+ returned;
+
+ * If ``limit`` is zero, then this is treated as 1.
+
+.. code-block:: jinja
+
+ {{ "one,two,three,four,five"|split(',', 3) }}
+ {# returns ['one', 'two', 'three,four,five'] #}
+
+If the ``delimiter`` is an empty string, then value will be split by equal
+chunks. Length is set by the ``limit`` argument (one character by default).
+
+.. code-block:: jinja
+
+ {{ "123"|split('') }}
+ {# returns ['1', '2', '3'] #}
+
+ {{ "aabbcc"|split('', 2) }}
+ {# returns ['aa', 'bb', 'cc'] #}
+
+.. note::
+
+ Internally, Twig uses the PHP `explode`_ or `str_split`_ (if delimiter is
+ empty) functions for string splitting.
+
+Arguments
+---------
+
+ * ``delimiter``: The delimiter
+ * ``limit``: The limit argument
+
+.. _`explode`: http://php.net/explode
+.. _`str_split`: http://php.net/str_split
--- /dev/null
+``striptags``
+=============
+
+The ``striptags`` filter strips SGML/XML tags and replace adjacent whitespace
+by one space:
+
+.. code-block:: jinja
+
+ {{ some_html|striptags }}
+
+.. note::
+
+ Internally, Twig uses the PHP `strip_tags`_ function.
+
+.. _`strip_tags`: http://php.net/strip_tags
--- /dev/null
+``title``
+=========
+
+The ``title`` filter returns a titlecased version of the value. Words will
+start with uppercase letters, all remaining characters are lowercase:
+
+.. code-block:: jinja
+
+ {{ 'my first car'|title }}
+
+ {# outputs 'My First Car' #}
--- /dev/null
+``trim``
+========
+
+.. versionadded:: 1.6.2
+ The trim filter was added in Twig 1.6.2.
+
+The ``trim`` filter strips whitespace (or other characters) from the beginning
+and end of a string:
+
+.. code-block:: jinja
+
+ {{ ' I like Twig. '|trim }}
+
+ {# outputs 'I like Twig.' #}
+
+ {{ ' I like Twig.'|trim('.') }}
+
+ {# outputs ' I like Twig' #}
+
+.. note::
+
+ Internally, Twig uses the PHP `trim`_ function.
+
+Arguments
+---------
+
+ * ``character_mask``: The characters to strip
+
+.. _`trim`: http://php.net/trim
--- /dev/null
+``upper``
+=========
+
+The ``upper`` filter converts a value to uppercase:
+
+.. code-block:: jinja
+
+ {{ 'welcome'|upper }}
+
+ {# outputs 'WELCOME' #}
--- /dev/null
+``url_encode``
+==============
+
+.. versionadded:: 1.12.3
+ Support for encoding an array as query string was added in Twig 1.12.3.
+
+The ``url_encode`` filter percent encodes a given string as URL segment
+or an array as query string:
+
+.. code-block:: jinja
+
+ {{ "path-seg*ment"|url_encode }}
+ {# outputs "path-seg%2Ament" #}
+
+ {{ "string with spaces"|url_encode(true) }}
+ {# outputs "string%20with%20spaces" #}
+
+ {{ {'param': 'value', 'foo': 'bar'}|url_encode }}
+ {# outputs "param=value&foo=bar" #}
+
+.. note::
+
+ Internally, Twig uses the PHP `urlencode`_ (or `rawurlencode`_ if you pass
+ ``true`` as the first parameter) or the `http_build_query`_ function.
+
+.. _`urlencode`: http://php.net/urlencode
+.. _`rawurlencode`: http://php.net/rawurlencode
+.. _`http_build_query`: http://php.net/http_build_query
--- /dev/null
+``attribute``
+=============
+
+.. versionadded:: 1.2
+ The ``attribute`` function was added in Twig 1.2.
+
+``attribute`` can be used to access a "dynamic" attribute of a variable:
+
+.. code-block:: jinja
+
+ {{ attribute(object, method) }}
+ {{ attribute(object, method, arguments) }}
+ {{ attribute(array, item) }}
+
+.. note::
+
+ The resolution algorithm is the same as the one used for the ``.``
+ notation, except that the item can be any valid expression.
--- /dev/null
+``block``
+=========
+
+When a template uses inheritance and if you want to print a block multiple
+times, use the ``block`` function:
+
+.. code-block:: jinja
+
+ <title>{% block title %}{% endblock %}</title>
+
+ <h1>{{ block('title') }}</h1>
+
+ {% block body %}{% endblock %}
+
+.. seealso:: :doc:`extends<../tags/extends>`, :doc:`parent<../functions/parent>`
--- /dev/null
+``constant``
+============
+
+.. versionadded: 1.12.1
+ constant now accepts object instances as the second argument.
+
+``constant`` returns the constant value for a given string:
+
+.. code-block:: jinja
+
+ {{ some_date|date(constant('DATE_W3C')) }}
+ {{ constant('Namespace\\Classname::CONSTANT_NAME') }}
+
+As of 1.12.1 you can read constants from object instances as well:
+
+.. code-block:: jinja
+
+ {{ constant('RSS', date) }}
--- /dev/null
+``cycle``
+=========
+
+The ``cycle`` function cycles on an array of values:
+
+.. code-block:: jinja
+
+ {% for i in 0..10 %}
+ {{ cycle(['odd', 'even'], i) }}
+ {% endfor %}
+
+The array can contain any number of values:
+
+.. code-block:: jinja
+
+ {% set fruits = ['apple', 'orange', 'citrus'] %}
+
+ {% for i in 0..10 %}
+ {{ cycle(fruits, i) }}
+ {% endfor %}
+
+Arguments
+---------
+
+ * ``position``: The cycle position
--- /dev/null
+``date``
+========
+
+.. versionadded:: 1.6
+ The date function has been added in Twig 1.6.
+
+.. versionadded:: 1.6.1
+ The default timezone support has been added in Twig 1.6.1.
+
+Converts an argument to a date to allow date comparison:
+
+.. code-block:: jinja
+
+ {% if date(user.created_at) < date('-2days') %}
+ {# do something #}
+ {% endif %}
+
+The argument must be in a format supported by the `date`_ function.
+
+You can pass a timezone as the second argument:
+
+.. code-block:: jinja
+
+ {% if date(user.created_at) < date('-2days', 'Europe/Paris') %}
+ {# do something #}
+ {% endif %}
+
+If no argument is passed, the function returns the current date:
+
+.. code-block:: jinja
+
+ {% if date(user.created_at) < date() %}
+ {# always! #}
+ {% endif %}
+
+.. note::
+
+ You can set the default timezone globally by calling ``setTimezone()`` on
+ the ``core`` extension instance:
+
+ .. code-block:: php
+
+ $twig = new Twig_Environment($loader);
+ $twig->getExtension('core')->setTimezone('Europe/Paris');
+
+Arguments
+---------
+
+ * ``date``: The date
+ * ``timezone``: The timezone
+
+.. _`date`: http://www.php.net/date
--- /dev/null
+``dump``
+========
+
+.. versionadded:: 1.5
+ The dump function was added in Twig 1.5.
+
+The ``dump`` function dumps information about a template variable. This is
+mostly useful to debug a template that does not behave as expected by
+introspecting its variables:
+
+.. code-block:: jinja
+
+ {{ dump(user) }}
+
+.. note::
+
+ The ``dump`` function is not available by default. You must add the
+ ``Twig_Extension_Debug`` extension explicitly when creating your Twig
+ environment::
+
+ $twig = new Twig_Environment($loader, array(
+ 'debug' => true,
+ // ...
+ ));
+ $twig->addExtension(new Twig_Extension_Debug());
+
+ Even when enabled, the ``dump`` function won't display anything if the
+ ``debug`` option on the environment is not enabled (to avoid leaking debug
+ information on a production server).
+
+In an HTML context, wrap the output with a ``pre`` tag to make it easier to
+read:
+
+.. code-block:: jinja
+
+ <pre>
+ {{ dump(user) }}
+ </pre>
+
+.. tip::
+
+ Using a ``pre`` tag is not needed when `XDebug`_ is enabled and
+ ``html_errors`` is ``on``; as a bonus, the output is also nicer with
+ XDebug enabled.
+
+You can debug several variables by passing them as additional arguments:
+
+.. code-block:: jinja
+
+ {{ dump(user, categories) }}
+
+If you don't pass any value, all variables from the current context are
+dumped:
+
+.. code-block:: jinja
+
+ {{ dump() }}
+
+.. note::
+
+ Internally, Twig uses the PHP `var_dump`_ function.
+
+Arguments
+---------
+
+ * ``context``: The context to dump
+
+.. _`XDebug`: http://xdebug.org/docs/display
+.. _`var_dump`: http://php.net/var_dump
--- /dev/null
+``include``
+===========
+
+.. versionadded:: 1.12
+ The include function was added in Twig 1.12.
+
+The ``include`` function returns the rendered content of a template:
+
+.. code-block:: jinja
+
+ {{ include('template.html') }}
+ {{ include(some_var) }}
+
+Included templates have access to the variables of the active context.
+
+If you are using the filesystem loader, the templates are looked for in the
+paths defined by it.
+
+The context is passed by default to the template but you can also pass
+additional variables:
+
+.. code-block:: jinja
+
+ {# template.html will have access to the variables from the current context and the additional ones provided #}
+ {{ include('template.html', {foo: 'bar'}) }}
+
+You can disable access to the context by setting ``with_context`` to
+``false``:
+
+.. code-block:: jinja
+
+ {# only the foo variable will be accessible #}
+ {{ include('template.html', {foo: 'bar'}, with_context = false) }}
+
+.. code-block:: jinja
+
+ {# no variables will be accessible #}
+ {{ include('template.html', with_context = false) }}
+
+And if the expression evaluates to a ``Twig_Template`` object, Twig will use it
+directly::
+
+ // {{ include(template) }}
+
+ $template = $twig->loadTemplate('some_template.twig');
+
+ $twig->loadTemplate('template.twig')->display(array('template' => $template));
+
+When you set the ``ignore_missing`` flag, Twig will return an empty string if
+the template does not exist:
+
+.. code-block:: jinja
+
+ {{ include('sidebar.html', ignore_missing = true) }}
+
+You can also provide a list of templates that are checked for existence before
+inclusion. The first template that exists will be rendered:
+
+.. code-block:: jinja
+
+ {{ include(['page_detailed.html', 'page.html']) }}
+
+If ``ignore_missing`` is set, it will fall back to rendering nothing if none
+of the templates exist, otherwise it will throw an exception.
+
+When including a template created by an end user, you should consider
+sandboxing it:
+
+.. code-block:: jinja
+
+ {{ include('page.html', sandboxed = true) }}
+
+Arguments
+---------
+
+ * ``template``: The template to render
+ * ``variables``: The variables to pass to the template
+ * ``with_context``: Whether to pass the current context variables or not
+ * ``ignore_missing``: Whether to ignore missing templates or not
+ * ``sandboxed``: Whether to sandbox the template or not
--- /dev/null
+Functions
+=========
+
+.. toctree::
+ :maxdepth: 1
+
+ attribute
+ block
+ constant
+ cycle
+ date
+ dump
+ include
+ parent
+ random
+ range
+ template_from_string
--- /dev/null
+``parent``
+==========
+
+When a template uses inheritance, it's possible to render the contents of the
+parent block when overriding a block by using the ``parent`` function:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% block sidebar %}
+ <h3>Table Of Contents</h3>
+ ...
+ {{ parent() }}
+ {% endblock %}
+
+The ``parent()`` call will return the content of the ``sidebar`` block as
+defined in the ``base.html`` template.
+
+.. seealso:: :doc:`extends<../tags/extends>`, :doc:`block<../functions/block>`, :doc:`block<../tags/block>`
--- /dev/null
+``random``
+==========
+
+.. versionadded:: 1.5
+ The random function was added in Twig 1.5.
+
+.. versionadded:: 1.6
+ String and integer handling was added in Twig 1.6.
+
+The ``random`` function returns a random value depending on the supplied
+parameter type:
+
+* a random item from a sequence;
+* a random character from a string;
+* a random integer between 0 and the integer parameter (inclusive).
+
+.. code-block:: jinja
+
+ {{ random(['apple', 'orange', 'citrus']) }} {# example output: orange #}
+ {{ random('ABC') }} {# example output: C #}
+ {{ random() }} {# example output: 15386094 (works as native PHP `mt_rand`_ function) #}
+ {{ random(5) }} {# example output: 3 #}
+
+Arguments
+---------
+
+ * ``values``: The values
+
+.. _`mt_rand`: http://php.net/mt_rand
--- /dev/null
+``range``
+=========
+
+Returns a list containing an arithmetic progression of integers:
+
+.. code-block:: jinja
+
+ {% for i in range(0, 3) %}
+ {{ i }},
+ {% endfor %}
+
+ {# returns 0, 1, 2, 3 #}
+
+When step is given (as the third parameter), it specifies the increment (or
+decrement):
+
+.. code-block:: jinja
+
+ {% for i in range(0, 6, 2) %}
+ {{ i }},
+ {% endfor %}
+
+ {# returns 0, 2, 4, 6 #}
+
+The Twig built-in ``..`` operator is just syntactic sugar for the ``range``
+function (with a step of 1):
+
+.. code-block:: jinja
+
+ {% for i in 0..3 %}
+ {{ i }},
+ {% endfor %}
+
+.. tip::
+
+ The ``range`` function works as the native PHP `range`_ function.
+
+Arguments
+---------
+
+ * ``low``: The first value of the sequence.
+ * ``high``: The highest possible value of the sequence.
+ * ``step``: The increment between elements of the sequence.
+
+.. _`range`: http://php.net/range
--- /dev/null
+``template_from_string``
+========================
+
+.. versionadded:: 1.11
+ The template_from_string function was added in Twig 1.11.
+
+The ``template_from_string`` function loads a template from a string:
+
+.. code-block:: jinja
+
+ {{ include(template_from_string("Hello {{ name }}") }}
+ {{ include(template_from_string(page.template)) }}
+
+.. note::
+
+ The ``template_from_string`` function is not available by default. You
+ must add the ``Twig_Extension_StringLoader`` extension explicitly when
+ creating your Twig environment::
+
+ $twig = new Twig_Environment(...);
+ $twig->addExtension(new Twig_Extension_StringLoader());
+
+.. note::
+
+ Even if you will probably always use the ``template_from_string`` function
+ with the ``include`` function, you can use it with any tag or function that
+ takes a template as an argument (like the ``embed`` or ``extends`` tags).
+
+Arguments
+---------
+
+ * ``template``: The template
--- /dev/null
+Twig
+====
+
+.. toctree::
+ :maxdepth: 2
+
+ intro
+ templates
+ api
+ advanced
+ internals
+ recipes
+ coding_standards
+ tags/index
+ filters/index
+ functions/index
+ tests/index
+ deprecated
--- /dev/null
+Twig Internals
+==============
+
+Twig is very extensible and you can easily hack it. Keep in mind that you
+should probably try to create an extension before hacking the core, as most
+features and enhancements can be done with extensions. This chapter is also
+useful for people who want to understand how Twig works under the hood.
+
+How Twig works?
+---------------
+
+The rendering of a Twig template can be summarized into four key steps:
+
+* **Load** the template: If the template is already compiled, load it and go
+ to the *evaluation* step, otherwise:
+
+ * First, the **lexer** tokenizes the template source code into small pieces
+ for easier processing;
+ * Then, the **parser** converts the token stream into a meaningful tree
+ of nodes (the Abstract Syntax Tree);
+ * Eventually, the *compiler* transforms the AST into PHP code;
+
+* **Evaluate** the template: It basically means calling the ``display()``
+ method of the compiled template and passing it the context.
+
+The Lexer
+---------
+
+The lexer tokenizes a template source code into a token stream (each token is
+an instance of ``Twig_Token``, and the stream is an instance of
+``Twig_TokenStream``). The default lexer recognizes 13 different token types:
+
+* ``Twig_Token::BLOCK_START_TYPE``, ``Twig_Token::BLOCK_END_TYPE``: Delimiters for blocks (``{% %}``)
+* ``Twig_Token::VAR_START_TYPE``, ``Twig_Token::VAR_END_TYPE``: Delimiters for variables (``{{ }}``)
+* ``Twig_Token::TEXT_TYPE``: A text outside an expression;
+* ``Twig_Token::NAME_TYPE``: A name in an expression;
+* ``Twig_Token::NUMBER_TYPE``: A number in an expression;
+* ``Twig_Token::STRING_TYPE``: A string in an expression;
+* ``Twig_Token::OPERATOR_TYPE``: An operator;
+* ``Twig_Token::PUNCTUATION_TYPE``: A punctuation sign;
+* ``Twig_Token::INTERPOLATION_START_TYPE``, ``Twig_Token::INTERPOLATION_END_TYPE`` (as of Twig 1.5): Delimiters for string interpolation;
+* ``Twig_Token::EOF_TYPE``: Ends of template.
+
+You can manually convert a source code into a token stream by calling the
+``tokenize()`` of an environment::
+
+ $stream = $twig->tokenize($source, $identifier);
+
+As the stream has a ``__toString()`` method, you can have a textual
+representation of it by echoing the object::
+
+ echo $stream."\n";
+
+Here is the output for the ``Hello {{ name }}`` template:
+
+.. code-block:: text
+
+ TEXT_TYPE(Hello )
+ VAR_START_TYPE()
+ NAME_TYPE(name)
+ VAR_END_TYPE()
+ EOF_TYPE()
+
+.. note::
+
+ You can change the default lexer use by Twig (``Twig_Lexer``) by calling
+ the ``setLexer()`` method::
+
+ $twig->setLexer($lexer);
+
+The Parser
+----------
+
+The parser converts the token stream into an AST (Abstract Syntax Tree), or a
+node tree (an instance of ``Twig_Node_Module``). The core extension defines
+the basic nodes like: ``for``, ``if``, ... and the expression nodes.
+
+You can manually convert a token stream into a node tree by calling the
+``parse()`` method of an environment::
+
+ $nodes = $twig->parse($stream);
+
+Echoing the node object gives you a nice representation of the tree::
+
+ echo $nodes."\n";
+
+Here is the output for the ``Hello {{ name }}`` template:
+
+.. code-block:: text
+
+ Twig_Node_Module(
+ Twig_Node_Text(Hello )
+ Twig_Node_Print(
+ Twig_Node_Expression_Name(name)
+ )
+ )
+
+.. note::
+
+ The default parser (``Twig_TokenParser``) can be also changed by calling the
+ ``setParser()`` method::
+
+ $twig->setParser($parser);
+
+The Compiler
+------------
+
+The last step is done by the compiler. It takes a node tree as an input and
+generates PHP code usable for runtime execution of the template.
+
+You can call the compiler by hand with the ``compile()`` method of an
+environment::
+
+ $php = $twig->compile($nodes);
+
+The ``compile()`` method returns the PHP source code representing the node.
+
+The generated template for a ``Hello {{ name }}`` template reads as follows
+(the actual output can differ depending on the version of Twig you are
+using)::
+
+ /* Hello {{ name }} */
+ class __TwigTemplate_1121b6f109fe93ebe8c6e22e3712bceb extends Twig_Template
+ {
+ protected function doDisplay(array $context, array $blocks = array())
+ {
+ // line 1
+ echo "Hello ";
+ echo twig_escape_filter($this->env, $this->getContext($context, "name"), "ndex", null, true);
+ }
+
+ // some more code
+ }
+
+.. note::
+
+ As for the lexer and the parser, the default compiler (``Twig_Compiler``) can
+ be changed by calling the ``setCompiler()`` method::
+
+ $twig->setCompiler($compiler);
--- /dev/null
+Introduction
+============
+
+This is the documentation for Twig, the flexible, fast, and secure template
+engine for PHP.
+
+If you have any exposure to other text-based template languages, such as
+Smarty, Django, or Jinja, you should feel right at home with Twig. It's both
+designer and developer friendly by sticking to PHP's principles and adding
+functionality useful for templating environments.
+
+The key-features are...
+
+* *Fast*: Twig compiles templates down to plain optimized PHP code. The
+ overhead compared to regular PHP code was reduced to the very minimum.
+
+* *Secure*: Twig has a sandbox mode to evaluate untrusted template code. This
+ allows Twig to be used as a template language for applications where users
+ may modify the template design.
+
+* *Flexible*: Twig is powered by a flexible lexer and parser. This allows the
+ developer to define its own custom tags and filters, and create its own DSL.
+
+Prerequisites
+-------------
+
+Twig needs at least **PHP 5.2.4** to run.
+
+Installation
+------------
+
+You have multiple ways to install Twig.
+
+Installing via Composer (recommended)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Install composer in your project:
+
+.. code-block:: bash
+
+ curl -s http://getcomposer.org/installer | php
+
+2. Create a ``composer.json`` file in your project root:
+
+.. code-block:: javascript
+
+ {
+ "require": {
+ "twig/twig": "1.*"
+ }
+ }
+
+3. Install via composer
+
+.. code-block:: bash
+
+ php composer.phar install
+
+.. note::
+ If you want to learn more about Composer, the ``composer.json`` file syntax
+ and its usage, you can read the `online documentation`_.
+
+Installing from the tarball release
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Download the most recent tarball from the `download page`_
+2. Unpack the tarball
+3. Move the files somewhere in your project
+
+Installing the development version
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Install Git
+2. ``git clone git://github.com/fabpot/Twig.git``
+
+Installing the PEAR package
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+1. Install PEAR
+2. ``pear channel-discover pear.twig-project.org``
+3. ``pear install twig/Twig`` (or ``pear install twig/Twig-beta``)
+
+
+Installing the C extension
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.4
+ The C extension was added in Twig 1.4.
+
+Twig comes with a C extension that enhances the performance of the Twig
+runtime engine. You can install it like any other PHP extension:
+
+.. code-block:: bash
+
+ $ cd ext/twig
+ $ phpize
+ $ ./configure
+ $ make
+ $ make install
+
+Finally, enable the extension in your ``php.ini`` configuration file:
+
+.. code-block:: ini
+
+ extension=twig.so
+
+And from now on, Twig will automatically compile your templates to take
+advantage of the C extension. Note that this extension does not replace the
+PHP code but only provides an optimized version of the
+``Twig_Template::getAttribute()`` method.
+
+.. tip::
+
+ On Windows, you can also simply download and install a `pre-built DLL`_.
+
+Basic API Usage
+---------------
+
+This section gives you a brief introduction to the PHP API for Twig.
+
+The first step to use Twig is to register its autoloader::
+
+ require_once '/path/to/lib/Twig/Autoloader.php';
+ Twig_Autoloader::register();
+
+Replace the ``/path/to/lib/`` path with the path you used for Twig
+installation.
+
+If you have installed Twig via Composer you can take advantage of Composer's
+autoload mechanism by replacing the previous snippet for::
+
+ require_once '/path/to/vendor/autoload.php';
+
+.. note::
+
+ Twig follows the PEAR convention names for its classes, which means you
+ can easily integrate Twig classes loading in your own autoloader.
+
+.. code-block:: php
+
+ $loader = new Twig_Loader_String();
+ $twig = new Twig_Environment($loader);
+
+ echo $twig->render('Hello {{ name }}!', array('name' => 'Fabien'));
+
+Twig uses a loader (``Twig_Loader_String``) to locate templates, and an
+environment (``Twig_Environment``) to store the configuration.
+
+The ``render()`` method loads the template passed as a first argument and
+renders it with the variables passed as a second argument.
+
+As templates are generally stored on the filesystem, Twig also comes with a
+filesystem loader::
+
+ $loader = new Twig_Loader_Filesystem('/path/to/templates');
+ $twig = new Twig_Environment($loader, array(
+ 'cache' => '/path/to/compilation_cache',
+ ));
+
+ echo $twig->render('index.html', array('name' => 'Fabien'));
+
+.. _`download page`: https://github.com/fabpot/Twig/tags
+.. _`online documentation`: http://getcomposer.org/doc
+.. _`pre-built DLL`: https://github.com/stealth35/stealth35.github.com/downloads
--- /dev/null
+Recipes
+=======
+
+Making a Layout conditional
+---------------------------
+
+Working with Ajax means that the same content is sometimes displayed as is,
+and sometimes decorated with a layout. As Twig layout template names can be
+any valid expression, you can pass a variable that evaluates to ``true`` when
+the request is made via Ajax and choose the layout accordingly:
+
+.. code-block:: jinja
+
+ {% extends request.ajax ? "base_ajax.html" : "base.html" %}
+
+ {% block content %}
+ This is the content to be displayed.
+ {% endblock %}
+
+Making an Include dynamic
+-------------------------
+
+When including a template, its name does not need to be a string. For
+instance, the name can depend on the value of a variable:
+
+.. code-block:: jinja
+
+ {% include var ~ '_foo.html' %}
+
+If ``var`` evaluates to ``index``, the ``index_foo.html`` template will be
+rendered.
+
+As a matter of fact, the template name can be any valid expression, such as
+the following:
+
+.. code-block:: jinja
+
+ {% include var|default('index') ~ '_foo.html' %}
+
+Overriding a Template that also extends itself
+----------------------------------------------
+
+A template can be customized in two different ways:
+
+* *Inheritance*: A template *extends* a parent template and overrides some
+ blocks;
+
+* *Replacement*: If you use the filesystem loader, Twig loads the first
+ template it finds in a list of configured directories; a template found in a
+ directory *replaces* another one from a directory further in the list.
+
+But how do you combine both: *replace* a template that also extends itself
+(aka a template in a directory further in the list)?
+
+Let's say that your templates are loaded from both ``.../templates/mysite``
+and ``.../templates/default`` in this order. The ``page.twig`` template,
+stored in ``.../templates/default`` reads as follows:
+
+.. code-block:: jinja
+
+ {# page.twig #}
+ {% extends "layout.twig" %}
+
+ {% block content %}
+ {% endblock %}
+
+You can replace this template by putting a file with the same name in
+``.../templates/mysite``. And if you want to extend the original template, you
+might be tempted to write the following:
+
+.. code-block:: jinja
+
+ {# page.twig in .../templates/mysite #}
+ {% extends "page.twig" %} {# from .../templates/default #}
+
+Of course, this will not work as Twig will always load the template from
+``.../templates/mysite``.
+
+It turns out it is possible to get this to work, by adding a directory right
+at the end of your template directories, which is the parent of all of the
+other directories: ``.../templates`` in our case. This has the effect of
+making every template file within our system uniquely addressable. Most of the
+time you will use the "normal" paths, but in the special case of wanting to
+extend a template with an overriding version of itself we can reference its
+parent's full, unambiguous template path in the extends tag:
+
+.. code-block:: jinja
+
+ {# page.twig in .../templates/mysite #}
+ {% extends "default/page.twig" %} {# from .../templates #}
+
+.. note::
+
+ This recipe was inspired by the following Django wiki page:
+ http://code.djangoproject.com/wiki/ExtendingTemplates
+
+Customizing the Syntax
+----------------------
+
+Twig allows some syntax customization for the block delimiters. It's not
+recommended to use this feature as templates will be tied with your custom
+syntax. But for specific projects, it can make sense to change the defaults.
+
+To change the block delimiters, you need to create your own lexer object::
+
+ $twig = new Twig_Environment();
+
+ $lexer = new Twig_Lexer($twig, array(
+ 'tag_comment' => array('{#', '#}'),
+ 'tag_block' => array('{%', '%}'),
+ 'tag_variable' => array('{{', '}}'),
+ 'interpolation' => array('#{', '}'),
+ ));
+ $twig->setLexer($lexer);
+
+Here are some configuration example that simulates some other template engines
+syntax::
+
+ // Ruby erb syntax
+ $lexer = new Twig_Lexer($twig, array(
+ 'tag_comment' => array('<%#', '%>'),
+ 'tag_block' => array('<%', '%>'),
+ 'tag_variable' => array('<%=', '%>'),
+ ));
+
+ // SGML Comment Syntax
+ $lexer = new Twig_Lexer($twig, array(
+ 'tag_comment' => array('<!--#', '-->'),
+ 'tag_block' => array('<!--', '-->'),
+ 'tag_variable' => array('${', '}'),
+ ));
+
+ // Smarty like
+ $lexer = new Twig_Lexer($twig, array(
+ 'tag_comment' => array('{*', '*}'),
+ 'tag_block' => array('{', '}'),
+ 'tag_variable' => array('{$', '}'),
+ ));
+
+Using dynamic Object Properties
+-------------------------------
+
+When Twig encounters a variable like ``article.title``, it tries to find a
+``title`` public property in the ``article`` object.
+
+It also works if the property does not exist but is rather defined dynamically
+thanks to the magic ``__get()`` method; you just need to also implement the
+``__isset()`` magic method like shown in the following snippet of code::
+
+ class Article
+ {
+ public function __get($name)
+ {
+ if ('title' == $name) {
+ return 'The title';
+ }
+
+ // throw some kind of error
+ }
+
+ public function __isset($name)
+ {
+ if ('title' == $name) {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+Accessing the parent Context in Nested Loops
+--------------------------------------------
+
+Sometimes, when using nested loops, you need to access the parent context. The
+parent context is always accessible via the ``loop.parent`` variable. For
+instance, if you have the following template data::
+
+ $data = array(
+ 'topics' => array(
+ 'topic1' => array('Message 1 of topic 1', 'Message 2 of topic 1'),
+ 'topic2' => array('Message 1 of topic 2', 'Message 2 of topic 2'),
+ ),
+ );
+
+And the following template to display all messages in all topics:
+
+.. code-block:: jinja
+
+ {% for topic, messages in topics %}
+ * {{ loop.index }}: {{ topic }}
+ {% for message in messages %}
+ - {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }}
+ {% endfor %}
+ {% endfor %}
+
+The output will be similar to:
+
+.. code-block:: text
+
+ * 1: topic1
+ - 1.1: The message 1 of topic 1
+ - 1.2: The message 2 of topic 1
+ * 2: topic2
+ - 2.1: The message 1 of topic 2
+ - 2.2: The message 2 of topic 2
+
+In the inner loop, the ``loop.parent`` variable is used to access the outer
+context. So, the index of the current ``topic`` defined in the outer for loop
+is accessible via the ``loop.parent.loop.index`` variable.
+
+Defining undefined Functions and Filters on the Fly
+---------------------------------------------------
+
+When a function (or a filter) is not defined, Twig defaults to throw a
+``Twig_Error_Syntax`` exception. However, it can also call a `callback`_ (any
+valid PHP callable) which should return a function (or a filter).
+
+For filters, register callbacks with ``registerUndefinedFilterCallback()``.
+For functions, use ``registerUndefinedFunctionCallback()``::
+
+ // auto-register all native PHP functions as Twig functions
+ // don't try this at home as it's not secure at all!
+ $twig->registerUndefinedFunctionCallback(function ($name) {
+ if (function_exists($name)) {
+ return new Twig_Function_Function($name);
+ }
+
+ return false;
+ });
+
+If the callable is not able to return a valid function (or filter), it must
+return ``false``.
+
+If you register more than one callback, Twig will call them in turn until one
+does not return ``false``.
+
+.. tip::
+
+ As the resolution of functions and filters is done during compilation,
+ there is no overhead when registering these callbacks.
+
+Validating the Template Syntax
+------------------------------
+
+When template code is providing by a third-party (through a web interface for
+instance), it might be interesting to validate the template syntax before
+saving it. If the template code is stored in a `$template` variable, here is
+how you can do it::
+
+ try {
+ $twig->parse($twig->tokenize($template));
+
+ // the $template is valid
+ } catch (Twig_Error_Syntax $e) {
+ // $template contains one or more syntax errors
+ }
+
+If you iterate over a set of files, you can pass the filename to the
+``tokenize()`` method to get the filename in the exception message::
+
+ foreach ($files as $file) {
+ try {
+ $twig->parse($twig->tokenize($template, $file));
+
+ // the $template is valid
+ } catch (Twig_Error_Syntax $e) {
+ // $template contains one or more syntax errors
+ }
+ }
+
+.. note::
+
+ This method won't catch any sandbox policy violations because the policy
+ is enforced during template rendering (as Twig needs the context for some
+ checks like allowed methods on objects).
+
+Refreshing modified Templates when APC is enabled and apc.stat = 0
+------------------------------------------------------------------
+
+When using APC with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing
+the template cache won't update the APC cache. To get around this, one can
+extend ``Twig_Environment`` and force the update of the APC cache when Twig
+rewrites the cache::
+
+ class Twig_Environment_APC extends Twig_Environment
+ {
+ protected function writeCacheFile($file, $content)
+ {
+ parent::writeCacheFile($file, $content);
+
+ // Compile cached file into bytecode cache
+ apc_compile_file($file);
+ }
+ }
+
+Reusing a stateful Node Visitor
+-------------------------------
+
+When attaching a visitor to a ``Twig_Environment`` instance, Twig uses it to
+visit *all* templates it compiles. If you need to keep some state information
+around, you probably want to reset it when visiting a new template.
+
+This can be easily achieved with the following code::
+
+ protected $someTemplateState = array();
+
+ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
+ {
+ if ($node instanceof Twig_Node_Module) {
+ // reset the state as we are entering a new template
+ $this->someTemplateState = array();
+ }
+
+ // ...
+
+ return $node;
+ }
+
+Using the Template name to set the default Escaping Strategy
+------------------------------------------------------------
+
+.. versionadded:: 1.8
+ This recipe requires Twig 1.8 or later.
+
+The ``autoescape`` option determines the default escaping strategy to use when
+no escaping is applied on a variable. When Twig is used to mostly generate
+HTML files, you can set it to ``html`` and explicitly change it to ``js`` when
+you have some dynamic JavaScript files thanks to the ``autoescape`` tag:
+
+.. code-block:: jinja
+
+ {% autoescape 'js' %}
+ ... some JS ...
+ {% endautoescape %}
+
+But if you have many HTML and JS files, and if your template names follow some
+conventions, you can instead determine the default escaping strategy to use
+based on the template name. Let's say that your template names always ends
+with ``.html`` for HTML files, ``.js`` for JavaScript ones, and ``.css`` for
+stylesheets, here is how you can configure Twig::
+
+ class TwigEscapingGuesser
+ {
+ function guess($filename)
+ {
+ // get the format
+ $format = substr($filename, strrpos($filename, '.') + 1);
+
+ switch ($format) {
+ case 'js':
+ return 'js';
+ case 'css':
+ return 'css';
+ case 'html':
+ default:
+ return 'html';
+ }
+ }
+ }
+
+ $loader = new Twig_Loader_Filesystem('/path/to/templates');
+ $twig = new Twig_Environment($loader, array(
+ 'autoescape' => array(new TwigEscapingGuesser(), 'guess'),
+ ));
+
+This dynamic strategy does not incur any overhead at runtime as auto-escaping
+is done at compilation time.
+
+Using a Database to store Templates
+-----------------------------------
+
+If you are developing a CMS, templates are usually stored in a database. This
+recipe gives you a simple PDO template loader you can use as a starting point
+for your own.
+
+First, let's create a temporary in-memory SQLite3 database to work with::
+
+ $dbh = new PDO('sqlite::memory:');
+ $dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)');
+ $base = '{% block content %}{% endblock %}';
+ $index = '
+ {% extends "base.twig" %}
+ {% block content %}Hello {{ name }}{% endblock %}
+ ';
+ $now = time();
+ $dbh->exec("INSERT INTO templates (name, source, last_modified) VALUES ('base.twig', '$base', $now)");
+ $dbh->exec("INSERT INTO templates (name, source, last_modified) VALUES ('index.twig', '$index', $now)");
+
+We have created a simple ``templates`` table that hosts two templates:
+``base.twig`` and ``index.twig``.
+
+Now, let's define a loader able to use this database::
+
+ class DatabaseTwigLoader implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
+ {
+ protected $dbh;
+
+ public function __construct(PDO $dbh)
+ {
+ $this->dbh = $dbh;
+ }
+
+ public function getSource($name)
+ {
+ if (false === $source = $this->getValue('source', $name)) {
+ throw new Twig_Error_Loader(sprintf('Template "%s" does not exist.', $name));
+ }
+
+ return $source;
+ }
+
+ // Twig_ExistsLoaderInterface as of Twig 1.11
+ public function exists($name)
+ {
+ return $name === $this->getValue('name', $name);
+ }
+
+ public function getCacheKey($name)
+ {
+ return $name;
+ }
+
+ public function isFresh($name, $time)
+ {
+ if (false === $lastModified = $this->getValue('last_modified', $name)) {
+ return false;
+ }
+
+ return $lastModified <= $time;
+ }
+
+ protected function getValue($column, $name)
+ {
+ $sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name');
+ $sth->execute(array(':name' => (string) $name));
+
+ return $sth->fetchColumn();
+ }
+ }
+
+Finally, here is an example on how you can use it::
+
+ $loader = new DatabaseTwigLoader($dbh);
+ $twig = new Twig_Environment($loader);
+
+ echo $twig->render('index.twig', array('name' => 'Fabien'));
+
+Using different Template Sources
+--------------------------------
+
+This recipe is the continuation of the previous one. Even if you store the
+contributed templates in a database, you might want to keep the original/base
+templates on the filesystem. When templates can be loaded from different
+sources, you need to use the ``Twig_Loader_Chain`` loader.
+
+As you can see in the previous recipe, we reference the template in the exact
+same way as we would have done it with a regular filesystem loader. This is
+the key to be able to mix and match templates coming from the database, the
+filesystem, or any other loader for that matter: the template name should be a
+logical name, and not the path from the filesystem::
+
+ $loader1 = new DatabaseTwigLoader($dbh);
+ $loader2 = new Twig_Loader_Array(array(
+ 'base.twig' => '{% block content %}{% endblock %}',
+ ));
+ $loader = new Twig_Loader_Chain(array($loader1, $loader2));
+
+ $twig = new Twig_Environment($loader);
+
+ echo $twig->render('index.twig', array('name' => 'Fabien'));
+
+Now that the ``base.twig`` templates is defined in an array loader, you can
+remove it from the database, and everything else will still work as before.
+
+.. _callback: http://www.php.net/manual/en/function.is-callable.php
--- /dev/null
+``autoescape``
+==============
+
+Whether automatic escaping is enabled or not, you can mark a section of a
+template to be escaped or not by using the ``autoescape`` tag:
+
+.. code-block:: jinja
+
+ {# The following syntax works as of Twig 1.8 -- see the note below for previous versions #}
+
+ {% autoescape %}
+ Everything will be automatically escaped in this block
+ using the HTML strategy
+ {% endautoescape %}
+
+ {% autoescape 'html' %}
+ Everything will be automatically escaped in this block
+ using the HTML strategy
+ {% endautoescape %}
+
+ {% autoescape 'js' %}
+ Everything will be automatically escaped in this block
+ using the js escaping strategy
+ {% endautoescape %}
+
+ {% autoescape false %}
+ Everything will be outputted as is in this block
+ {% endautoescape %}
+
+.. note::
+
+ Before Twig 1.8, the syntax was different:
+
+ .. code-block:: jinja
+
+ {% autoescape true %}
+ Everything will be automatically escaped in this block
+ using the HTML strategy
+ {% endautoescape %}
+
+ {% autoescape false %}
+ Everything will be outputted as is in this block
+ {% endautoescape %}
+
+ {% autoescape true js %}
+ Everything will be automatically escaped in this block
+ using the js escaping strategy
+ {% endautoescape %}
+
+When automatic escaping is enabled everything is escaped by default except for
+values explicitly marked as safe. Those can be marked in the template by using
+the :doc:`raw<../filters/raw>` filter:
+
+.. code-block:: jinja
+
+ {% autoescape %}
+ {{ safe_value|raw }}
+ {% endautoescape %}
+
+Functions returning template data (like :doc:`macros<macro>` and
+:doc:`parent<../functions/parent>`) always return safe markup.
+
+.. note::
+
+ Twig is smart enough to not escape an already escaped value by the
+ :doc:`escape<../filters/escape>` filter.
+
+.. note::
+
+ The chapter :doc:`Twig for Developers<../api>` gives more information
+ about when and how automatic escaping is applied.
--- /dev/null
+``block``
+=========
+
+Blocks are used for inheritance and act as placeholders and replacements at
+the same time. They are documented in detail in the documentation for the
+:doc:`extends<../tags/extends>` tag.
+
+Block names should consist of alphanumeric characters, and underscores. Dashes
+are not permitted.
+
+.. seealso:: :doc:`block<../functions/block>`, :doc:`parent<../functions/parent>`, :doc:`use<../tags/use>`, :doc:`extends<../tags/extends>`
--- /dev/null
+``do``
+======
+
+.. versionadded:: 1.5
+ The do tag was added in Twig 1.5.
+
+The ``do`` tag works exactly like the regular variable expression (``{{ ...
+}}``) just that it doesn't print anything:
+
+.. code-block:: jinja
+
+ {% do 1 + 2 %}
--- /dev/null
+``embed``
+=========
+
+.. versionadded:: 1.8
+ The ``embed`` tag was added in Twig 1.8.
+
+The ``embed`` tag combines the behaviour of :doc:`include<include>` and
+:doc:`extends<extends>`.
+It allows you to include another template's contents, just like ``include``
+does. But it also allows you to override any block defined inside the
+included template, like when extending a template.
+
+Think of an embedded template as a "micro layout skeleton".
+
+.. code-block:: jinja
+
+ {% embed "teasers_skeleton.twig" %}
+ {# These blocks are defined in "teasers_skeleton.twig" #}
+ {# and we override them right here: #}
+ {% block left_teaser %}
+ Some content for the left teaser box
+ {% endblock %}
+ {% block right_teaser %}
+ Some content for the right teaser box
+ {% endblock %}
+ {% endembed %}
+
+The ``embed`` tag takes the idea of template inheritance to the level of
+content fragments. While template inheritance allows for "document skeletons",
+which are filled with life by child templates, the ``embed`` tag allows you to
+create "skeletons" for smaller units of content and re-use and fill them
+anywhere you like.
+
+Since the use case may not be obvious, let's look at a simplified example.
+Imagine a base template shared by multiple HTML pages, defining a single block
+named "content":
+
+.. code-block:: text
+
+ ┌─── page layout ─────────────────────┐
+ │ │
+ │ ┌── block "content" ──┐ │
+ │ │ │ │
+ │ │ │ │
+ │ │ (child template to │ │
+ │ │ put content here) │ │
+ │ │ │ │
+ │ │ │ │
+ │ └─────────────────────┘ │
+ │ │
+ └─────────────────────────────────────┘
+
+Some pages ("foo" and "bar") share the same content structure -
+two vertically stacked boxes:
+
+.. code-block:: text
+
+ ┌─── page layout ─────────────────────┐
+ │ │
+ │ ┌── block "content" ──┐ │
+ │ │ ┌─ block "top" ───┐ │ │
+ │ │ │ │ │ │
+ │ │ └─────────────────┘ │ │
+ │ │ ┌─ block "bottom" ┐ │ │
+ │ │ │ │ │ │
+ │ │ └─────────────────┘ │ │
+ │ └─────────────────────┘ │
+ │ │
+ └─────────────────────────────────────┘
+
+While other pages ("boom" and "baz") share a different content structure -
+two boxes side by side:
+
+.. code-block:: text
+
+ ┌─── page layout ─────────────────────┐
+ │ │
+ │ ┌── block "content" ──┐ │
+ │ │ │ │
+ │ │ ┌ block ┐ ┌ block ┐ │ │
+ │ │ │"left" │ │"right"│ │ │
+ │ │ │ │ │ │ │ │
+ │ │ │ │ │ │ │ │
+ │ │ └───────┘ └───────┘ │ │
+ │ └─────────────────────┘ │
+ │ │
+ └─────────────────────────────────────┘
+
+Without the ``embed`` tag, you have two ways to design your templates:
+
+ * Create two "intermediate" base templates that extend the master layout
+ template: one with vertically stacked boxes to be used by the "foo" and
+ "bar" pages and another one with side-by-side boxes for the "boom" and
+ "baz" pages.
+
+ * Embed the markup for the top/bottom and left/right boxes into each page
+ template directly.
+
+These two solutions do not scale well because they each have a major drawback:
+
+ * The first solution may indeed work for this simplified example. But imagine
+ we add a sidebar, which may again contain different, recurring structures
+ of content. Now we would need to create intermediate base templates for
+ all occurring combinations of content structure and sidebar structure...
+ and so on.
+
+ * The second solution involves duplication of common code with all its negative
+ consequences: any change involves finding and editing all affected copies
+ of the structure, correctness has to be verified for each copy, copies may
+ go out of sync by careless modifications etc.
+
+In such a situation, the ``embed`` tag comes in handy. The common layout
+code can live in a single base template, and the two different content structures,
+let's call them "micro layouts" go into separate templates which are embedded
+as necessary:
+
+Page template ``foo.twig``:
+
+.. code-block:: jinja
+
+ {% extends "layout_skeleton.twig" %}
+
+ {% block content %}
+ {% embed "vertical_boxes_skeleton.twig" %}
+ {% block top %}
+ Some content for the top box
+ {% endblock %}
+
+ {% block bottom %}
+ Some content for the bottom box
+ {% endblock %}
+ {% endembed %}
+ {% endblock %}
+
+And here is the code for ``vertical_boxes_skeleton.twig``:
+
+.. code-block:: html+jinja
+
+ <div class="top_box">
+ {% block top %}
+ Top box default content
+ {% endblock %}
+ </div>
+
+ <div class="bottom_box">
+ {% block bottom %}
+ Bottom box default content
+ {% endblock %}
+ </div>
+
+The goal of the ``vertical_boxes_skeleton.twig`` template being to factor
+out the HTML markup for the boxes.
+
+The ``embed`` tag takes the exact same arguments as the ``include`` tag:
+
+.. code-block:: jinja
+
+ {% embed "base" with {'foo': 'bar'} %}
+ ...
+ {% endembed %}
+
+ {% embed "base" with {'foo': 'bar'} only %}
+ ...
+ {% endembed %}
+
+ {% embed "base" ignore missing %}
+ ...
+ {% endembed %}
+
+.. warning::
+
+ As embedded templates do not have "names", auto-escaping strategies based
+ on the template "filename" won't work as expected if you change the
+ context (for instance, if you embed a CSS/JavaScript template into an HTML
+ one). In that case, explicitly set the default auto-escaping strategy with
+ the ``autoescape`` tag.
+
+.. seealso:: :doc:`include<../tags/include>`
--- /dev/null
+``extends``
+===========
+
+The ``extends`` tag can be used to extend a template from another one.
+
+.. note::
+
+ Like PHP, Twig does not support multiple inheritance. So you can only have
+ one extends tag called per rendering. However, Twig supports horizontal
+ :doc:`reuse<use>`.
+
+Let's define a base template, ``base.html``, which defines a simple HTML
+skeleton document:
+
+.. code-block:: html+jinja
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ {% block head %}
+ <link rel="stylesheet" href="style.css" />
+ <title>{% block title %}{% endblock %} - My Webpage</title>
+ {% endblock %}
+ </head>
+ <body>
+ <div id="content">{% block content %}{% endblock %}</div>
+ <div id="footer">
+ {% block footer %}
+ © Copyright 2011 by <a href="http://domain.invalid/">you</a>.
+ {% endblock %}
+ </div>
+ </body>
+ </html>
+
+In this example, the :doc:`block<block>` tags define four blocks that child
+templates can fill in.
+
+All the ``block`` tag does is to tell the template engine that a child
+template may override those portions of the template.
+
+Child Template
+--------------
+
+A child template might look like this:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% block title %}Index{% endblock %}
+ {% block head %}
+ {{ parent() }}
+ <style type="text/css">
+ .important { color: #336699; }
+ </style>
+ {% endblock %}
+ {% block content %}
+ <h1>Index</h1>
+ <p class="important">
+ Welcome on my awesome homepage.
+ </p>
+ {% endblock %}
+
+The ``extends`` tag is the key here. It tells the template engine that this
+template "extends" another template. When the template system evaluates this
+template, first it locates the parent. The extends tag should be the first tag
+in the template.
+
+Note that since the child template doesn't define the ``footer`` block, the
+value from the parent template is used instead.
+
+You can't define multiple ``block`` tags with the same name in the same
+template. This limitation exists because a block tag works in "both"
+directions. That is, a block tag doesn't just provide a hole to fill - it also
+defines the content that fills the hole in the *parent*. If there were two
+similarly-named ``block`` tags in a template, that template's parent wouldn't
+know which one of the blocks' content to use.
+
+If you want to print a block multiple times you can however use the
+``block`` function:
+
+.. code-block:: jinja
+
+ <title>{% block title %}{% endblock %}</title>
+ <h1>{{ block('title') }}</h1>
+ {% block body %}{% endblock %}
+
+Parent Blocks
+-------------
+
+It's possible to render the contents of the parent block by using the
+:doc:`parent<../functions/parent>` function. This gives back the results of
+the parent block:
+
+.. code-block:: jinja
+
+ {% block sidebar %}
+ <h3>Table Of Contents</h3>
+ ...
+ {{ parent() }}
+ {% endblock %}
+
+Named Block End-Tags
+--------------------
+
+Twig allows you to put the name of the block after the end tag for better
+readability:
+
+.. code-block:: jinja
+
+ {% block sidebar %}
+ {% block inner_sidebar %}
+ ...
+ {% endblock inner_sidebar %}
+ {% endblock sidebar %}
+
+Of course, the name after the ``endblock`` word must match the block name.
+
+Block Nesting and Scope
+-----------------------
+
+Blocks can be nested for more complex layouts. Per default, blocks have access
+to variables from outer scopes:
+
+.. code-block:: jinja
+
+ {% for item in seq %}
+ <li>{% block loop_item %}{{ item }}{% endblock %}</li>
+ {% endfor %}
+
+Block Shortcuts
+---------------
+
+For blocks with few content, it's possible to use a shortcut syntax. The
+following constructs do the same:
+
+.. code-block:: jinja
+
+ {% block title %}
+ {{ page_title|title }}
+ {% endblock %}
+
+.. code-block:: jinja
+
+ {% block title page_title|title %}
+
+Dynamic Inheritance
+-------------------
+
+Twig supports dynamic inheritance by using a variable as the base template:
+
+.. code-block:: jinja
+
+ {% extends some_var %}
+
+If the variable evaluates to a ``Twig_Template`` object, Twig will use it as
+the parent template::
+
+ // {% extends layout %}
+
+ $layout = $twig->loadTemplate('some_layout_template.twig');
+
+ $twig->display('template.twig', array('layout' => $layout));
+
+.. versionadded:: 1.2
+ The possibility to pass an array of templates has been added in Twig 1.2.
+
+You can also provide a list of templates that are checked for existence. The
+first template that exists will be used as a parent:
+
+.. code-block:: jinja
+
+ {% extends ['layout.html', 'base_layout.html'] %}
+
+Conditional Inheritance
+-----------------------
+
+As the template name for the parent can be any valid Twig expression, it's
+possible to make the inheritance mechanism conditional:
+
+.. code-block:: jinja
+
+ {% extends standalone ? "minimum.html" : "base.html" %}
+
+In this example, the template will extend the "minimum.html" layout template
+if the ``standalone`` variable evaluates to ``true``, and "base.html"
+otherwise.
+
+How blocks work?
+----------------
+
+A block provides a way to change how a certain part of a template is rendered
+but it does not interfere in any way with the logic around it.
+
+Let's take the following example to illustrate how a block work and more
+importantly, how it does not work:
+
+.. code-block:: jinja
+
+ {# base.twig #}
+
+ {% for post in posts %}
+ {% block post %}
+ <h1>{{ post.title }}</h1>
+ <p>{{ post.body }}</p>
+ {% endblock %}
+ {% endfor %}
+
+If you render this template, the result would be exactly the same with or
+without the ``block`` tag. The ``block`` inside the ``for`` loop is just a way
+to make it overridable by a child template:
+
+.. code-block:: jinja
+
+ {# child.twig #}
+
+ {% extends "base.twig" %}
+
+ {% block post %}
+ <article>
+ <header>{{ post.title }}</header>
+ <section>{{ post.text }}</section>
+ </article>
+ {% endblock %}
+
+Now, when rendering the child template, the loop is going to use the block
+defined in the child template instead of the one defined in the base one; the
+executed template is then equivalent to the following one:
+
+.. code-block:: jinja
+
+ {% for post in posts %}
+ <article>
+ <header>{{ post.title }}</header>
+ <section>{{ post.text }}</section>
+ </article>
+ {% endfor %}
+
+Let's take another example: a block included within an ``if`` statement:
+
+.. code-block:: jinja
+
+ {% if posts is empty %}
+ {% block head %}
+ {{ parent() }}
+
+ <meta name="robots" content="noindex, follow">
+ {% endblock head %}
+ {% endif %}
+
+Contrary to what you might think, this template does not define a block
+conditionally; it just makes overridable by a child template the output of
+what will be rendered when the condition is ``true``.
+
+If you want the output to be displayed conditionally, use the following
+instead:
+
+.. code-block:: jinja
+
+ {% block head %}
+ {{ parent() }}
+
+ {% if posts is empty %}
+ <meta name="robots" content="noindex, follow">
+ {% endif %}
+ {% endblock head %}
+
+.. seealso:: :doc:`block<../functions/block>`, :doc:`block<../tags/block>`, :doc:`parent<../functions/parent>`, :doc:`use<../tags/use>`
--- /dev/null
+``filter``
+==========
+
+Filter sections allow you to apply regular Twig filters on a block of template
+data. Just wrap the code in the special ``filter`` section:
+
+.. code-block:: jinja
+
+ {% filter upper %}
+ This text becomes uppercase
+ {% endfilter %}
+
+You can also chain filters:
+
+.. code-block:: jinja
+
+ {% filter lower|escape %}
+ <strong>SOME TEXT</strong>
+ {% endfilter %}
+
+ {# outputs "<strong>some text</strong>" #}
--- /dev/null
+``flush``
+=========
+
+.. versionadded:: 1.5
+ The flush tag was added in Twig 1.5.
+
+The ``flush`` tag tells Twig to flush the output buffer:
+
+.. code-block:: jinja
+
+ {% flush %}
+
+.. note::
+
+ Internally, Twig uses the PHP `flush`_ function.
+
+.. _`flush`: http://php.net/flush
--- /dev/null
+``for``
+=======
+
+Loop over each item in a sequence. For example, to display a list of users
+provided in a variable called ``users``:
+
+.. code-block:: jinja
+
+ <h1>Members</h1>
+ <ul>
+ {% for user in users %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+
+.. note::
+
+ A sequence can be either an array or an object implementing the
+ ``Traversable`` interface.
+
+If you do need to iterate over a sequence of numbers, you can use the ``..``
+operator:
+
+.. code-block:: jinja
+
+ {% for i in 0..10 %}
+ * {{ i }}
+ {% endfor %}
+
+The above snippet of code would print all numbers from 0 to 10.
+
+It can be also useful with letters:
+
+.. code-block:: jinja
+
+ {% for letter in 'a'..'z' %}
+ * {{ letter }}
+ {% endfor %}
+
+The ``..`` operator can take any expression at both sides:
+
+.. code-block:: jinja
+
+ {% for letter in 'a'|upper..'z'|upper %}
+ * {{ letter }}
+ {% endfor %}
+
+.. tip:
+
+ If you need a step different from 1, you can use the ``range`` function
+ instead.
+
+The `loop` variable
+-------------------
+
+Inside of a ``for`` loop block you can access some special variables:
+
+===================== =============================================================
+Variable Description
+===================== =============================================================
+``loop.index`` The current iteration of the loop. (1 indexed)
+``loop.index0`` The current iteration of the loop. (0 indexed)
+``loop.revindex`` The number of iterations from the end of the loop (1 indexed)
+``loop.revindex0`` The number of iterations from the end of the loop (0 indexed)
+``loop.first`` True if first iteration
+``loop.last`` True if last iteration
+``loop.length`` The number of items in the sequence
+``loop.parent`` The parent context
+===================== =============================================================
+
+.. code-block:: jinja
+
+ {% for user in users %}
+ {{ loop.index }} - {{ user.username }}
+ {% endfor %}
+
+.. note::
+
+ The ``loop.length``, ``loop.revindex``, ``loop.revindex0``, and
+ ``loop.last`` variables are only available for PHP arrays, or objects that
+ implement the ``Countable`` interface. They are also not available when
+ looping with a condition.
+
+.. versionadded:: 1.2
+ The ``if`` modifier support has been added in Twig 1.2.
+
+Adding a condition
+------------------
+
+Unlike in PHP, it's not possible to ``break`` or ``continue`` in a loop. You
+can however filter the sequence during iteration which allows you to skip
+items. The following example skips all the users which are not active:
+
+.. code-block:: jinja
+
+ <ul>
+ {% for user in users if user.active %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+
+The advantage is that the special loop variable will count correctly thus not
+counting the users not iterated over. Keep in mind that properties like
+``loop.last`` will not be defined when using loop conditions.
+
+.. note::
+
+ Using the ``loop`` variable within the condition is not recommended as it
+ will probably not be doing what you expect it to. For instance, adding a
+ condition like ``loop.index > 4`` won't work as the index is only
+ incremented when the condition is true (so the condition will never
+ match).
+
+The `else` Clause
+-----------------
+
+If no iteration took place because the sequence was empty, you can render a
+replacement block by using ``else``:
+
+.. code-block:: jinja
+
+ <ul>
+ {% for user in users %}
+ <li>{{ user.username|e }}</li>
+ {% else %}
+ <li><em>no user found</em></li>
+ {% endfor %}
+ </ul>
+
+Iterating over Keys
+-------------------
+
+By default, a loop iterates over the values of the sequence. You can iterate
+on keys by using the ``keys`` filter:
+
+.. code-block:: jinja
+
+ <h1>Members</h1>
+ <ul>
+ {% for key in users|keys %}
+ <li>{{ key }}</li>
+ {% endfor %}
+ </ul>
+
+Iterating over Keys and Values
+------------------------------
+
+You can also access both keys and values:
+
+.. code-block:: jinja
+
+ <h1>Members</h1>
+ <ul>
+ {% for key, user in users %}
+ <li>{{ key }}: {{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+
+Iterating over a Subset
+-----------------------
+
+You might want to iterate over a subset of values. This can be achieved using
+the :doc:`slice <../filters/slice>` filter:
+
+.. code-block:: jinja
+
+ <h1>Top Ten Members</h1>
+ <ul>
+ {% for user in users|slice(0, 10) %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
--- /dev/null
+``from``
+========
+
+The ``from`` tags import :doc:`macro<../tags/macro>` names into the current
+namespace. The tag is documented in detail in the documentation for the
+:doc:`import<../tags/import>` tag.
+
+.. seealso:: :doc:`macro<../tags/macro>`, :doc:`import<../tags/import>`
--- /dev/null
+``if``
+======
+
+The ``if`` statement in Twig is comparable with the if statements of PHP.
+
+In the simplest form you can use it to test if an expression evaluates to
+``true``:
+
+.. code-block:: jinja
+
+ {% if online == false %}
+ <p>Our website is in maintenance mode. Please, come back later.</p>
+ {% endif %}
+
+You can also test if an array is not empty:
+
+.. code-block:: jinja
+
+ {% if users %}
+ <ul>
+ {% for user in users %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+.. note::
+
+ If you want to test if the variable is defined, use ``if users is
+ defined`` instead.
+
+For multiple branches ``elseif`` and ``else`` can be used like in PHP. You can use
+more complex ``expressions`` there too:
+
+.. code-block:: jinja
+
+ {% if kenny.sick %}
+ Kenny is sick.
+ {% elseif kenny.dead %}
+ You killed Kenny! You bastard!!!
+ {% else %}
+ Kenny looks okay --- so far
+ {% endif %}
--- /dev/null
+``import``
+==========
+
+Twig supports putting often used code into :doc:`macros<../tags/macro>`. These
+macros can go into different templates and get imported from there.
+
+There are two ways to import templates. You can import the complete template
+into a variable or request specific macros from it.
+
+Imagine we have a helper module that renders forms (called ``forms.html``):
+
+.. code-block:: jinja
+
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+ {% macro textarea(name, value, rows) %}
+ <textarea name="{{ name }}" rows="{{ rows|default(10) }}" cols="{{ cols|default(40) }}">{{ value|e }}</textarea>
+ {% endmacro %}
+
+The easiest and most flexible is importing the whole module into a variable.
+That way you can access the attributes:
+
+.. code-block:: jinja
+
+ {% import 'forms.html' as forms %}
+
+ <dl>
+ <dt>Username</dt>
+ <dd>{{ forms.input('username') }}</dd>
+ <dt>Password</dt>
+ <dd>{{ forms.input('password', null, 'password') }}</dd>
+ </dl>
+ <p>{{ forms.textarea('comment') }}</p>
+
+Alternatively you can import names from the template into the current
+namespace:
+
+.. code-block:: jinja
+
+ {% from 'forms.html' import input as input_field, textarea %}
+
+ <dl>
+ <dt>Username</dt>
+ <dd>{{ input_field('username') }}</dd>
+ <dt>Password</dt>
+ <dd>{{ input_field('password', '', 'password') }}</dd>
+ </dl>
+ <p>{{ textarea('comment') }}</p>
+
+.. tip::
+
+ To import macros from the current file, use the special ``_self`` variable
+ for the source.
+
+.. seealso:: :doc:`macro<../tags/macro>`, :doc:`from<../tags/from>`
--- /dev/null
+``include``
+===========
+
+The ``include`` statement includes a template and return the rendered content
+of that file into the current namespace:
+
+.. code-block:: jinja
+
+ {% include 'header.html' %}
+ Body
+ {% include 'footer.html' %}
+
+Included templates have access to the variables of the active context.
+
+If you are using the filesystem loader, the templates are looked for in the
+paths defined by it.
+
+You can add additional variables by passing them after the ``with`` keyword:
+
+.. code-block:: jinja
+
+ {# template.html will have access to the variables from the current context and the additional ones provided #}
+ {% include 'template.html' with {'foo': 'bar'} %}
+
+ {% set vars = {'foo': 'bar'} %}
+ {% include 'template.html' with vars %}
+
+You can disable access to the context by appending the ``only`` keyword:
+
+.. code-block:: jinja
+
+ {# only the foo variable will be accessible #}
+ {% include 'template.html' with {'foo': 'bar'} only %}
+
+.. code-block:: jinja
+
+ {# no variables will be accessible #}
+ {% include 'template.html' only %}
+
+.. tip::
+
+ When including a template created by an end user, you should consider
+ sandboxing it. More information in the :doc:`Twig for Developers<../api>`
+ chapter and in the :doc:`sandbox<../tags/sandbox>` tag documentation.
+
+The template name can be any valid Twig expression:
+
+.. code-block:: jinja
+
+ {% include some_var %}
+ {% include ajax ? 'ajax.html' : 'not_ajax.html' %}
+
+And if the expression evaluates to a ``Twig_Template`` object, Twig will use it
+directly::
+
+ // {% include template %}
+
+ $template = $twig->loadTemplate('some_template.twig');
+
+ $twig->loadTemplate('template.twig')->display(array('template' => $template));
+
+.. versionadded:: 1.2
+ The ``ignore missing`` feature has been added in Twig 1.2.
+
+You can mark an include with ``ignore missing`` in which case Twig will ignore
+the statement if the template to be included does not exist. It has to be
+placed just after the template name. Here some valid examples:
+
+.. code-block:: jinja
+
+ {% include 'sidebar.html' ignore missing %}
+ {% include 'sidebar.html' ignore missing with {'foo': 'bar'} %}
+ {% include 'sidebar.html' ignore missing only %}
+
+.. versionadded:: 1.2
+ The possibility to pass an array of templates has been added in Twig 1.2.
+
+You can also provide a list of templates that are checked for existence before
+inclusion. The first template that exists will be included:
+
+.. code-block:: jinja
+
+ {% include ['page_detailed.html', 'page.html'] %}
+
+If ``ignore missing`` is given, it will fall back to rendering nothing if none
+of the templates exist, otherwise it will throw an exception.
--- /dev/null
+Tags
+====
+
+.. toctree::
+ :maxdepth: 1
+
+ autoescape
+ block
+ filter
+ do
+ embed
+ extends
+ flush
+ for
+ from
+ if
+ import
+ include
+ macro
+ sandbox
+ set
+ spaceless
+ use
+ verbatim
--- /dev/null
+``macro``
+=========
+
+Macros are comparable with functions in regular programming languages. They
+are useful to put often used HTML idioms into reusable elements to not repeat
+yourself.
+
+Here is a small example of a macro that renders a form element:
+
+.. code-block:: jinja
+
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+Macros differs from native PHP functions in a few ways:
+
+* Default argument values are defined by using the ``default`` filter in the
+ macro body;
+
+* Arguments of a macro are always optional.
+
+But as with PHP functions, macros don't have access to the current template
+variables.
+
+.. tip::
+
+ You can pass the whole context as an argument by using the special
+ ``_context`` variable.
+
+Macros can be defined in any template, and need to be "imported" before being
+used (see the documentation for the :doc:`import<../tags/import>` tag for more
+information):
+
+.. code-block:: jinja
+
+ {% import "forms.html" as forms %}
+
+The above ``import`` call imports the "forms.html" file (which can contain only
+macros, or a template and some macros), and import the functions as items of
+the ``forms`` variable.
+
+The macro can then be called at will:
+
+.. code-block:: jinja
+
+ <p>{{ forms.input('username') }}</p>
+ <p>{{ forms.input('password', null, 'password') }}</p>
+
+If macros are defined and used in the same template, you can use the
+special ``_self`` variable to import them:
+
+.. code-block:: jinja
+
+ {% import _self as forms %}
+
+ <p>{{ forms.input('username') }}</p>
+
+.. warning::
+
+ When you define a macro in the template where you are going to use it, you
+ might be tempted to call the macro directly via ``_self.input()`` instead
+ of importing it; even if seems to work, this is just a side-effect of the
+ current implementation and it won't work anymore in Twig 2.x.
+
+When you want to use a macro in another macro from the same file, you need to
+import it locally:
+
+.. code-block:: jinja
+
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+ {% macro wrapped_input(name, value, type, size) %}
+ {% import _self as forms %}
+
+ <div class="field">
+ {{ forms.input(name, value, type, size) }}
+ </div>
+ {% endmacro %}
+
+.. seealso:: :doc:`from<../tags/from>`, :doc:`import<../tags/import>`
--- /dev/null
+``sandbox``
+===========
+
+The ``sandbox`` tag can be used to enable the sandboxing mode for an included
+template, when sandboxing is not enabled globally for the Twig environment:
+
+.. code-block:: jinja
+
+ {% sandbox %}
+ {% include 'user.html' %}
+ {% endsandbox %}
+
+.. warning::
+
+ The ``sandbox`` tag is only available when the sandbox extension is
+ enabled (see the :doc:`Twig for Developers<../api>` chapter).
+
+.. note::
+
+ The ``sandbox`` tag can only be used to sandbox an include tag and it
+ cannot be used to sandbox a section of a template. The following example
+ won't work:
+
+ .. code-block:: jinja
+
+ {% sandbox %}
+ {% for i in 1..2 %}
+ {{ i }}
+ {% endfor %}
+ {% endsandbox %}
--- /dev/null
+``set``
+=======
+
+Inside code blocks you can also assign values to variables. Assignments use
+the ``set`` tag and can have multiple targets.
+
+Here is how you can assign the ``bar`` value to the ``foo`` variable:
+
+.. code-block:: jinja
+
+ {% set foo = 'bar' %}
+
+After the ``set`` call, the ``foo`` variable is available in the template like
+any other ones:
+
+.. code-block:: jinja
+
+ {# displays bar #}
+ {{ foo }}
+
+The assigned value can be any valid :ref:`Twig expressions
+<twig-expressions>`:
+
+.. code-block:: jinja
+
+ {% set foo = [1, 2] %}
+ {% set foo = {'foo': 'bar'} %}
+ {% set foo = 'foo' ~ 'bar' %}
+
+Several variables can be assigned in one block:
+
+.. code-block:: jinja
+
+ {% set foo, bar = 'foo', 'bar' %}
+
+ {# is equivalent to #}
+
+ {% set foo = 'foo' %}
+ {% set bar = 'bar' %}
+
+The ``set`` tag can also be used to 'capture' chunks of text:
+
+.. code-block:: jinja
+
+ {% set foo %}
+ <div id="pagination">
+ ...
+ </div>
+ {% endset %}
+
+.. caution::
+
+ If you enable automatic output escaping, Twig will only consider the
+ content to be safe when capturing chunks of text.
+
+.. note::
+
+ Note that loops are scoped in Twig; therefore a variable declared inside a
+ ``for`` loop is not accessible outside the loop itself:
+
+ .. code-block:: jinja
+
+ {% for item in list %}
+ {% set foo = item %}
+ {% endfor %}
+
+ {# foo is NOT available #}
+
+ If you want to access the variable, just declare it before the loop:
+
+ .. code-block:: jinja
+
+ {% set foo = "" %}
+ {% for item in list %}
+ {% set foo = item %}
+ {% endfor %}
+
+ {# foo is available #}
--- /dev/null
+``spaceless``
+=============
+
+Use the ``spaceless`` tag to remove whitespace *between HTML tags*, not
+whitespace within HTML tags or whitespace in plain text:
+
+.. code-block:: jinja
+
+ {% spaceless %}
+ <div>
+ <strong>foo</strong>
+ </div>
+ {% endspaceless %}
+
+ {# output will be <div><strong>foo</strong></div> #}
+
+This tag is not meant to "optimize" the size of the generated HTML content but
+merely to avoid extra whitespace between HTML tags to avoid browser rendering
+quirks under some circumstances.
+
+.. tip::
+
+ If you want to optimize the size of the generated HTML content, gzip
+ compress the output instead.
+
+.. tip::
+
+ If you want to create a tag that actually removes all extra whitespace in
+ an HTML string, be warned that this is not as easy as it seems to be
+ (think of ``textarea`` or ``pre`` tags for instance). Using a third-party
+ library like Tidy is probably a better idea.
+
+.. tip::
+
+ For more information on whitespace control, read the
+ :doc:`dedicated<../templates>` section of the documentation and learn how
+ you can also use the whitespace control modifier on your tags.
--- /dev/null
+``use``
+=======
+
+.. versionadded:: 1.1
+ Horizontal reuse was added in Twig 1.1.
+
+.. note::
+
+ Horizontal reuse is an advanced Twig feature that is hardly ever needed in
+ regular templates. It is mainly used by projects that need to make
+ template blocks reusable without using inheritance.
+
+Template inheritance is one of the most powerful Twig's feature but it is
+limited to single inheritance; a template can only extend one other template.
+This limitation makes template inheritance simple to understand and easy to
+debug:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% block title %}{% endblock %}
+ {% block content %}{% endblock %}
+
+Horizontal reuse is a way to achieve the same goal as multiple inheritance,
+but without the associated complexity:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% use "blocks.html" %}
+
+ {% block title %}{% endblock %}
+ {% block content %}{% endblock %}
+
+The ``use`` statement tells Twig to import the blocks defined in
+```blocks.html`` into the current template (it's like macros, but for blocks):
+
+.. code-block:: jinja
+
+ # blocks.html
+ {% block sidebar %}{% endblock %}
+
+In this example, the ``use`` statement imports the ``sidebar`` block into the
+main template. The code is mostly equivalent to the following one (the
+imported blocks are not outputted automatically):
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% block sidebar %}{% endblock %}
+ {% block title %}{% endblock %}
+ {% block content %}{% endblock %}
+
+.. note::
+
+ The ``use`` tag only imports a template if it does not extend another
+ template, if it does not define macros, and if the body is empty. But it
+ can *use* other templates.
+
+.. note::
+
+ Because ``use`` statements are resolved independently of the context
+ passed to the template, the template reference cannot be an expression.
+
+The main template can also override any imported block. If the template
+already defines the ``sidebar`` block, then the one defined in ``blocks.html``
+is ignored. To avoid name conflicts, you can rename imported blocks:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% use "blocks.html" with sidebar as base_sidebar %}
+
+ {% block sidebar %}{% endblock %}
+ {% block title %}{% endblock %}
+ {% block content %}{% endblock %}
+
+.. versionadded:: 1.3
+ The ``parent()`` support was added in Twig 1.3.
+
+The ``parent()`` function automatically determines the correct inheritance
+tree, so it can be used when overriding a block defined in an imported
+template:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% use "blocks.html" %}
+
+ {% block sidebar %}
+ {{ parent() }}
+ {% endblock %}
+
+ {% block title %}{% endblock %}
+ {% block content %}{% endblock %}
+
+In this example, ``parent()`` will correctly call the ``sidebar`` block from
+the ``blocks.html`` template.
+
+.. tip::
+
+ In Twig 1.2, renaming allows you to simulate inheritance by calling the
+ "parent" block:
+
+ .. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% use "blocks.html" with sidebar as parent_sidebar %}
+
+ {% block sidebar %}
+ {{ block('parent_sidebar') }}
+ {% endblock %}
+
+.. note::
+
+ You can use as many ``use`` statements as you want in any given template.
+ If two imported templates define the same block, the latest one wins.
--- /dev/null
+``verbatim``
+============
+
+.. versionadded:: 1.12
+ The ``verbatim`` tag was added in Twig 1.12 (it was named ``raw`` before).
+
+The ``verbatim`` tag marks sections as being raw text that should not be
+parsed. For example to put Twig syntax as example into a template you can use
+this snippet:
+
+.. code-block:: jinja
+
+ {% verbatim %}
+ <ul>
+ {% for item in seq %}
+ <li>{{ item }}</li>
+ {% endfor %}
+ </ul>
+ {% endverbatim %}
+
+.. note::
+
+ The ``verbatim`` tag works in the exact same way as the old ``raw`` tag,
+ but was renamed to avoid confusion with the ``raw`` filter.
\ No newline at end of file
--- /dev/null
+Twig for Template Designers
+===========================
+
+This document describes the syntax and semantics of the template engine and
+will be most useful as reference to those creating Twig templates.
+
+Synopsis
+--------
+
+A template is simply a text file. It can generate any text-based format (HTML,
+XML, CSV, LaTeX, etc.). It doesn't have a specific extension, ``.html`` or
+``.xml`` are just fine.
+
+A template contains **variables** or **expressions**, which get replaced with
+values when the template is evaluated, and **tags**, which control the logic
+of the template.
+
+Below is a minimal template that illustrates a few basics. We will cover the
+details later on:
+
+.. code-block:: html+jinja
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>My Webpage</title>
+ </head>
+ <body>
+ <ul id="navigation">
+ {% for item in navigation %}
+ <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
+ {% endfor %}
+ </ul>
+
+ <h1>My Webpage</h1>
+ {{ a_variable }}
+ </body>
+ </html>
+
+There are two kinds of delimiters: ``{% ... %}`` and ``{{ ... }}``. The first
+one is used to execute statements such as for-loops, the latter prints the
+result of an expression to the template.
+
+IDEs Integration
+----------------
+
+Many IDEs support syntax highlighting and auto-completion for Twig:
+
+* *Textmate* via the `Twig bundle`_
+* *Vim* via the `Jinja syntax plugin`_
+* *Netbeans* via the `Twig syntax plugin`_ (until 7.1, native as of 7.2)
+* *PhpStorm* (native as of 2.1)
+* *Eclipse* via the `Twig plugin`_
+* *Sublime Text* via the `Twig bundle`_
+* *GtkSourceView* via the `Twig language definition`_ (used by gedit and other projects)
+* *Coda* and *SubEthaEdit* via the `Twig syntax mode`_
+* *Coda 2* via the `other Twig syntax mode`_
+* *Komodo* and *Komodo Edit* via the Twig highlight/syntax check mode
+* *Notepad++* via the `Notepad++ Twig Highlighter`_
+* *Emacs* via `web-mode.el`_
+
+Variables
+---------
+
+The application passes variables to the templates you can mess around in the
+template. Variables may have attributes or elements on them you can access
+too. How a variable looks like heavily depends on the application providing
+those.
+
+You can use a dot (``.``) to access attributes of a variable (methods or
+properties of a PHP object, or items of a PHP array), or the so-called
+"subscript" syntax (``[]``):
+
+.. code-block:: jinja
+
+ {{ foo.bar }}
+ {{ foo['bar'] }}
+
+When the attribute contains special characters (like ``-`` that would be
+interpreted as the minus operator), use the ``attribute`` function instead to
+access the variable attribute:
+
+.. code-block:: jinja
+
+ {# equivalent to the non-working foo.data-foo #}
+ {{ attribute(foo, 'data-foo') }}
+
+.. note::
+
+ It's important to know that the curly braces are *not* part of the
+ variable but the print statement. If you access variables inside tags
+ don't put the braces around.
+
+If a variable or attribute does not exist, you will get back a ``null`` value
+when the ``strict_variables`` option is set to ``false``, otherwise Twig will
+throw an error (see :ref:`environment options<environment_options>`).
+
+.. sidebar:: Implementation
+
+ For convenience sake ``foo.bar`` does the following things on the PHP
+ layer:
+
+ * check if ``foo`` is an array and ``bar`` a valid element;
+ * if not, and if ``foo`` is an object, check that ``bar`` is a valid property;
+ * if not, and if ``foo`` is an object, check that ``bar`` is a valid method
+ (even if ``bar`` is the constructor - use ``__construct()`` instead);
+ * if not, and if ``foo`` is an object, check that ``getBar`` is a valid method;
+ * if not, and if ``foo`` is an object, check that ``isBar`` is a valid method;
+ * if not, return a ``null`` value.
+
+ ``foo['bar']`` on the other hand only works with PHP arrays:
+
+ * check if ``foo`` is an array and ``bar`` a valid element;
+ * if not, return a ``null`` value.
+
+.. note::
+
+ If you want to get a dynamic attribute on a variable, use the
+ :doc:`attribute<functions/attribute>` function instead.
+
+Global Variables
+~~~~~~~~~~~~~~~~
+
+The following variables are always available in templates:
+
+* ``_self``: references the current template;
+* ``_context``: references the current context;
+* ``_charset``: references the current charset.
+
+Setting Variables
+~~~~~~~~~~~~~~~~~
+
+You can assign values to variables inside code blocks. Assignments use the
+:doc:`set<tags/set>` tag:
+
+.. code-block:: jinja
+
+ {% set foo = 'foo' %}
+ {% set foo = [1, 2] %}
+ {% set foo = {'foo': 'bar'} %}
+
+Filters
+-------
+
+Variables can be modified by **filters**. Filters are separated from the
+variable by a pipe symbol (``|``) and may have optional arguments in
+parentheses. Multiple filters can be chained. The output of one filter is
+applied to the next.
+
+The following example removes all HTML tags from the ``name`` and title-cases
+it:
+
+.. code-block:: jinja
+
+ {{ name|striptags|title }}
+
+Filters that accept arguments have parentheses around the arguments. This
+example will join a list by commas:
+
+.. code-block:: jinja
+
+ {{ list|join(', ') }}
+
+To apply a filter on a section of code, wrap it with the
+:doc:`filter<tags/filter>` tag:
+
+.. code-block:: jinja
+
+ {% filter upper %}
+ This text becomes uppercase
+ {% endfilter %}
+
+Go to the :doc:`filters<filters/index>` page to learn more about the built-in
+filters.
+
+Functions
+---------
+
+Functions can be called to generate content. Functions are called by their
+name followed by parentheses (``()``) and may have arguments.
+
+For instance, the ``range`` function returns a list containing an arithmetic
+progression of integers:
+
+.. code-block:: jinja
+
+ {% for i in range(0, 3) %}
+ {{ i }},
+ {% endfor %}
+
+Go to the :doc:`functions<functions/index>` page to learn more about the
+built-in functions.
+
+Named Arguments
+---------------
+
+.. versionadded:: 1.12
+ Support for named arguments was added in Twig 1.12.
+
+Arguments for filters and functions can also be passed as *named arguments*:
+
+.. code-block:: jinja
+
+ {% for i in range(low=1, high=10, step=2) %}
+ {{ i }},
+ {% endfor %}
+
+Using named arguments makes your templates more explicit about the meaning of
+the values you pass as arguments:
+
+.. code-block:: jinja
+
+ {{ data|convert_encoding('UTF-8', 'iso-2022-jp') }}
+
+ {# versus #}
+
+ {{ data|convert_encoding(from='iso-2022-jp', to='UTF-8') }}
+
+Named arguments also allow you to skip some arguments for which you don't want
+to change the default value:
+
+.. code-block:: jinja
+
+ {# the first argument is the date format, which defaults to the global date format if null is passed #}
+ {{ "now"|date(null, "Europe/Paris") }}
+
+ {# or skip the format value by using a named argument for the timezone #}
+ {{ "now"|date(timezone="Europe/Paris") }}
+
+You can also use both positional and named arguments in one call, in which
+case positional arguments must always come before named arguments:
+
+.. code-block:: jinja
+
+ {{ "now"|date('d/m/Y H:i', timezone="Europe/Paris") }}
+
+.. tip::
+
+ Each function and filter documentation page has a section where the names
+ of all arguments are listed when supported.
+
+Control Structure
+-----------------
+
+A control structure refers to all those things that control the flow of a
+program - conditionals (i.e. ``if``/``elseif``/``else``), ``for``-loops, as
+well as things like blocks. Control structures appear inside ``{% ... %}``
+blocks.
+
+For example, to display a list of users provided in a variable called
+``users``, use the :doc:`for<tags/for>` tag:
+
+.. code-block:: jinja
+
+ <h1>Members</h1>
+ <ul>
+ {% for user in users %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+
+The :doc:`if<tags/if>` tag can be used to test an expression:
+
+.. code-block:: jinja
+
+ {% if users|length > 0 %}
+ <ul>
+ {% for user in users %}
+ <li>{{ user.username|e }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+Go to the :doc:`tags<tags/index>` page to learn more about the built-in tags.
+
+Comments
+--------
+
+To comment-out part of a line in a template, use the comment syntax ``{# ...
+#}``. This is useful for debugging or to add information for other template
+designers or yourself:
+
+.. code-block:: jinja
+
+ {# note: disabled template because we no longer use this
+ {% for user in users %}
+ ...
+ {% endfor %}
+ #}
+
+Including other Templates
+-------------------------
+
+The :doc:`include<tags/include>` tag is useful to include a template and
+return the rendered content of that template into the current one:
+
+.. code-block:: jinja
+
+ {% include 'sidebar.html' %}
+
+Per default included templates are passed the current context.
+
+The context that is passed to the included template includes variables defined
+in the template:
+
+.. code-block:: jinja
+
+ {% for box in boxes %}
+ {% include "render_box.html" %}
+ {% endfor %}
+
+The included template ``render_box.html`` is able to access ``box``.
+
+The filename of the template depends on the template loader. For instance, the
+``Twig_Loader_Filesystem`` allows you to access other templates by giving the
+filename. You can access templates in subdirectories with a slash:
+
+.. code-block:: jinja
+
+ {% include "sections/articles/sidebar.html" %}
+
+This behavior depends on the application embedding Twig.
+
+Template Inheritance
+--------------------
+
+The most powerful part of Twig is template inheritance. Template inheritance
+allows you to build a base "skeleton" template that contains all the common
+elements of your site and defines **blocks** that child templates can
+override.
+
+Sounds complicated but is very basic. It's easier to understand it by
+starting with an example.
+
+Let's define a base template, ``base.html``, which defines a simple HTML
+skeleton document that you might use for a simple two-column page:
+
+.. code-block:: html+jinja
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ {% block head %}
+ <link rel="stylesheet" href="style.css" />
+ <title>{% block title %}{% endblock %} - My Webpage</title>
+ {% endblock %}
+ </head>
+ <body>
+ <div id="content">{% block content %}{% endblock %}</div>
+ <div id="footer">
+ {% block footer %}
+ © Copyright 2011 by <a href="http://domain.invalid/">you</a>.
+ {% endblock %}
+ </div>
+ </body>
+ </html>
+
+In this example, the :doc:`block<tags/block>` tags define four blocks that
+child templates can fill in. All the ``block`` tag does is to tell the
+template engine that a child template may override those portions of the
+template.
+
+A child template might look like this:
+
+.. code-block:: jinja
+
+ {% extends "base.html" %}
+
+ {% block title %}Index{% endblock %}
+ {% block head %}
+ {{ parent() }}
+ <style type="text/css">
+ .important { color: #336699; }
+ </style>
+ {% endblock %}
+ {% block content %}
+ <h1>Index</h1>
+ <p class="important">
+ Welcome to my awesome homepage.
+ </p>
+ {% endblock %}
+
+The :doc:`extends<tags/extends>` tag is the key here. It tells the template
+engine that this template "extends" another template. When the template system
+evaluates this template, first it locates the parent. The extends tag should
+be the first tag in the template.
+
+Note that since the child template doesn't define the ``footer`` block, the
+value from the parent template is used instead.
+
+It's possible to render the contents of the parent block by using the
+:doc:`parent<functions/parent>` function. This gives back the results of the
+parent block:
+
+.. code-block:: jinja
+
+ {% block sidebar %}
+ <h3>Table Of Contents</h3>
+ ...
+ {{ parent() }}
+ {% endblock %}
+
+.. tip::
+
+ The documentation page for the :doc:`extends<tags/extends>` tag describes
+ more advanced features like block nesting, scope, dynamic inheritance, and
+ conditional inheritance.
+
+.. note::
+
+ Twig also supports multiple inheritance with the so called horizontal reuse
+ with the help of the :doc:`use<tags/use>` tag. This is an advanced feature
+ hardly ever needed in regular templates.
+
+HTML Escaping
+-------------
+
+When generating HTML from templates, there's always a risk that a variable
+will include characters that affect the resulting HTML. There are two
+approaches: manually escaping each variable or automatically escaping
+everything by default.
+
+Twig supports both, automatic escaping is enabled by default.
+
+.. note::
+
+ Automatic escaping is only supported if the *escaper* extension has been
+ enabled (which is the default).
+
+Working with Manual Escaping
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If manual escaping is enabled, it is **your** responsibility to escape
+variables if needed. What to escape? Any variable you don't trust.
+
+Escaping works by piping the variable through the
+:doc:`escape<filters/escape>` or ``e`` filter:
+
+.. code-block:: jinja
+
+ {{ user.username|e }}
+
+By default, the ``escape`` filter uses the ``html`` strategy, but depending on
+the escaping context, you might want to explicitly use any other available
+strategies:
+
+.. code-block:: jinja
+
+ {{ user.username|e('js') }}
+ {{ user.username|e('css') }}
+ {{ user.username|e('url') }}
+ {{ user.username|e('html_attr') }}
+
+Working with Automatic Escaping
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Whether automatic escaping is enabled or not, you can mark a section of a
+template to be escaped or not by using the :doc:`autoescape<tags/autoescape>`
+tag:
+
+.. code-block:: jinja
+
+ {% autoescape %}
+ Everything will be automatically escaped in this block (using the HTML strategy)
+ {% endautoescape %}
+
+By default, auto-escaping uses the ``html`` escaping strategy. If you output
+variables in other contexts, you need to explicitly escape them with the
+appropriate escaping strategy:
+
+.. code-block:: jinja
+
+ {% autoescape 'js' %}
+ Everything will be automatically escaped in this block (using the JS strategy)
+ {% endautoescape %}
+
+Escaping
+--------
+
+It is sometimes desirable or even necessary to have Twig ignore parts it would
+otherwise handle as variables or blocks. For example if the default syntax is
+used and you want to use ``{{`` as raw string in the template and not start a
+variable you have to use a trick.
+
+The easiest way is to output the variable delimiter (``{{``) by using a variable
+expression:
+
+.. code-block:: jinja
+
+ {{ '{{' }}
+
+For bigger sections it makes sense to mark a block
+:doc:`verbatim<tags/verbatim>`.
+
+Macros
+------
+
+.. versionadded:: 1.12
+ Support for default argument values was added in Twig 1.12.
+
+Macros are comparable with functions in regular programming languages. They
+are useful to reuse often used HTML fragments to not repeat yourself.
+
+A macro is defined via the :doc:`macro<tags/macro>` tag. Here is a small example
+(subsequently called ``forms.html``) of a macro that renders a form element:
+
+.. code-block:: jinja
+
+ {% macro input(name, value, type, size) %}
+ <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
+ {% endmacro %}
+
+Macros can be defined in any template, and need to be "imported" via the
+:doc:`import<tags/import>` tag before being used:
+
+.. code-block:: jinja
+
+ {% import "forms.html" as forms %}
+
+ <p>{{ forms.input('username') }}</p>
+
+Alternatively, you can import individual macro names from a template into the
+current namespace via the :doc:`from<tags/from>` tag and optionally alias them:
+
+.. code-block:: jinja
+
+ {% from 'forms.html' import input as input_field %}
+
+ <dl>
+ <dt>Username</dt>
+ <dd>{{ input_field('username') }}</dd>
+ <dt>Password</dt>
+ <dd>{{ input_field('password', '', 'password') }}</dd>
+ </dl>
+
+A default value can also be defined for macro arguments when not provided in a
+macro call:
+
+.. code-block:: jinja
+
+ {% macro input(name, value = "", type = "text", size = 20) %}
+ <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}" />
+ {% endmacro %}
+
+.. _twig-expressions:
+
+Expressions
+-----------
+
+Twig allows expressions everywhere. These work very similar to regular PHP and
+even if you're not working with PHP you should feel comfortable with it.
+
+.. note::
+
+ The operator precedence is as follows, with the lowest-precedence
+ operators listed first: ``b-and``, ``b-xor``, ``b-or``, ``or``, ``and``,
+ ``==``, ``!=``, ``<``, ``>``, ``>=``, ``<=``, ``in``, ``..``, ``+``,
+ ``-``, ``~``, ``*``, ``/``, ``//``, ``%``, ``is``, ``**``, ``|``, ``[]``,
+ and ``.``:
+
+ .. code-block:: jinja
+
+ {% set greeting = 'Hello' %}
+ {% set name = 'Fabien' %}
+
+ {{ greeting ~ name|lower }} {# Hello fabien #}
+
+ {# use parenthesis to change precedence #}
+ {{ (greeting ~ name)|lower }} {# hello fabien #}
+
+Literals
+~~~~~~~~
+
+.. versionadded:: 1.5
+ Support for hash keys as names and expressions was added in Twig 1.5.
+
+The simplest form of expressions are literals. Literals are representations
+for PHP types such as strings, numbers, and arrays. The following literals
+exist:
+
+* ``"Hello World"``: Everything between two double or single quotes is a
+ string. They are useful whenever you need a string in the template (for
+ example as arguments to function calls, filters or just to extend or include
+ a template). A string can contain a delimiter if it is preceded by a
+ backslash (``\``) -- like in ``'It\'s good'``.
+
+* ``42`` / ``42.23``: Integers and floating point numbers are created by just
+ writing the number down. If a dot is present the number is a float,
+ otherwise an integer.
+
+* ``["foo", "bar"]``: Arrays are defined by a sequence of expressions
+ separated by a comma (``,``) and wrapped with squared brackets (``[]``).
+
+* ``{"foo": "bar"}``: Hashes are defined by a list of keys and values
+ separated by a comma (``,``) and wrapped with curly braces (``{}``):
+
+ .. code-block:: jinja
+
+ {# keys as string #}
+ { 'foo': 'foo', 'bar': 'bar' }
+
+ {# keys as names (equivalent to the previous hash) -- as of Twig 1.5 #}
+ { foo: 'foo', bar: 'bar' }
+
+ {# keys as integer #}
+ { 2: 'foo', 4: 'bar' }
+
+ {# keys as expressions (the expression must be enclosed into parentheses) -- as of Twig 1.5 #}
+ { (1 + 1): 'foo', (a ~ 'b'): 'bar' }
+
+* ``true`` / ``false``: ``true`` represents the true value, ``false``
+ represents the false value.
+
+* ``null``: ``null`` represents no specific value. This is the value returned
+ when a variable does not exist. ``none`` is an alias for ``null``.
+
+Arrays and hashes can be nested:
+
+.. code-block:: jinja
+
+ {% set foo = [1, {"foo": "bar"}] %}
+
+.. tip::
+
+ Using double-quoted or single-quoted strings has no impact on performance
+ but string interpolation is only supported in double-quoted strings.
+
+Math
+~~~~
+
+Twig allows you to calculate with values. This is rarely useful in templates
+but exists for completeness' sake. The following operators are supported:
+
+* ``+``: Adds two objects together (the operands are casted to numbers). ``{{
+ 1 + 1 }}`` is ``2``.
+
+* ``-``: Subtracts the second number from the first one. ``{{ 3 - 2 }}`` is
+ ``1``.
+
+* ``/``: Divides two numbers. The returned value will be a floating point
+ number. ``{{ 1 / 2 }}`` is ``{{ 0.5 }}``.
+
+* ``%``: Calculates the remainder of an integer division. ``{{ 11 % 7 }}`` is
+ ``4``.
+
+* ``//``: Divides two numbers and returns the truncated integer result. ``{{
+ 20 // 7 }}`` is ``2``.
+
+* ``*``: Multiplies the left operand with the right one. ``{{ 2 * 2 }}`` would
+ return ``4``.
+
+* ``**``: Raises the left operand to the power of the right operand. ``{{ 2 **
+ 3 }}`` would return ``8``.
+
+Logic
+~~~~~
+
+You can combine multiple expressions with the following operators:
+
+* ``and``: Returns true if the left and the right operands are both true.
+
+* ``or``: Returns true if the left or the right operand is true.
+
+* ``not``: Negates a statement.
+
+* ``(expr)``: Groups an expression.
+
+.. note::
+
+ Twig also support bitwise operators (``b-and``, ``b-xor``, and ``b-or``).
+
+Comparisons
+~~~~~~~~~~~
+
+The following comparison operators are supported in any expression: ``==``,
+``!=``, ``<``, ``>``, ``>=``, and ``<=``.
+
+Containment Operator
+~~~~~~~~~~~~~~~~~~~~
+
+The ``in`` operator performs containment test.
+
+It returns ``true`` if the left operand is contained in the right:
+
+.. code-block:: jinja
+
+ {# returns true #}
+
+ {{ 1 in [1, 2, 3] }}
+
+ {{ 'cd' in 'abcde' }}
+
+.. tip::
+
+ You can use this filter to perform a containment test on strings, arrays,
+ or objects implementing the ``Traversable`` interface.
+
+To perform a negative test, use the ``not in`` operator:
+
+.. code-block:: jinja
+
+ {% if 1 not in [1, 2, 3] %}
+
+ {# is equivalent to #}
+ {% if not (1 in [1, 2, 3]) %}
+
+Test Operator
+~~~~~~~~~~~~~
+
+The ``is`` operator performs tests. Tests can be used to test a variable against
+a common expression. The right operand is name of the test:
+
+.. code-block:: jinja
+
+ {# find out if a variable is odd #}
+
+ {{ name is odd }}
+
+Tests can accept arguments too:
+
+.. code-block:: jinja
+
+ {% if loop.index is divisibleby(3) %}
+
+Tests can be negated by using the ``is not`` operator:
+
+.. code-block:: jinja
+
+ {% if loop.index is not divisibleby(3) %}
+
+ {# is equivalent to #}
+ {% if not (loop.index is divisibleby(3)) %}
+
+Go to the :doc:`tests<tests/index>` page to learn more about the built-in
+tests.
+
+Other Operators
+~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.12.0
+ Support for the extended ternary operator was added in Twig 1.12.0.
+
+The following operators are very useful but don't fit into any of the other
+categories:
+
+* ``..``: Creates a sequence based on the operand before and after the
+ operator (this is just syntactic sugar for the :doc:`range<functions/range>`
+ function).
+
+* ``|``: Applies a filter.
+
+* ``~``: Converts all operands into strings and concatenates them. ``{{ "Hello
+ " ~ name ~ "!" }}`` would return (assuming ``name`` is ``'John'``) ``Hello
+ John!``.
+
+* ``.``, ``[]``: Gets an attribute of an object.
+
+* ``?:``: The ternary operator:
+
+ .. code-block:: jinja
+
+ {{ foo ? 'yes' : 'no' }}
+
+ {# as of Twig 1.12.0 #}
+ {{ foo ?: 'no' }} == {{ foo ? foo : 'no' }}
+ {{ foo ? 'yes' }} == {{ foo ? 'yes' : '' }}
+
+String Interpolation
+~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.5
+ String interpolation was added in Twig 1.5.
+
+String interpolation (`#{expression}`) allows any valid expression to appear
+within a *double-quoted string*. The result of evaluating that expression is
+inserted into the string:
+
+.. code-block:: jinja
+
+ {{ "foo #{bar} baz" }}
+ {{ "foo #{1 + 2} baz" }}
+
+Whitespace Control
+------------------
+
+.. versionadded:: 1.1
+ Tag level whitespace control was added in Twig 1.1.
+
+The first newline after a template tag is removed automatically (like in PHP.)
+Whitespace is not further modified by the template engine, so each whitespace
+(spaces, tabs, newlines etc.) is returned unchanged.
+
+Use the ``spaceless`` tag to remove whitespace *between HTML tags*:
+
+.. code-block:: jinja
+
+ {% spaceless %}
+ <div>
+ <strong>foo bar</strong>
+ </div>
+ {% endspaceless %}
+
+ {# output will be <div><strong>foo bar</strong></div> #}
+
+In addition to the spaceless tag you can also control whitespace on a per tag
+level. By using the whitespace control modifier on your tags, you can trim
+leading and or trailing whitespace:
+
+.. code-block:: jinja
+
+ {% set value = 'no spaces' %}
+ {#- No leading/trailing whitespace -#}
+ {%- if true -%}
+ {{- value -}}
+ {%- endif -%}
+
+ {# output 'no spaces' #}
+
+The above sample shows the default whitespace control modifier, and how you can
+use it to remove whitespace around tags. Trimming space will consume all whitespace
+for that side of the tag. It is possible to use whitespace trimming on one side
+of a tag:
+
+.. code-block:: jinja
+
+ {% set value = 'no spaces' %}
+ <li> {{- value }} </li>
+
+ {# outputs '<li>no spaces </li>' #}
+
+Extensions
+----------
+
+Twig can be easily extended.
+
+If you are looking for new tags, filters, or functions, have a look at the Twig official
+`extension repository`_.
+
+If you want to create your own, read the :ref:`Creating an
+Extension<creating_extensions>` chapter.
+
+.. _`Twig bundle`: https://github.com/Anomareh/PHP-Twig.tmbundle
+.. _`Jinja syntax plugin`: http://jinja.pocoo.org/2/documentation/integration
+.. _`Twig syntax plugin`: http://plugins.netbeans.org/plugin/37069/php-twig
+.. _`Twig plugin`: https://github.com/pulse00/Twig-Eclipse-Plugin
+.. _`Twig language definition`: https://github.com/gabrielcorpse/gedit-twig-template-language
+.. _`extension repository`: http://github.com/fabpot/Twig-extensions
+.. _`Twig syntax mode`: https://github.com/bobthecow/Twig-HTML.mode
+.. _`other Twig syntax mode`: https://github.com/muxx/Twig-HTML.mode
+.. _`Notepad++ Twig Highlighter`: https://github.com/Banane9/notepadplusplus-twig
+.. _`web-mode.el`: http://web-mode.org/
--- /dev/null
+``constant``
+============
+
+.. versionadded: 1.13.1
+ constant now accepts object instances as the second argument.
+
+``constant`` checks if a variable has the exact same value as a constant. You
+can use either global constants or class constants:
+
+.. code-block:: jinja
+
+ {% if post.status is constant('Post::PUBLISHED') %}
+ the status attribute is exactly the same as Post::PUBLISHED
+ {% endif %}
+
+You can test constants from object instances as well:
+
+.. code-block:: jinja
+
+ {% if post.status is constant('PUBLISHED', post) %}
+ the status attribute is exactly the same as Post::PUBLISHED
+ {% endif %}
--- /dev/null
+``defined``
+===========
+
+``defined`` checks if a variable is defined in the current context. This is very
+useful if you use the ``strict_variables`` option:
+
+.. code-block:: jinja
+
+ {# defined works with variable names #}
+ {% if foo is defined %}
+ ...
+ {% endif %}
+
+ {# and attributes on variables names #}
+ {% if foo.bar is defined %}
+ ...
+ {% endif %}
+
+ {% if foo['bar'] is defined %}
+ ...
+ {% endif %}
+
+When using the ``defined`` test on an expression that uses variables in some
+method calls, be sure that they are all defined first:
+
+.. code-block:: jinja
+
+ {% if var is defined and foo.method(var) is defined %}
+ ...
+ {% endif %}
--- /dev/null
+``divisibleby``
+===============
+
+``divisibleby`` checks if a variable is divisible by a number:
+
+.. code-block:: jinja
+
+ {% if loop.index is divisibleby(3) %}
+ ...
+ {% endif %}
--- /dev/null
+``empty``
+=========
+
+``empty`` checks if a variable is empty:
+
+.. code-block:: jinja
+
+ {# evaluates to true if the foo variable is null, false, an empty array, or the empty string #}
+ {% if foo is empty %}
+ ...
+ {% endif %}
--- /dev/null
+``even``
+========
+
+``even`` returns ``true`` if the given number is even:
+
+.. code-block:: jinja
+
+ {{ var is even }}
+
+.. seealso:: :doc:`odd<../tests/odd>`
--- /dev/null
+Tests
+=====
+
+.. toctree::
+ :maxdepth: 1
+
+ constant
+ defined
+ divisibleby
+ empty
+ even
+ iterable
+ null
+ odd
+ sameas
--- /dev/null
+``iterable``
+============
+
+.. versionadded:: 1.7
+ The iterable test was added in Twig 1.7.
+
+``iterable`` checks if a variable is an array or a traversable object:
+
+.. code-block:: jinja
+
+ {# evaluates to true if the foo variable is iterable #}
+ {% if users is iterable %}
+ {% for user in users %}
+ Hello {{ user }}!
+ {% endfor %}
+ {% else %}
+ {# users is probably a string #}
+ Hello {{ users }}!
+ {% endif %}
--- /dev/null
+``null``
+========
+
+``null`` returns ``true`` if the variable is ``null``:
+
+.. code-block:: jinja
+
+ {{ var is null }}
+
+.. note::
+
+ ``none`` is an alias for ``null``.
--- /dev/null
+``odd``
+=======
+
+``odd`` returns ``true`` if the given number is odd:
+
+.. code-block:: jinja
+
+ {{ var is odd }}
+
+.. seealso:: :doc:`even<../tests/even>`
--- /dev/null
+``sameas``
+==========
+
+``sameas`` checks if a variable points to the same memory address than another
+variable:
+
+.. code-block:: jinja
+
+ {% if foo.attribute is sameas(false) %}
+ the foo attribute really is the ``false`` PHP value
+ {% endif %}
--- /dev/null
+*.sw*
+.deps
+Makefile
+Makefile.fragments
+Makefile.global
+Makefile.objects
+acinclude.m4
+aclocal.m4
+build/
+config.cache
+config.guess
+config.h
+config.h.in
+config.log
+config.nice
+config.status
+config.sub
+configure
+configure.in
+install-sh
+libtool
+ltmain.sh
+missing
+mkinstalldirs
+run-tests.php
+twig.loT
+.libs/
+modules/
+twig.la
+twig.lo
--- /dev/null
+Copyright (c) 2011, Derick Rethans <derick@derickrethans.nl>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+dnl config.m4 for extension twig
+
+PHP_ARG_ENABLE(twig, whether to enable twig support,
+[ --enable-twig Enable twig support])
+
+if test "$PHP_TWIG" != "no"; then
+ PHP_NEW_EXTENSION(twig, twig.c, $ext_shared)
+fi
--- /dev/null
+// vim:ft=javascript
+
+ARG_ENABLE("twig", "Twig support", "no");
+
+if (PHP_TWIG != "no") {
+ AC_DEFINE('HAVE_TWIG', 1);
+ EXTENSION('twig', 'twig.c');
+}
--- /dev/null
+/*
+ +----------------------------------------------------------------------+
+ | Twig Extension |
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2011 Derick Rethans |
+ +----------------------------------------------------------------------+
+ | Redistribution and use in source and binary forms, with or without |
+ | modification, are permitted provided that the conditions mentioned |
+ | in the accompanying LICENSE file are met (BSD, revised). |
+ +----------------------------------------------------------------------+
+ | Author: Derick Rethans <derick@derickrethans.nl> |
+ +----------------------------------------------------------------------+
+ */
+
+#ifndef PHP_TWIG_H
+#define PHP_TWIG_H
+
+#define PHP_TWIG_VERSION "1.13.2"
+
+#include "php.h"
+
+extern zend_module_entry twig_module_entry;
+#define phpext_twig_ptr &twig_module_entry
+
+#ifdef ZTS
+#include "TSRM.h"
+#endif
+
+PHP_FUNCTION(twig_template_get_attributes);
+
+#endif
--- /dev/null
+/*
+ +----------------------------------------------------------------------+
+ | Twig Extension |
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2011 Derick Rethans |
+ +----------------------------------------------------------------------+
+ | Redistribution and use in source and binary forms, with or without |
+ | modification, are permitted provided that the conditions mentioned |
+ | in the accompanying LICENSE file are met (BSD, revised). |
+ +----------------------------------------------------------------------+
+ | Author: Derick Rethans <derick@derickrethans.nl> |
+ +----------------------------------------------------------------------+
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "php.h"
+#include "php_twig.h"
+#include "ext/standard/php_string.h"
+#include "ext/standard/php_smart_str.h"
+
+#include "Zend/zend_object_handlers.h"
+#include "Zend/zend_interfaces.h"
+#include "Zend/zend_exceptions.h"
+
+#ifndef Z_ADDREF_P
+#define Z_ADDREF_P(pz) (pz)->refcount++
+#endif
+
+#define FREE_DTOR(z) \
+ zval_dtor(z); \
+ efree(z);
+
+#if PHP_VERSION_ID >= 50300
+ #define APPLY_TSRMLS_DC TSRMLS_DC
+ #define APPLY_TSRMLS_CC TSRMLS_CC
+ #define APPLY_TSRMLS_FETCH()
+#else
+ #define APPLY_TSRMLS_DC
+ #define APPLY_TSRMLS_CC
+ #define APPLY_TSRMLS_FETCH() TSRMLS_FETCH()
+#endif
+
+ZEND_BEGIN_ARG_INFO_EX(twig_template_get_attribute_args, ZEND_SEND_BY_VAL, ZEND_RETURN_VALUE, 6)
+ ZEND_ARG_INFO(0, template)
+ ZEND_ARG_INFO(0, object)
+ ZEND_ARG_INFO(0, item)
+ ZEND_ARG_INFO(0, arguments)
+ ZEND_ARG_INFO(0, type)
+ ZEND_ARG_INFO(0, isDefinedTest)
+ZEND_END_ARG_INFO()
+
+zend_function_entry twig_functions[] = {
+ PHP_FE(twig_template_get_attributes, twig_template_get_attribute_args)
+ {NULL, NULL, NULL}
+};
+
+
+zend_module_entry twig_module_entry = {
+ STANDARD_MODULE_HEADER,
+ "twig",
+ twig_functions,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ PHP_TWIG_VERSION,
+ STANDARD_MODULE_PROPERTIES
+};
+
+
+#ifdef COMPILE_DL_TWIG
+ZEND_GET_MODULE(twig)
+#endif
+
+int TWIG_ARRAY_KEY_EXISTS(zval *array, zval *key)
+{
+ zval temp;
+ int result;
+
+ if (Z_TYPE_P(array) != IS_ARRAY) {
+ return 0;
+ }
+
+ switch (Z_TYPE_P(key)) {
+ case IS_NULL:
+ return zend_hash_exists(Z_ARRVAL_P(array), "", 1);
+
+ case IS_BOOL:
+ case IS_DOUBLE:
+ convert_to_long(key);
+ case IS_LONG:
+ return zend_hash_index_exists(Z_ARRVAL_P(array), Z_LVAL_P(key));
+
+ default:
+ convert_to_string(key);
+ return zend_symtable_exists(Z_ARRVAL_P(array), Z_STRVAL_P(key), Z_STRLEN_P(key) + 1);
+ }
+}
+
+int TWIG_INSTANCE_OF(zval *object, zend_class_entry *interface TSRMLS_DC)
+{
+ if (Z_TYPE_P(object) != IS_OBJECT) {
+ return 0;
+ }
+ return instanceof_function(Z_OBJCE_P(object), interface TSRMLS_CC);
+}
+
+int TWIG_INSTANCE_OF_USERLAND(zval *object, char *interface TSRMLS_DC)
+{
+ zend_class_entry **pce;
+ if (Z_TYPE_P(object) != IS_OBJECT) {
+ return 0;
+ }
+ if (zend_lookup_class(interface, strlen(interface), &pce TSRMLS_CC) == FAILURE) {
+ return 0;
+ }
+ return instanceof_function(Z_OBJCE_P(object), *pce TSRMLS_CC);
+}
+
+zval *TWIG_GET_ARRAYOBJECT_ELEMENT(zval *object, zval *offset TSRMLS_DC)
+{
+ zend_class_entry *ce = Z_OBJCE_P(object);
+ zval *retval;
+
+ if (Z_TYPE_P(object) == IS_OBJECT) {
+ SEPARATE_ARG_IF_REF(offset);
+ zend_call_method_with_1_params(&object, ce, NULL, "offsetget", &retval, offset);
+
+ zval_ptr_dtor(&offset);
+
+ if (!retval) {
+ if (!EG(exception)) {
+ zend_error(E_ERROR, "Undefined offset for object of type %s used as array", ce->name);
+ }
+ return NULL;
+ }
+
+ return retval;
+ }
+ return NULL;
+}
+
+int TWIG_ISSET_ARRAYOBJECT_ELEMENT(zval *object, zval *offset TSRMLS_DC)
+{
+ zend_class_entry *ce = Z_OBJCE_P(object);
+ zval *retval;
+
+ if (Z_TYPE_P(object) == IS_OBJECT) {
+ SEPARATE_ARG_IF_REF(offset);
+ zend_call_method_with_1_params(&object, ce, NULL, "offsetexists", &retval, offset);
+
+ zval_ptr_dtor(&offset);
+
+ if (!retval) {
+ if (!EG(exception)) {
+ zend_error(E_ERROR, "Undefined offset for object of type %s used as array", ce->name);
+ }
+ return 0;
+ }
+
+ return (retval && Z_TYPE_P(retval) == IS_BOOL && Z_LVAL_P(retval));
+ }
+ return 0;
+}
+
+char *TWIG_STRTOLOWER(const char *str, int str_len)
+{
+ char *item_dup;
+
+ item_dup = estrndup(str, str_len);
+ php_strtolower(item_dup, str_len);
+ return item_dup;
+}
+
+zval *TWIG_CALL_USER_FUNC_ARRAY(zval *object, char *function, zval *arguments TSRMLS_DC)
+{
+ zend_fcall_info fci;
+ zval ***args = NULL;
+ int arg_count = 0;
+ HashTable *table;
+ HashPosition pos;
+ int i = 0;
+ zval *retval_ptr;
+ zval *zfunction;
+
+ if (arguments) {
+ table = HASH_OF(arguments);
+ args = safe_emalloc(sizeof(zval **), table->nNumOfElements, 0);
+
+ zend_hash_internal_pointer_reset_ex(table, &pos);
+
+ while (zend_hash_get_current_data_ex(table, (void **)&args[i], &pos) == SUCCESS) {
+ i++;
+ zend_hash_move_forward_ex(table, &pos);
+ }
+ arg_count = table->nNumOfElements;
+ }
+
+ MAKE_STD_ZVAL(zfunction);
+ ZVAL_STRING(zfunction, function, 1);
+ fci.size = sizeof(fci);
+ fci.function_table = EG(function_table);
+ fci.function_name = zfunction;
+ fci.symbol_table = NULL;
+#if PHP_VERSION_ID >= 50300
+ fci.object_ptr = object;
+#else
+ fci.object_pp = &object;
+#endif
+ fci.retval_ptr_ptr = &retval_ptr;
+ fci.param_count = arg_count;
+ fci.params = args;
+ fci.no_separation = 0;
+
+ if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE) {
+ ALLOC_INIT_ZVAL(retval_ptr);
+ ZVAL_BOOL(retval_ptr, 0);
+ }
+
+ if (args) {
+ efree(fci.params);
+ }
+ FREE_DTOR(zfunction);
+ return retval_ptr;
+}
+
+int TWIG_CALL_BOOLEAN(zval *object, char *functionName TSRMLS_DC)
+{
+ zval *ret;
+ int res;
+
+ ret = TWIG_CALL_USER_FUNC_ARRAY(object, functionName, NULL TSRMLS_CC);
+ res = Z_LVAL_P(ret);
+ zval_ptr_dtor(&ret);
+ return res;
+}
+
+zval *TWIG_GET_STATIC_PROPERTY(zval *class, char *prop_name TSRMLS_DC)
+{
+ zval **tmp_zval;
+ zend_class_entry *ce;
+
+ if (class == NULL || Z_TYPE_P(class) != IS_OBJECT) {
+ return NULL;
+ }
+
+ ce = zend_get_class_entry(class TSRMLS_CC);
+#if PHP_VERSION_ID >= 50400
+ tmp_zval = zend_std_get_static_property(ce, prop_name, strlen(prop_name), 0, NULL TSRMLS_CC);
+#else
+ tmp_zval = zend_std_get_static_property(ce, prop_name, strlen(prop_name), 0 TSRMLS_CC);
+#endif
+ return *tmp_zval;
+}
+
+zval *TWIG_GET_ARRAY_ELEMENT_ZVAL(zval *class, zval *prop_name TSRMLS_DC)
+{
+ zval **tmp_zval;
+ char *tmp_name;
+
+ if (class == NULL || Z_TYPE_P(class) != IS_ARRAY) {
+ if (class != NULL && Z_TYPE_P(class) == IS_OBJECT && TWIG_INSTANCE_OF(class, zend_ce_arrayaccess TSRMLS_CC)) {
+ // array access object
+ return TWIG_GET_ARRAYOBJECT_ELEMENT(class, prop_name TSRMLS_CC);
+ }
+ return NULL;
+ }
+
+ switch(Z_TYPE_P(prop_name)) {
+ case IS_NULL:
+ zend_hash_find(HASH_OF(class), "", 1, (void**) &tmp_zval);
+ return *tmp_zval;
+
+ case IS_BOOL:
+ case IS_DOUBLE:
+ convert_to_long(prop_name);
+ case IS_LONG:
+ zend_hash_index_find(HASH_OF(class), Z_LVAL_P(prop_name), (void **) &tmp_zval);
+ return *tmp_zval;
+
+ case IS_STRING:
+ zend_symtable_find(HASH_OF(class), Z_STRVAL_P(prop_name), Z_STRLEN_P(prop_name) + 1, (void**) &tmp_zval);
+ return *tmp_zval;
+ }
+
+ return NULL;
+}
+
+zval *TWIG_GET_ARRAY_ELEMENT(zval *class, char *prop_name, int prop_name_length TSRMLS_DC)
+{
+ zval **tmp_zval;
+
+ if (class == NULL/* || Z_TYPE_P(class) != IS_ARRAY*/) {
+ return NULL;
+ }
+
+ if (class != NULL && Z_TYPE_P(class) == IS_OBJECT && TWIG_INSTANCE_OF(class, zend_ce_arrayaccess TSRMLS_CC)) {
+ // array access object
+ zval *tmp_name_zval;
+ zval *tmp_ret_zval;
+
+ ALLOC_INIT_ZVAL(tmp_name_zval);
+ ZVAL_STRING(tmp_name_zval, prop_name, 1);
+ tmp_ret_zval = TWIG_GET_ARRAYOBJECT_ELEMENT(class, tmp_name_zval TSRMLS_CC);
+ FREE_DTOR(tmp_name_zval);
+ return tmp_ret_zval;
+ }
+
+ if (zend_symtable_find(HASH_OF(class), prop_name, prop_name_length+1, (void**)&tmp_zval) == SUCCESS) {
+ return *tmp_zval;
+ }
+ return NULL;
+}
+
+zval *TWIG_PROPERTY(zval *object, zval *propname TSRMLS_DC)
+{
+ zval *tmp = NULL;
+
+ if (Z_OBJ_HT_P(object)->read_property) {
+#if PHP_VERSION_ID >= 50400
+ tmp = Z_OBJ_HT_P(object)->read_property(object, propname, BP_VAR_IS, NULL TSRMLS_CC);
+#else
+ tmp = Z_OBJ_HT_P(object)->read_property(object, propname, BP_VAR_IS TSRMLS_CC);
+#endif
+ if (tmp != EG(uninitialized_zval_ptr)) {
+ return tmp;
+ } else {
+ return NULL;
+ }
+ }
+ return tmp;
+}
+
+int TWIG_HAS_PROPERTY(zval *object, zval *propname TSRMLS_DC)
+{
+ if (Z_OBJ_HT_P(object)->has_property) {
+#if PHP_VERSION_ID >= 50400
+ return Z_OBJ_HT_P(object)->has_property(object, propname, 0, NULL TSRMLS_CC);
+#else
+ return Z_OBJ_HT_P(object)->has_property(object, propname, 0 TSRMLS_CC);
+#endif
+ }
+ return 0;
+}
+
+int TWIG_HAS_DYNAMIC_PROPERTY(zval *object, char *prop, int prop_len TSRMLS_DC)
+{
+ if (Z_OBJ_HT_P(object)->get_properties) {
+ return zend_hash_quick_exists(
+ Z_OBJ_HT_P(object)->get_properties(object TSRMLS_CC), // the properties hash
+ prop, // property name
+ prop_len + 1, // property length
+ zend_get_hash_value(prop, prop_len + 1) // hash value
+ );
+ }
+ return 0;
+}
+
+zval *TWIG_PROPERTY_CHAR(zval *object, char *propname TSRMLS_DC)
+{
+ zval *tmp_name_zval, *tmp;
+
+ ALLOC_INIT_ZVAL(tmp_name_zval);
+ ZVAL_STRING(tmp_name_zval, propname, 1);
+ tmp = TWIG_PROPERTY(object, tmp_name_zval TSRMLS_CC);
+ FREE_DTOR(tmp_name_zval);
+ return tmp;
+}
+
+int TWIG_CALL_B_0(zval *object, char *method)
+{
+ return 0;
+}
+
+zval *TWIG_CALL_S(zval *object, char *method, char *arg0 TSRMLS_DC)
+{
+ zend_fcall_info fci;
+ zval **args[1];
+ zval *argument;
+ zval *zfunction;
+ zval *retval_ptr;
+
+ MAKE_STD_ZVAL(argument);
+ ZVAL_STRING(argument, arg0, 1);
+ args[0] = &argument;
+
+ MAKE_STD_ZVAL(zfunction);
+ ZVAL_STRING(zfunction, method, 1);
+ fci.size = sizeof(fci);
+ fci.function_table = EG(function_table);
+ fci.function_name = zfunction;
+ fci.symbol_table = NULL;
+#if PHP_VERSION_ID >= 50300
+ fci.object_ptr = object;
+#else
+ fci.object_pp = &object;
+#endif
+ fci.retval_ptr_ptr = &retval_ptr;
+ fci.param_count = 1;
+ fci.params = args;
+ fci.no_separation = 0;
+
+ if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE) {
+ FREE_DTOR(zfunction);
+ zval_ptr_dtor(&argument);
+ return 0;
+ }
+ FREE_DTOR(zfunction);
+ zval_ptr_dtor(&argument);
+ return retval_ptr;
+}
+
+int TWIG_CALL_SB(zval *object, char *method, char *arg0 TSRMLS_DC)
+{
+ zval *retval_ptr;
+ int success;
+
+ retval_ptr = TWIG_CALL_S(object, method, arg0 TSRMLS_CC);
+ success = (retval_ptr && (Z_TYPE_P(retval_ptr) == IS_BOOL) && Z_LVAL_P(retval_ptr));
+
+ if (retval_ptr) {
+ zval_ptr_dtor(&retval_ptr);
+ }
+
+ return success;
+}
+
+int TWIG_CALL_Z(zval *object, char *method, zval *arg1 TSRMLS_DC)
+{
+ zend_fcall_info fci;
+ zval **args[1];
+ zval *zfunction;
+ zval *retval_ptr;
+ int success;
+
+ args[0] = &arg1;
+
+ MAKE_STD_ZVAL(zfunction);
+ ZVAL_STRING(zfunction, method, 1);
+ fci.size = sizeof(fci);
+ fci.function_table = EG(function_table);
+ fci.function_name = zfunction;
+ fci.symbol_table = NULL;
+#if PHP_VERSION_ID >= 50300
+ fci.object_ptr = object;
+#else
+ fci.object_pp = &object;
+#endif
+ fci.retval_ptr_ptr = &retval_ptr;
+ fci.param_count = 1;
+ fci.params = args;
+ fci.no_separation = 0;
+
+ if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE) {
+ FREE_DTOR(zfunction);
+ if (retval_ptr) {
+ zval_ptr_dtor(&retval_ptr);
+ }
+ return 0;
+ }
+
+ FREE_DTOR(zfunction);
+
+ success = (retval_ptr && (Z_TYPE_P(retval_ptr) == IS_BOOL) && Z_LVAL_P(retval_ptr));
+ if (retval_ptr) {
+ zval_ptr_dtor(&retval_ptr);
+ }
+
+ return success;
+}
+
+int TWIG_CALL_ZZ(zval *object, char *method, zval *arg1, zval *arg2 TSRMLS_DC)
+{
+ zend_fcall_info fci;
+ zval **args[2];
+ zval *zfunction;
+ zval *retval_ptr;
+ int success;
+
+ args[0] = &arg1;
+ args[1] = &arg2;
+
+ MAKE_STD_ZVAL(zfunction);
+ ZVAL_STRING(zfunction, method, 1);
+ fci.size = sizeof(fci);
+ fci.function_table = EG(function_table);
+ fci.function_name = zfunction;
+ fci.symbol_table = NULL;
+#if PHP_VERSION_ID >= 50300
+ fci.object_ptr = object;
+#else
+ fci.object_pp = &object;
+#endif
+ fci.retval_ptr_ptr = &retval_ptr;
+ fci.param_count = 2;
+ fci.params = args;
+ fci.no_separation = 0;
+
+ if (zend_call_function(&fci, NULL TSRMLS_CC) == FAILURE) {
+ FREE_DTOR(zfunction);
+ return 0;
+ }
+
+ FREE_DTOR(zfunction);
+
+ success = (retval_ptr && (Z_TYPE_P(retval_ptr) == IS_BOOL) && Z_LVAL_P(retval_ptr));
+ if (retval_ptr) {
+ zval_ptr_dtor(&retval_ptr);
+ }
+
+ return success;
+}
+
+#ifndef Z_SET_REFCOUNT_P
+# define Z_SET_REFCOUNT_P(pz, rc) pz->refcount = rc
+# define Z_UNSET_ISREF_P(pz) pz->is_ref = 0
+#endif
+
+void TWIG_NEW(zval *object, char *class, zval *arg0, zval *arg1 TSRMLS_DC)
+{
+ zend_class_entry **pce;
+
+ if (zend_lookup_class(class, strlen(class), &pce TSRMLS_CC) == FAILURE) {
+ return;
+ }
+
+ Z_TYPE_P(object) = IS_OBJECT;
+ object_init_ex(object, *pce);
+ Z_SET_REFCOUNT_P(object, 1);
+ Z_UNSET_ISREF_P(object);
+
+ TWIG_CALL_ZZ(object, "__construct", arg0, arg1 TSRMLS_CC);
+}
+
+static int twig_add_array_key_to_string(void *pDest APPLY_TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
+{
+ smart_str *buf;
+ char *joiner;
+ APPLY_TSRMLS_FETCH();
+
+ buf = va_arg(args, smart_str*);
+ joiner = va_arg(args, char*);
+
+ if (buf->len != 0) {
+ smart_str_appends(buf, joiner);
+ }
+
+ if (hash_key->nKeyLength == 0) {
+ smart_str_append_long(buf, (long) hash_key->h);
+ } else {
+ char *key, *tmp_str;
+ int key_len, tmp_len;
+ key = php_addcslashes(hash_key->arKey, hash_key->nKeyLength - 1, &key_len, 0, "'\\", 2 TSRMLS_CC);
+ tmp_str = php_str_to_str_ex(key, key_len, "\0", 1, "' . \"\\0\" . '", 12, &tmp_len, 0, NULL);
+
+ smart_str_appendl(buf, tmp_str, tmp_len);
+ efree(key);
+ efree(tmp_str);
+ }
+
+ return 0;
+}
+
+char *TWIG_IMPLODE_ARRAY_KEYS(char *joiner, zval *array TSRMLS_DC)
+{
+ smart_str collector = { 0, 0, 0 };
+
+ smart_str_appendl(&collector, "", 0);
+ zend_hash_apply_with_arguments(HASH_OF(array) APPLY_TSRMLS_CC, twig_add_array_key_to_string, 2, &collector, joiner);
+ smart_str_0(&collector);
+
+ return collector.c;
+}
+
+static void TWIG_THROW_EXCEPTION(char *exception_name TSRMLS_DC, char *message, ...)
+{
+ char *buffer;
+ va_list args;
+ zend_class_entry **pce;
+
+ if (zend_lookup_class(exception_name, strlen(exception_name), &pce TSRMLS_CC) == FAILURE) {
+ return;
+ }
+
+ va_start(args, message);
+ vspprintf(&buffer, 0, message, args);
+ va_end(args);
+
+ zend_throw_exception_ex(*pce, 0 TSRMLS_CC, buffer);
+ efree(buffer);
+}
+
+static void TWIG_RUNTIME_ERROR(zval *template TSRMLS_DC, char *message, ...)
+{
+ char *buffer;
+ va_list args;
+ zend_class_entry **pce;
+ zval *ex;
+ zval *constructor;
+ zval *zmessage;
+ zval *lineno;
+ zval *filename_func;
+ zval *filename;
+ zval *constructor_args[3];
+ zval *constructor_retval;
+
+ if (zend_lookup_class("Twig_Error_Runtime", strlen("Twig_Error_Runtime"), &pce TSRMLS_CC) == FAILURE) {
+ return;
+ }
+
+ va_start(args, message);
+ vspprintf(&buffer, 0, message, args);
+ va_end(args);
+
+ MAKE_STD_ZVAL(ex);
+ object_init_ex(ex, *pce);
+
+ // Call Twig_Error constructor
+ MAKE_STD_ZVAL(constructor);
+ MAKE_STD_ZVAL(zmessage);
+ MAKE_STD_ZVAL(lineno);
+ MAKE_STD_ZVAL(filename);
+ MAKE_STD_ZVAL(filename_func);
+ MAKE_STD_ZVAL(constructor_retval);
+
+ ZVAL_STRINGL(constructor, "__construct", sizeof("__construct")-1, 1);
+ ZVAL_STRING(zmessage, buffer, 1);
+ ZVAL_LONG(lineno, -1);
+
+ // Get template filename
+ ZVAL_STRINGL(filename_func, "getTemplateName", sizeof("getTemplateName")-1, 1);
+ call_user_function(EG(function_table), &template, filename_func, filename, 0, 0 TSRMLS_CC);
+
+ constructor_args[0] = zmessage;
+ constructor_args[1] = lineno;
+ constructor_args[2] = filename;
+ call_user_function(EG(function_table), &ex, constructor, constructor_retval, 3, constructor_args TSRMLS_CC);
+
+ zval_ptr_dtor(&constructor_retval);
+ zval_ptr_dtor(&zmessage);
+ zval_ptr_dtor(&lineno);
+ zval_ptr_dtor(&filename);
+ FREE_DTOR(constructor);
+ FREE_DTOR(filename_func);
+ efree(buffer);
+
+ zend_throw_exception_object(ex TSRMLS_CC);
+}
+
+static char *TWIG_GET_CLASS_NAME(zval *object TSRMLS_DC)
+{
+ char *class_name;
+ zend_uint class_name_len;
+
+ if (Z_TYPE_P(object) != IS_OBJECT) {
+ return "";
+ }
+#if PHP_API_VERSION >= 20100412
+ zend_get_object_classname(object, (const char **) &class_name, &class_name_len TSRMLS_CC);
+#else
+ zend_get_object_classname(object, &class_name, &class_name_len TSRMLS_CC);
+#endif
+ return class_name;
+}
+
+static int twig_add_method_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
+{
+ zval *retval;
+ char *item;
+ size_t item_len;
+ zend_function *mptr = (zend_function *) pDest;
+ APPLY_TSRMLS_FETCH();
+
+ if (!(mptr->common.fn_flags & ZEND_ACC_PUBLIC)) {
+ return 0;
+ }
+
+ retval = va_arg(args, zval*);
+
+ item_len = strlen(mptr->common.function_name);
+ item = estrndup(mptr->common.function_name, item_len);
+ php_strtolower(item, item_len);
+
+ add_assoc_stringl_ex(retval, item, item_len+1, item, item_len, 0);
+
+ return 0;
+}
+
+static int twig_add_property_to_class(void *pDest APPLY_TSRMLS_DC, int num_args, va_list args, zend_hash_key *hash_key)
+{
+ zend_class_entry *ce;
+ zval *retval;
+ char *class_name, *prop_name;
+ zend_property_info *pptr = (zend_property_info *) pDest;
+ APPLY_TSRMLS_FETCH();
+
+ if (!(pptr->flags & ZEND_ACC_PUBLIC)) {
+ return 0;
+ }
+
+ ce = *va_arg(args, zend_class_entry**);
+ retval = va_arg(args, zval*);
+
+#if PHP_API_VERSION >= 20100412
+ zend_unmangle_property_name(pptr->name, pptr->name_length, (const char **) &class_name, (const char **) &prop_name);
+#else
+ zend_unmangle_property_name(pptr->name, pptr->name_length, &class_name, &prop_name);
+#endif
+
+ add_assoc_string(retval, prop_name, prop_name, 1);
+
+ return 0;
+}
+
+static void twig_add_class_to_cache(zval *cache, zval *object, char *class_name TSRMLS_DC)
+{
+ zval *class_info, *class_methods, *class_properties;
+ zend_class_entry *class_ce;
+
+ class_ce = zend_get_class_entry(object TSRMLS_CC);
+
+ ALLOC_INIT_ZVAL(class_info);
+ ALLOC_INIT_ZVAL(class_methods);
+ ALLOC_INIT_ZVAL(class_properties);
+ array_init(class_info);
+ array_init(class_methods);
+ array_init(class_properties);
+ // add all methods to self::cache[$class]['methods']
+ zend_hash_apply_with_arguments(&class_ce->function_table APPLY_TSRMLS_CC, twig_add_method_to_class, 1, class_methods);
+ zend_hash_apply_with_arguments(&class_ce->properties_info APPLY_TSRMLS_CC, twig_add_property_to_class, 2, &class_ce, class_properties);
+
+ add_assoc_zval(class_info, "methods", class_methods);
+ add_assoc_zval(class_info, "properties", class_properties);
+ add_assoc_zval(cache, class_name, class_info);
+}
+
+/* {{{ proto mixed twig_template_get_attributes(TwigTemplate template, mixed object, mixed item, array arguments, string type, boolean isDefinedTest, boolean ignoreStrictCheck)
+ A C implementation of TwigTemplate::getAttribute() */
+PHP_FUNCTION(twig_template_get_attributes)
+{
+ zval *template;
+ zval *object;
+ char *item;
+ int item_len;
+ zval *zitem, ztmpitem;
+ zval *arguments = NULL;
+ zval *ret = NULL;
+ char *type = NULL;
+ int type_len = 0;
+ zend_bool isDefinedTest = 0;
+ zend_bool ignoreStrictCheck = 0;
+ int free_ret = 0;
+ zval *tmp_self_cache;
+
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ozz|asbb", &template, &object, &zitem, &arguments, &type, &type_len, &isDefinedTest, &ignoreStrictCheck) == FAILURE) {
+ return;
+ }
+
+ // convert the item to a string
+ ztmpitem = *zitem;
+ zval_copy_ctor(&ztmpitem);
+ convert_to_string(&ztmpitem);
+ item_len = Z_STRLEN(ztmpitem);
+ item = estrndup(Z_STRVAL(ztmpitem), item_len);
+ zval_dtor(&ztmpitem);
+
+ if (!type) {
+ type = "any";
+ }
+
+/*
+ // array
+ if (Twig_TemplateInterface::METHOD_CALL !== $type) {
+ $arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item;
+
+ if ((is_array($object) && array_key_exists($arrayItem, $object))
+ || ($object instanceof ArrayAccess && isset($object[$arrayItem]))
+ ) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ return $object[$arrayItem];
+ }
+*/
+
+
+ if (strcmp("method", type) != 0) {
+ if ((TWIG_ARRAY_KEY_EXISTS(object, zitem))
+ || (TWIG_INSTANCE_OF(object, zend_ce_arrayaccess TSRMLS_CC) && TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, zitem TSRMLS_CC))
+ ) {
+
+ if (isDefinedTest) {
+ RETURN_TRUE;
+ }
+
+ ret = TWIG_GET_ARRAY_ELEMENT_ZVAL(object, zitem TSRMLS_CC);
+
+ if (!ret) {
+ ret = &EG(uninitialized_zval);
+ }
+ RETVAL_ZVAL(ret, 1, 0);
+ if (free_ret) {
+ zval_ptr_dtor(&ret);
+ }
+ return;
+ }
+/*
+ if (Twig_TemplateInterface::ARRAY_CALL === $type) {
+ if ($isDefinedTest) {
+ return false;
+ }
+ if ($ignoreStrictCheck || !$this->env->isStrictVariables()) {
+ return null;
+ }
+*/
+ if (strcmp("array", type) == 0 || Z_TYPE_P(object) != IS_OBJECT) {
+ if (isDefinedTest) {
+ RETURN_FALSE;
+ }
+ if (ignoreStrictCheck || !TWIG_CALL_BOOLEAN(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "isStrictVariables" TSRMLS_CC)) {
+ return;
+ }
+/*
+ if (is_object($object)) {
+ throw new Twig_Error_Runtime(sprintf('Key "%s" in object (with ArrayAccess) of type "%s" does not exist', $arrayItem, get_class($object)), -1, $this->getTemplateName());
+ } elseif (is_array($object)) {
+ throw new Twig_Error_Runtime(sprintf('Key "%s" for array with keys "%s" does not exist', $arrayItem, implode(', ', array_keys($object))), -1, $this->getTemplateName());
+ } elseif (Twig_TemplateInterface::ARRAY_CALL === $type) {
+ throw new Twig_Error_Runtime(sprintf('Impossible to access a key ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName());
+ } else {
+ throw new Twig_Error_Runtime(sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName());
+ }
+ }
+ }
+*/
+ if (Z_TYPE_P(object) == IS_OBJECT) {
+ TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Key \"%s\" in object (with ArrayAccess) of type \"%s\" does not exist", item, TWIG_GET_CLASS_NAME(object TSRMLS_CC));
+ } else if (Z_TYPE_P(object) == IS_ARRAY) {
+ TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Key \"%s\" for array with keys \"%s\" does not exist", item, TWIG_IMPLODE_ARRAY_KEYS(", ", object TSRMLS_CC));
+ } else {
+ char *type_name = zend_zval_type_name(object);
+ Z_ADDREF_P(object);
+ convert_to_string(object);
+ TWIG_RUNTIME_ERROR(template TSRMLS_CC,
+ (strcmp("array", type) == 0)
+ ? "Impossible to access a key (\"%s\") on a %s variable (\"%s\")"
+ : "Impossible to access an attribute (\"%s\") on a %s variable (\"%s\")",
+ item, type_name, Z_STRVAL_P(object));
+ zval_ptr_dtor(&object);
+ }
+ return;
+ }
+ }
+
+/*
+ if (!is_object($object)) {
+ if ($isDefinedTest) {
+ return false;
+ }
+*/
+
+ if (Z_TYPE_P(object) != IS_OBJECT) {
+ if (isDefinedTest) {
+ RETURN_FALSE;
+ }
+/*
+ if ($ignoreStrictCheck || !$this->env->isStrictVariables()) {
+ return null;
+ }
+ throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName());
+ }
+*/
+ if (ignoreStrictCheck || !TWIG_CALL_BOOLEAN(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "isStrictVariables" TSRMLS_CC)) {
+ return;
+ }
+
+ char *type_name = zend_zval_type_name(object);
+ Z_ADDREF_P(object);
+ convert_to_string_ex(&object);
+
+ TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Impossible to invoke a method (\"%s\") on a %s variable (\"%s\")", item, type_name, Z_STRVAL_P(object));
+
+ zval_ptr_dtor(&object);
+
+ return;
+ }
+/*
+ $class = get_class($object);
+*/
+ char *class_name = NULL;
+ zval *tmp_class;
+
+ class_name = TWIG_GET_CLASS_NAME(object TSRMLS_CC);
+ tmp_self_cache = TWIG_GET_STATIC_PROPERTY(template, "cache" TSRMLS_CC);
+ tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
+
+ if (!tmp_class) {
+ twig_add_class_to_cache(tmp_self_cache, object, class_name TSRMLS_CC);
+ tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
+ }
+ efree(class_name);
+
+/*
+ // object property
+ if (Twig_TemplateInterface::METHOD_CALL !== $type) {
+ if (isset($object->$item) || array_key_exists((string) $item, $object)) {
+ if ($isDefinedTest) {
+ return true;
+ }
+
+ if ($this->env->hasExtension('sandbox')) {
+ $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item);
+ }
+
+ return $object->$item;
+ }
+ }
+*/
+ if (strcmp("method", type) != 0) {
+ zval *tmp_properties, *tmp_item;
+
+ tmp_properties = TWIG_GET_ARRAY_ELEMENT(tmp_class, "properties", strlen("properties") TSRMLS_CC);
+ tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC);
+
+ if (tmp_item || TWIG_HAS_PROPERTY(object, zitem TSRMLS_CC) || TWIG_HAS_DYNAMIC_PROPERTY(object, item, item_len TSRMLS_CC)) {
+ if (isDefinedTest) {
+ RETURN_TRUE;
+ }
+ if (TWIG_CALL_SB(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "hasExtension", "sandbox" TSRMLS_CC)) {
+ TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkPropertyAllowed", object, zitem TSRMLS_CC);
+ }
+ if (EG(exception)) {
+ return;
+ }
+
+ ret = TWIG_PROPERTY(object, zitem TSRMLS_CC);
+ RETURN_ZVAL(ret, 1, 0);
+ }
+ }
+/*
+ // object method
+ if (!isset(self::$cache[$class]['methods'])) {
+ self::$cache[$class]['methods'] = array_change_key_case(array_flip(get_class_methods($object)));
+ }
+
+ $lcItem = strtolower($item);
+ if (isset(self::$cache[$class]['methods'][$lcItem])) {
+ $method = (string) $item;
+ } elseif (isset(self::$cache[$class]['methods']['get'.$lcItem])) {
+ $method = 'get'.$item;
+ } elseif (isset(self::$cache[$class]['methods']['is'.$lcItem])) {
+ $method = 'is'.$item;
+ } elseif (isset(self::$cache[$class]['methods']['__call'])) {
+ $method = (string) $item;
+*/
+ {
+ char *lcItem = TWIG_STRTOLOWER(item, item_len);
+ int lcItem_length;
+ char *method = NULL;
+ char *tmp_method_name_get;
+ char *tmp_method_name_is;
+ zval *tmp_methods;
+
+ lcItem_length = strlen(lcItem);
+ tmp_method_name_get = emalloc(4 + lcItem_length);
+ tmp_method_name_is = emalloc(3 + lcItem_length);
+
+ sprintf(tmp_method_name_get, "get%s", lcItem);
+ sprintf(tmp_method_name_is, "is%s", lcItem);
+
+ tmp_methods = TWIG_GET_ARRAY_ELEMENT(tmp_class, "methods", strlen("methods") TSRMLS_CC);
+
+ if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, lcItem, lcItem_length TSRMLS_CC)) {
+ method = item;
+ } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, tmp_method_name_get, lcItem_length + 3 TSRMLS_CC)) {
+ method = tmp_method_name_get;
+ } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, tmp_method_name_is, lcItem_length + 2 TSRMLS_CC)) {
+ method = tmp_method_name_is;
+ } else if (TWIG_GET_ARRAY_ELEMENT(tmp_methods, "__call", 6 TSRMLS_CC)) {
+ method = item;
+/*
+ } else {
+ if ($isDefinedTest) {
+ return false;
+ }
+
+ if ($ignoreStrictCheck || !$this->env->isStrictVariables()) {
+ return null;
+ }
+
+ throw new Twig_Error_Runtime(sprintf('Method "%s" for object "%s" does not exist', $item, get_class($object)), -1, $this->getTemplateName());
+ }
+
+ if ($isDefinedTest) {
+ return true;
+ }
+*/
+ } else {
+ efree(tmp_method_name_get);
+ efree(tmp_method_name_is);
+ efree(lcItem);
+
+ if (isDefinedTest) {
+ RETURN_FALSE;
+ }
+ if (ignoreStrictCheck || !TWIG_CALL_BOOLEAN(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "isStrictVariables" TSRMLS_CC)) {
+ return;
+ }
+ TWIG_RUNTIME_ERROR(template TSRMLS_CC, "Method \"%s\" for object \"%s\" does not exist", item, TWIG_GET_CLASS_NAME(object TSRMLS_CC));
+ return;
+ }
+
+ if (isDefinedTest) {
+ efree(tmp_method_name_get);
+ efree(tmp_method_name_is);
+ efree(lcItem);
+ RETURN_TRUE;
+ }
+/*
+ if ($this->env->hasExtension('sandbox')) {
+ $this->env->getExtension('sandbox')->checkMethodAllowed($object, $method);
+ }
+*/
+ if (TWIG_CALL_SB(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "hasExtension", "sandbox" TSRMLS_CC)) {
+ TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkMethodAllowed", object, zitem TSRMLS_CC);
+ }
+ if (EG(exception)) {
+ efree(tmp_method_name_get);
+ efree(tmp_method_name_is);
+ efree(lcItem);
+ return;
+ }
+/*
+ $ret = call_user_func_array(array($object, $method), $arguments);
+*/
+ ret = TWIG_CALL_USER_FUNC_ARRAY(object, method, arguments TSRMLS_CC);
+ free_ret = 1;
+ efree(tmp_method_name_get);
+ efree(tmp_method_name_is);
+ efree(lcItem);
+ }
+/*
+ // useful when calling a template method from a template
+ // this is not supported but unfortunately heavily used in the Symfony profiler
+ if ($object instanceof Twig_TemplateInterface) {
+ return $ret === '' ? '' : new Twig_Markup($ret, $this->env->getCharset());
+ }
+
+ return $ret;
+*/
+ // ret can be null, if e.g. the called method throws an exception
+ if (ret) {
+ if (TWIG_INSTANCE_OF_USERLAND(object, "Twig_TemplateInterface" TSRMLS_CC)) {
+ if (Z_STRLEN_P(ret) != 0) {
+ zval *charset = TWIG_CALL_USER_FUNC_ARRAY(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getCharset", NULL TSRMLS_CC);
+ TWIG_NEW(return_value, "Twig_Markup", ret, charset TSRMLS_CC);
+ zval_ptr_dtor(&charset);
+ if (ret) {
+ zval_ptr_dtor(&ret);
+ }
+ return;
+ }
+ }
+
+ RETVAL_ZVAL(ret, 1, 0);
+ if (free_ret) {
+ zval_ptr_dtor(&ret);
+ }
+ }
+}
*/
class Twig_Environment
{
- const VERSION = '1.13.1';
+ const VERSION = '1.13.2';
protected $charset;
protected $loader;
public function addNodeVisitor(Twig_NodeVisitorInterface $visitor)
{
if ($this->extensionInitialized) {
- throw new LogicException('Unable to add a node visitor as extensions have already been initialized.', $extension->getName());
+ throw new LogicException('Unable to add a node visitor as extensions have already been initialized.');
}
$this->staging->addNodeVisitor($visitor);
protected function guessTemplateInfo()
{
$template = null;
+ $templateClass = null;
if (version_compare(phpversion(), '5.3.6', '>=')) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
foreach ($backtrace as $trace) {
if (isset($trace['object']) && $trace['object'] instanceof Twig_Template && 'Twig_Template' !== get_class($trace['object'])) {
- if (null === $this->filename || $this->filename == $trace['object']->getTemplateName()) {
+ $currentClass = get_class($trace['object']);
+ $isEmbedContainer = 0 === strpos($templateClass, $currentClass);
+ if (null === $this->filename || ($this->filename == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
$template = $trace['object'];
+ $templateClass = get_class($trace['object']);
}
}
}
*/
class Twig_Loader_Filesystem implements Twig_LoaderInterface, Twig_ExistsLoaderInterface
{
+ /** Identifier of the main namespace. */
+ const MAIN_NAMESPACE = '__main__';
+
protected $paths;
protected $cache;
*
* @return array The array of paths where to look for templates
*/
- public function getPaths($namespace = '__main__')
+ public function getPaths($namespace = self::MAIN_NAMESPACE)
{
return isset($this->paths[$namespace]) ? $this->paths[$namespace] : array();
}
/**
* Returns the path namespaces.
*
- * The "__main__" namespace is always defined.
+ * The main namespace is always defined.
*
* @return array The array of defined namespaces
*/
* @param string|array $paths A path or an array of paths where to look for templates
* @param string $namespace A path namespace
*/
- public function setPaths($paths, $namespace = '__main__')
+ public function setPaths($paths, $namespace = self::MAIN_NAMESPACE)
{
if (!is_array($paths)) {
$paths = array($paths);
*
* @throws Twig_Error_Loader
*/
- public function addPath($path, $namespace = '__main__')
+ public function addPath($path, $namespace = self::MAIN_NAMESPACE)
{
// invalidate the cache
$this->cache = array();
*
* @throws Twig_Error_Loader
*/
- public function prependPath($path, $namespace = '__main__')
+ public function prependPath($path, $namespace = self::MAIN_NAMESPACE)
{
// invalidate the cache
$this->cache = array();
$this->validateName($name);
- $namespace = '__main__';
+ $namespace = self::MAIN_NAMESPACE;
if (isset($name[0]) && '@' == $name[0]) {
if (false === $pos = strpos($name, '/')) {
throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
if (array_key_exists($name, $parameters)) {
if (array_key_exists($pos, $parameters)) {
- throw new Twig_Error_Syntax(sprintf('Arguments "%s" is defined twice for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+ throw new Twig_Error_Syntax(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
}
$arguments[] = $parameters[$name];
}
}
- foreach (array_keys($parameters) as $name) {
- throw new Twig_Error_Syntax(sprintf('Unknown argument "%s" for %s "%s".', $name, $this->getAttribute('type'), $this->getAttribute('name')));
+ if (!empty($parameters)) {
+ throw new Twig_Error_Syntax(sprintf('Unknown argument%s "%s" for %s "%s".', count($parameters) > 1 ? 's' : '' , implode('", "', array_keys($parameters)), $this->getAttribute('type'), $this->getAttribute('name')));
}
return $arguments;
--- /dev/null
+<?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="test/bootstrap.php"
+>
+ <testsuites>
+ <testsuite name="Twig Test Suite">
+ <directory>./test/Twig/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory suffix=".php">./lib/Twig/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_AutoloaderTest extends PHPUnit_Framework_TestCase
+{
+ public function testAutoload()
+ {
+ $this->assertFalse(class_exists('FooBarFoo'), '->autoload() does not try to load classes that does not begin with Twig');
+
+ $autoloader = new Twig_Autoloader();
+ $this->assertNull($autoloader->autoload('Foo'), '->autoload() returns false if it is not able to load a class');
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_CompilerTest extends PHPUnit_Framework_TestCase
+{
+ public function testReprNumericValueWithLocale()
+ {
+ $compiler = new Twig_Compiler(new Twig_Environment());
+
+ $locale = setlocale(LC_NUMERIC, 0);
+ if (false === $locale) {
+ $this->markTestSkipped('Your platform does not support locales.');
+ }
+
+ $required_locales = array('fr_FR.UTF-8', 'fr_FR.UTF8', 'fr_FR.utf-8', 'fr_FR.utf8', 'French_France.1252');
+ if (false === setlocale(LC_ALL, $required_locales)) {
+ $this->markTestSkipped('Could not set any of required locales: ' . implode(", ", $required_locales));
+ }
+
+ $this->assertEquals('1.2', $compiler->repr(1.2)->getSource());
+ $this->assertContains('fr', strtolower(setlocale(LC_NUMERIC, 0)));
+
+ setlocale(LC_ALL, $locale);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_EnvironmentTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException LogicException
+ * @expectedExceptionMessage You must set a loader first.
+ */
+ public function testRenderNoLoader()
+ {
+ $env = new Twig_Environment();
+ $env->render('test');
+ }
+
+ public function testAutoescapeOption()
+ {
+ $loader = new Twig_Loader_Array(array(
+ 'html' => '{{ foo }} {{ foo }}',
+ 'js' => '{{ bar }} {{ bar }}',
+ ));
+
+ $twig = new Twig_Environment($loader, array(
+ 'debug' => true,
+ 'cache' => false,
+ 'autoescape' => array($this, 'escapingStrategyCallback'),
+ ));
+
+ $this->assertEquals('foo<br/ > foo<br/ >', $twig->render('html', array('foo' => 'foo<br/ >')));
+ $this->assertEquals('foo\x3Cbr\x2F\x20\x3E foo\x3Cbr\x2F\x20\x3E', $twig->render('js', array('bar' => 'foo<br/ >')));
+ }
+
+ public function escapingStrategyCallback($filename)
+ {
+ return $filename;
+ }
+
+ public function testGlobals()
+ {
+ // globals can be added after calling getGlobals
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->addGlobal('foo', 'bar');
+ $globals = $twig->getGlobals();
+ $this->assertEquals('bar', $globals['foo']);
+
+ // globals can be modified after runtime init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->initRuntime();
+ $twig->addGlobal('foo', 'bar');
+ $globals = $twig->getGlobals();
+ $this->assertEquals('bar', $globals['foo']);
+
+ // globals can be modified after extensions init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->getFunctions();
+ $twig->addGlobal('foo', 'bar');
+ $globals = $twig->getGlobals();
+ $this->assertEquals('bar', $globals['foo']);
+
+ // globals can be modified after extensions and runtime init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->getFunctions();
+ $twig->initRuntime();
+ $twig->addGlobal('foo', 'bar');
+ $globals = $twig->getGlobals();
+ $this->assertEquals('bar', $globals['foo']);
+
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->getGlobals();
+ $twig->addGlobal('foo', 'bar');
+ $template = $twig->loadTemplate('{{foo}}');
+ $this->assertEquals('bar', $template->render(array()));
+
+ /* to be uncomment in Twig 2.0
+ // globals cannot be added after runtime init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->initRuntime();
+ try {
+ $twig->addGlobal('bar', 'bar');
+ $this->fail();
+ } catch (LogicException $e) {
+ $this->assertFalse(array_key_exists('bar', $twig->getGlobals()));
+ }
+
+ // globals cannot be added after extensions init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->getFunctions();
+ try {
+ $twig->addGlobal('bar', 'bar');
+ $this->fail();
+ } catch (LogicException $e) {
+ $this->assertFalse(array_key_exists('bar', $twig->getGlobals()));
+ }
+
+ // globals cannot be added after extensions and runtime init
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addGlobal('foo', 'foo');
+ $globals = $twig->getGlobals();
+ $twig->getFunctions();
+ $twig->initRuntime();
+ try {
+ $twig->addGlobal('bar', 'bar');
+ $this->fail();
+ } catch (LogicException $e) {
+ $this->assertFalse(array_key_exists('bar', $twig->getGlobals()));
+ }
+
+ // test adding globals after initRuntime without call to getGlobals
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->initRuntime();
+ try {
+ $twig->addGlobal('bar', 'bar');
+ $this->fail();
+ } catch (LogicException $e) {
+ $this->assertFalse(array_key_exists('bar', $twig->getGlobals()));
+ }
+ */
+ }
+
+ public function testExtensionsAreNotInitializedWhenRenderingACompiledTemplate()
+ {
+ $options = array('cache' => sys_get_temp_dir().'/twig', 'auto_reload' => false, 'debug' => false);
+
+ // force compilation
+ $twig = new Twig_Environment(new Twig_Loader_String(), $options);
+ $cache = $twig->getCacheFilename('{{ foo }}');
+ if (!is_dir(dirname($cache))) {
+ mkdir(dirname($cache), 0777, true);
+ }
+ file_put_contents($cache, $twig->compileSource('{{ foo }}', '{{ foo }}'));
+
+ // check that extensions won't be initialized when rendering a template that is already in the cache
+ $twig = $this
+ ->getMockBuilder('Twig_Environment')
+ ->setConstructorArgs(array(new Twig_Loader_String(), $options))
+ ->setMethods(array('initExtensions'))
+ ->getMock()
+ ;
+
+ $twig->expects($this->never())->method('initExtensions');
+
+ // render template
+ $output = $twig->render('{{ foo }}', array('foo' => 'bar'));
+ $this->assertEquals('bar', $output);
+
+ unlink($cache);
+ }
+
+ public function testAddExtension()
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addExtension(new Twig_Tests_EnvironmentTest_Extension());
+
+ $this->assertArrayHasKey('test', $twig->getTags());
+ $this->assertArrayHasKey('foo_filter', $twig->getFilters());
+ $this->assertArrayHasKey('foo_function', $twig->getFunctions());
+ $this->assertArrayHasKey('foo_test', $twig->getTests());
+ $this->assertArrayHasKey('foo_unary', $twig->getUnaryOperators());
+ $this->assertArrayHasKey('foo_binary', $twig->getBinaryOperators());
+ $this->assertArrayHasKey('foo_global', $twig->getGlobals());
+ $visitors = $twig->getNodeVisitors();
+ $this->assertEquals('Twig_Tests_EnvironmentTest_NodeVisitor', get_class($visitors[2]));
+ }
+
+ public function testRemoveExtension()
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String());
+ $twig->addExtension(new Twig_Tests_EnvironmentTest_Extension());
+ $twig->removeExtension('environment_test');
+
+ $this->assertFalse(array_key_exists('test', $twig->getTags()));
+ $this->assertFalse(array_key_exists('foo_filter', $twig->getFilters()));
+ $this->assertFalse(array_key_exists('foo_function', $twig->getFunctions()));
+ $this->assertFalse(array_key_exists('foo_test', $twig->getTests()));
+ $this->assertFalse(array_key_exists('foo_unary', $twig->getUnaryOperators()));
+ $this->assertFalse(array_key_exists('foo_binary', $twig->getBinaryOperators()));
+ $this->assertFalse(array_key_exists('foo_global', $twig->getGlobals()));
+ $this->assertCount(2, $twig->getNodeVisitors());
+ }
+}
+
+class Twig_Tests_EnvironmentTest_Extension extends Twig_Extension
+{
+ public function getTokenParsers()
+ {
+ return array(
+ new Twig_Tests_EnvironmentTest_TokenParser(),
+ );
+ }
+
+ public function getNodeVisitors()
+ {
+ return array(
+ new Twig_Tests_EnvironmentTest_NodeVisitor(),
+ );
+ }
+
+ public function getFilters()
+ {
+ return array(
+ 'foo_filter' => new Twig_Filter_Function('foo_filter'),
+ );
+ }
+
+ public function getTests()
+ {
+ return array(
+ 'foo_test' => new Twig_Test_Function('foo_test'),
+ );
+ }
+
+ public function getFunctions()
+ {
+ return array(
+ 'foo_function' => new Twig_Function_Function('foo_function'),
+ );
+ }
+
+ public function getOperators()
+ {
+ return array(
+ array('foo_unary' => array()),
+ array('foo_binary' => array()),
+ );
+ }
+
+ public function getGlobals()
+ {
+ return array(
+ 'foo_global' => 'foo_global',
+ );
+ }
+
+ public function getName()
+ {
+ return 'environment_test';
+ }
+}
+
+class Twig_Tests_EnvironmentTest_TokenParser extends Twig_TokenParser
+{
+ public function parse(Twig_Token $token)
+ {
+ }
+
+ public function getTag()
+ {
+ return 'test';
+ }
+}
+
+class Twig_Tests_EnvironmentTest_NodeVisitor implements Twig_NodeVisitorInterface
+{
+ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
+ {
+ return $node;
+ }
+
+ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
+ {
+ return $node;
+ }
+
+ public function getPriority()
+ {
+ return 0;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_ErrorTest extends PHPUnit_Framework_TestCase
+{
+ public function testErrorWithObjectFilename()
+ {
+ $error = new Twig_Error('foo');
+ $error->setTemplateFile(new SplFileInfo(__FILE__));
+
+ $this->assertContains('test'.DIRECTORY_SEPARATOR.'Twig'.DIRECTORY_SEPARATOR.'Tests'.DIRECTORY_SEPARATOR.'ErrorTest.php', $error->getMessage());
+ }
+
+ public function testErrorWithArrayFilename()
+ {
+ $error = new Twig_Error('foo');
+ $error->setTemplateFile(array('foo' => 'bar'));
+
+ $this->assertEquals('foo in {"foo":"bar"}', $error->getMessage());
+ }
+
+ public function testTwigExceptionAddsFileAndLineWhenMissing()
+ {
+ $loader = new Twig_Loader_Array(array('index' => "\n\n{{ foo.bar }}\n\n\n{{ 'foo' }}"));
+ $twig = new Twig_Environment($loader, array('strict_variables' => true, 'debug' => true, 'cache' => false));
+
+ $template = $twig->loadTemplate('index');
+
+ try {
+ $template->render(array());
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('Variable "foo" does not exist in "index" at line 3', $e->getMessage());
+ $this->assertEquals(3, $e->getTemplateLine());
+ $this->assertEquals('index', $e->getTemplateFile());
+ }
+ }
+
+ public function testRenderWrapsExceptions()
+ {
+ $loader = new Twig_Loader_Array(array('index' => "\n\n\n{{ foo.bar }}\n\n\n\n{{ 'foo' }}"));
+ $twig = new Twig_Environment($loader, array('strict_variables' => true, 'debug' => true, 'cache' => false));
+
+ $template = $twig->loadTemplate('index');
+
+ try {
+ $template->render(array('foo' => new Twig_Tests_ErrorTest_Foo()));
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('An exception has been thrown during the rendering of a template ("Runtime error...") in "index" at line 4.', $e->getMessage());
+ $this->assertEquals(4, $e->getTemplateLine());
+ $this->assertEquals('index', $e->getTemplateFile());
+ }
+ }
+
+ public function testTwigExceptionAddsFileAndLineWhenMissingWithInheritance()
+ {
+ $loader = new Twig_Loader_Array(array(
+ 'index' => "{% extends 'base' %}
+ {% block content %}
+ {{ foo.bar }}
+ {% endblock %}
+ {% block foo %}
+ {{ foo.bar }}
+ {% endblock %}",
+ 'base' => '{% block content %}{% endblock %}'
+ ));
+ $twig = new Twig_Environment($loader, array('strict_variables' => true, 'debug' => true, 'cache' => false));
+
+ $template = $twig->loadTemplate('index');
+ try {
+ $template->render(array());
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('Variable "foo" does not exist in "index" at line 3', $e->getMessage());
+ $this->assertEquals(3, $e->getTemplateLine());
+ $this->assertEquals('index', $e->getTemplateFile());
+ }
+
+ try {
+ $template->render(array('foo' => new Twig_Tests_ErrorTest_Foo()));
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('An exception has been thrown during the rendering of a template ("Runtime error...") in "index" at line 3.', $e->getMessage());
+ $this->assertEquals(3, $e->getTemplateLine());
+ $this->assertEquals('index', $e->getTemplateFile());
+ }
+ }
+
+ public function testTwigExceptionAddsFileAndLineWhenMissingWithInheritanceAgain()
+ {
+ $loader = new Twig_Loader_Array(array(
+ 'index' => "{% extends 'base' %}
+ {% block content %}
+ {{ parent() }}
+ {% endblock %}",
+ 'base' => '{% block content %}{{ foo }}{% endblock %}'
+ ));
+ $twig = new Twig_Environment($loader, array('strict_variables' => true, 'debug' => true, 'cache' => false));
+
+ $template = $twig->loadTemplate('index');
+ try {
+ $template->render(array());
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('Variable "foo" does not exist in "base" at line 1', $e->getMessage());
+ $this->assertEquals(1, $e->getTemplateLine());
+ $this->assertEquals('base', $e->getTemplateFile());
+ }
+ }
+
+ public function testTwigExceptionAddsFileAndLineWhenMissingWithInheritanceOnDisk()
+ {
+ $loader = new Twig_Loader_Filesystem(dirname(__FILE__).'/Fixtures/errors');
+ $twig = new Twig_Environment($loader, array('strict_variables' => true, 'debug' => true, 'cache' => false));
+
+ $template = $twig->loadTemplate('index.html');
+ try {
+ $template->render(array());
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('Variable "foo" does not exist in "index.html" at line 3', $e->getMessage());
+ $this->assertEquals(3, $e->getTemplateLine());
+ $this->assertEquals('index.html', $e->getTemplateFile());
+ }
+
+ try {
+ $template->render(array('foo' => new Twig_Tests_ErrorTest_Foo()));
+
+ $this->fail();
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertEquals('An exception has been thrown during the rendering of a template ("Runtime error...") in "index.html" at line 3.', $e->getMessage());
+ $this->assertEquals(3, $e->getTemplateLine());
+ $this->assertEquals('index.html', $e->getTemplateFile());
+ }
+ }
+}
+
+class Twig_Tests_ErrorTest_Foo
+{
+ public function bar()
+ {
+ throw new Exception('Runtime error...');
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_ExpressionParserTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @dataProvider getFailingTestsForAssignment
+ */
+ public function testCanOnlyAssignToNames($template)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize($template, 'index'));
+ }
+
+ public function getFailingTestsForAssignment()
+ {
+ return array(
+ array('{% set false = "foo" %}'),
+ array('{% set true = "foo" %}'),
+ array('{% set none = "foo" %}'),
+ array('{% set 3 = "foo" %}'),
+ array('{% set 1 + 2 = "foo" %}'),
+ array('{% set "bar" = "foo" %}'),
+ array('{% set %}{% endset %}')
+ );
+ }
+
+ /**
+ * @dataProvider getTestsForArray
+ */
+ public function testArrayExpression($template, $expected)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $stream = $env->tokenize($template, 'index');
+ $parser = new Twig_Parser($env);
+
+ $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @dataProvider getFailingTestsForArray
+ */
+ public function testArraySyntaxError($template)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize($template, 'index'));
+ }
+
+ public function getFailingTestsForArray()
+ {
+ return array(
+ array('{{ [1, "a": "b"] }}'),
+ array('{{ {"a": "b", 2} }}'),
+ );
+ }
+
+ public function getTestsForArray()
+ {
+ return array(
+ // simple array
+ array('{{ [1, 2] }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant(0, 1),
+ new Twig_Node_Expression_Constant(1, 1),
+
+ new Twig_Node_Expression_Constant(1, 1),
+ new Twig_Node_Expression_Constant(2, 1),
+ ), 1),
+ ),
+
+ // array with trailing ,
+ array('{{ [1, 2, ] }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant(0, 1),
+ new Twig_Node_Expression_Constant(1, 1),
+
+ new Twig_Node_Expression_Constant(1, 1),
+ new Twig_Node_Expression_Constant(2, 1),
+ ), 1),
+ ),
+
+ // simple hash
+ array('{{ {"a": "b", "b": "c"} }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant('a', 1),
+ new Twig_Node_Expression_Constant('b', 1),
+
+ new Twig_Node_Expression_Constant('b', 1),
+ new Twig_Node_Expression_Constant('c', 1),
+ ), 1),
+ ),
+
+ // hash with trailing ,
+ array('{{ {"a": "b", "b": "c", } }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant('a', 1),
+ new Twig_Node_Expression_Constant('b', 1),
+
+ new Twig_Node_Expression_Constant('b', 1),
+ new Twig_Node_Expression_Constant('c', 1),
+ ), 1),
+ ),
+
+ // hash in an array
+ array('{{ [1, {"a": "b", "b": "c"}] }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant(0, 1),
+ new Twig_Node_Expression_Constant(1, 1),
+
+ new Twig_Node_Expression_Constant(1, 1),
+ new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant('a', 1),
+ new Twig_Node_Expression_Constant('b', 1),
+
+ new Twig_Node_Expression_Constant('b', 1),
+ new Twig_Node_Expression_Constant('c', 1),
+ ), 1),
+ ), 1),
+ ),
+
+ // array in a hash
+ array('{{ {"a": [1, 2], "b": "c"} }}', new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant('a', 1),
+ new Twig_Node_Expression_Array(array(
+ new Twig_Node_Expression_Constant(0, 1),
+ new Twig_Node_Expression_Constant(1, 1),
+
+ new Twig_Node_Expression_Constant(1, 1),
+ new Twig_Node_Expression_Constant(2, 1),
+ ), 1),
+ new Twig_Node_Expression_Constant('b', 1),
+ new Twig_Node_Expression_Constant('c', 1),
+ ), 1),
+ ),
+ );
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ */
+ public function testStringExpressionDoesNotConcatenateTwoConsecutiveStrings()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $stream = $env->tokenize('{{ "a" "b" }}', 'index');
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($stream);
+ }
+
+ /**
+ * @dataProvider getTestsForString
+ */
+ public function testStringExpression($template, $expected)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false, 'optimizations' => 0));
+ $stream = $env->tokenize($template, 'index');
+ $parser = new Twig_Parser($env);
+
+ $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)->getNode('expr'));
+ }
+
+ public function getTestsForString()
+ {
+ return array(
+ array(
+ '{{ "foo" }}', new Twig_Node_Expression_Constant('foo', 1),
+ ),
+ array(
+ '{{ "foo #{bar}" }}', new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Constant('foo ', 1),
+ new Twig_Node_Expression_Name('bar', 1),
+ 1
+ ),
+ ),
+ array(
+ '{{ "foo #{bar} baz" }}', new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Constant('foo ', 1),
+ new Twig_Node_Expression_Name('bar', 1),
+ 1
+ ),
+ new Twig_Node_Expression_Constant(' baz', 1),
+ 1
+ )
+ ),
+
+ array(
+ '{{ "foo #{"foo #{bar} baz"} baz" }}', new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Constant('foo ', 1),
+ new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Binary_Concat(
+ new Twig_Node_Expression_Constant('foo ', 1),
+ new Twig_Node_Expression_Name('bar', 1),
+ 1
+ ),
+ new Twig_Node_Expression_Constant(' baz', 1),
+ 1
+ ),
+ 1
+ ),
+ new Twig_Node_Expression_Constant(' baz', 1),
+ 1
+ ),
+ ),
+ );
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ */
+ public function testAttributeCallDoesNotSupportNamedArguments()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{{ foo.bar(name="Foo") }}', 'index'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ */
+ public function testMacroCallDoesNotSupportNamedArguments()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{% from _self import foo %}{% macro foo() %}{% endmacro %}{{ foo(name="Foo") }}', 'index'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage An argument must be a name. Unexpected token "string" of value "a" ("name" expected) in "index" at line 1
+ */
+ public function testMacroDefinitionDoesNotSupportNonNameVariableName()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{% macro foo("a") %}{% endmacro %}', 'index'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage A default value for an argument must be a constant (a boolean, a string, a number, or an array) in "index" at line 1
+ * @dataProvider getMacroDefinitionDoesNotSupportNonConstantDefaultValues
+ */
+ public function testMacroDefinitionDoesNotSupportNonConstantDefaultValues($template)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize($template, 'index'));
+ }
+
+ public function getMacroDefinitionDoesNotSupportNonConstantDefaultValues()
+ {
+ return array(
+ array('{% macro foo(name = "a #{foo} a") %}{% endmacro %}'),
+ array('{% macro foo(name = [["b", "a #{foo} a"]]) %}{% endmacro %}'),
+ );
+ }
+
+ /**
+ * @dataProvider getMacroDefinitionSupportsConstantDefaultValues
+ */
+ public function testMacroDefinitionSupportsConstantDefaultValues($template)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize($template, 'index'));
+ }
+
+ public function getMacroDefinitionSupportsConstantDefaultValues()
+ {
+ return array(
+ array('{% macro foo(name = "aa") %}{% endmacro %}'),
+ array('{% macro foo(name = 12) %}{% endmacro %}'),
+ array('{% macro foo(name = true) %}{% endmacro %}'),
+ array('{% macro foo(name = ["a"]) %}{% endmacro %}'),
+ array('{% macro foo(name = [["a"]]) %}{% endmacro %}'),
+ array('{% macro foo(name = {a: "a"}) %}{% endmacro %}'),
+ array('{% macro foo(name = {a: {b: "a"}}) %}{% endmacro %}'),
+ );
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage The function "cycl" does not exist. Did you mean "cycle" in "index" at line 1
+ */
+ public function testUnknownFunction()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{{ cycl() }}', 'index'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage The filter "lowe" does not exist. Did you mean "lower" in "index" at line 1
+ */
+ public function testUnknownFilter()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{{ 1|lowe }}', 'index'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage The test "nul" does not exist. Did you mean "null" in "index" at line 1
+ */
+ public function testUnknownTest()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $parser = new Twig_Parser($env);
+
+ $parser->parse($env->tokenize('{{ 1 is nul }}', 'index'));
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Extension_CoreTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getRandomFunctionTestData
+ */
+ public function testRandomFunction($value, $expectedInArray)
+ {
+ $env = new Twig_Environment();
+
+ for ($i = 0; $i < 100; $i++) {
+ $this->assertTrue(in_array(twig_random($env, $value), $expectedInArray, true)); // assertContains() would not consider the type
+ }
+ }
+
+ public function getRandomFunctionTestData()
+ {
+ return array(
+ array( // array
+ array('apple', 'orange', 'citrus'),
+ array('apple', 'orange', 'citrus'),
+ ),
+ array( // Traversable
+ new ArrayObject(array('apple', 'orange', 'citrus')),
+ array('apple', 'orange', 'citrus'),
+ ),
+ array( // unicode string
+ 'Ä€é',
+ array('Ä', '€', 'é'),
+ ),
+ array( // numeric but string
+ '123',
+ array('1', '2', '3'),
+ ),
+ array( // integer
+ 5,
+ range(0, 5, 1),
+ ),
+ array( // float
+ 5.9,
+ range(0, 5, 1),
+ ),
+ array( // negative
+ -2,
+ array(0, -1, -2),
+ ),
+ );
+ }
+
+ public function testRandomFunctionWithoutParameter()
+ {
+ $max = mt_getrandmax();
+
+ for ($i = 0; $i < 100; $i++) {
+ $val = twig_random(new Twig_Environment());
+ $this->assertTrue(is_int($val) && $val >= 0 && $val <= $max);
+ }
+ }
+
+ public function testRandomFunctionReturnsAsIs()
+ {
+ $this->assertSame('', twig_random(new Twig_Environment(), ''));
+ $this->assertSame('', twig_random(new Twig_Environment(null, array('charset' => null)), ''));
+
+ $instance = new stdClass();
+ $this->assertSame($instance, twig_random(new Twig_Environment(), $instance));
+ }
+
+ /**
+ * @expectedException Twig_Error_Runtime
+ */
+ public function testRandomFunctionOfEmptyArrayThrowsException()
+ {
+ twig_random(new Twig_Environment(), array());
+ }
+
+ public function testRandomFunctionOnNonUTF8String()
+ {
+ if (!function_exists('iconv') && !function_exists('mb_convert_encoding')) {
+ $this->markTestSkipped('needs iconv or mbstring');
+ }
+
+ $twig = new Twig_Environment();
+ $twig->setCharset('ISO-8859-1');
+
+ $text = twig_convert_encoding('Äé', 'ISO-8859-1', 'UTF-8');
+ for ($i = 0; $i < 30; $i++) {
+ $rand = twig_random($twig, $text);
+ $this->assertTrue(in_array(twig_convert_encoding($rand, 'UTF-8', 'ISO-8859-1'), array('Ä', 'é'), true));
+ }
+ }
+
+ public function testReverseFilterOnNonUTF8String()
+ {
+ if (!function_exists('iconv') && !function_exists('mb_convert_encoding')) {
+ $this->markTestSkipped('needs iconv or mbstring');
+ }
+
+ $twig = new Twig_Environment();
+ $twig->setCharset('ISO-8859-1');
+
+ $input = twig_convert_encoding('Äé', 'ISO-8859-1', 'UTF-8');
+ $output = twig_convert_encoding(twig_reverse_filter($twig, $input), 'UTF-8', 'ISO-8859-1');
+
+ $this->assertEquals($output, 'éÄ');
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Extension_SandboxTest extends PHPUnit_Framework_TestCase
+{
+ protected static $params, $templates;
+
+ public function setUp()
+ {
+ self::$params = array(
+ 'name' => 'Fabien',
+ 'obj' => new FooObject(),
+ 'arr' => array('obj' => new FooObject()),
+ );
+
+ self::$templates = array(
+ '1_basic1' => '{{ obj.foo }}',
+ '1_basic2' => '{{ name|upper }}',
+ '1_basic3' => '{% if name %}foo{% endif %}',
+ '1_basic4' => '{{ obj.bar }}',
+ '1_basic5' => '{{ obj }}',
+ '1_basic6' => '{{ arr.obj }}',
+ '1_basic7' => '{{ cycle(["foo","bar"], 1) }}',
+ '1_basic8' => '{{ obj.getfoobar }}{{ obj.getFooBar }}',
+ '1_basic' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+ '1_layout' => '{% block content %}{% endblock %}',
+ '1_child' => '{% extends "1_layout" %}{% block content %}{{ "a"|json_encode }}{% endblock %}',
+ );
+ }
+
+ /**
+ * @expectedException Twig_Sandbox_SecurityError
+ * @expectedExceptionMessage Filter "json_encode" is not allowed in "1_child".
+ */
+ public function testSandboxWithInheritance()
+ {
+ $twig = $this->getEnvironment(true, array(), self::$templates, array('block'));
+ $twig->loadTemplate('1_child')->render(array());
+ }
+
+ public function testSandboxGloballySet()
+ {
+ $twig = $this->getEnvironment(false, array(), self::$templates);
+ $this->assertEquals('FOO', $twig->loadTemplate('1_basic')->render(self::$params), 'Sandbox does nothing if it is disabled globally');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic1')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed method is called');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic2')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed filter is called');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic3')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed tag is used in the template');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic4')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed property is called in the template');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic5')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic6')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed method (__toString()) is called in the template');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('1_basic7')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception if an unallowed function is called in the template');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array('FooObject' => 'foo'));
+ FooObject::reset();
+ $this->assertEquals('foo', $twig->loadTemplate('1_basic1')->render(self::$params), 'Sandbox allow some methods');
+ $this->assertEquals(1, FooObject::$called['foo'], 'Sandbox only calls method once');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array('FooObject' => '__toString'));
+ FooObject::reset();
+ $this->assertEquals('foo', $twig->loadTemplate('1_basic5')->render(self::$params), 'Sandbox allow some methods');
+ $this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array('upper'));
+ $this->assertEquals('FABIEN', $twig->loadTemplate('1_basic2')->render(self::$params), 'Sandbox allow some filters');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array('if'));
+ $this->assertEquals('foo', $twig->loadTemplate('1_basic3')->render(self::$params), 'Sandbox allow some tags');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array(), array('FooObject' => 'bar'));
+ $this->assertEquals('bar', $twig->loadTemplate('1_basic4')->render(self::$params), 'Sandbox allow some properties');
+
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array(), array(), array('cycle'));
+ $this->assertEquals('bar', $twig->loadTemplate('1_basic7')->render(self::$params), 'Sandbox allow some functions');
+
+ foreach (array('getfoobar', 'getFoobar', 'getFooBar') as $name) {
+ $twig = $this->getEnvironment(true, array(), self::$templates, array(), array(), array('FooObject' => $name));
+ FooObject::reset();
+ $this->assertEquals('foobarfoobar', $twig->loadTemplate('1_basic8')->render(self::$params), 'Sandbox allow methods in a case-insensitive way');
+ $this->assertEquals(2, FooObject::$called['getFooBar'], 'Sandbox only calls method once');
+ }
+ }
+
+ public function testSandboxLocallySetForAnInclude()
+ {
+ self::$templates = array(
+ '2_basic' => '{{ obj.foo }}{% include "2_included" %}{{ obj.foo }}',
+ '2_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+ );
+
+ $twig = $this->getEnvironment(false, array(), self::$templates);
+ $this->assertEquals('fooFOOfoo', $twig->loadTemplate('2_basic')->render(self::$params), 'Sandbox does nothing if disabled globally and sandboxed not used for the include');
+
+ self::$templates = array(
+ '3_basic' => '{{ obj.foo }}{% sandbox %}{% include "3_included" %}{% endsandbox %}{{ obj.foo }}',
+ '3_included' => '{% if obj.foo %}{{ obj.foo|upper }}{% endif %}',
+ );
+
+ $twig = $this->getEnvironment(true, array(), self::$templates);
+ try {
+ $twig->loadTemplate('3_basic')->render(self::$params);
+ $this->fail('Sandbox throws a SecurityError exception when the included file is sandboxed');
+ } catch (Twig_Sandbox_SecurityError $e) {
+ }
+ }
+
+ public function testMacrosInASandbox()
+ {
+ $twig = $this->getEnvironment(true, array('autoescape' => true), array('index' => <<<EOF
+{%- import _self as macros %}
+
+{%- macro test(text) %}<p>{{ text }}</p>{% endmacro %}
+
+{{- macros.test('username') }}
+EOF
+ ), array('macro', 'import'), array('escape'));
+
+ $this->assertEquals('<p>username</p>', $twig->loadTemplate('index')->render(array()));
+ }
+
+ protected function getEnvironment($sandboxed, $options, $templates, $tags = array(), $filters = array(), $methods = array(), $properties = array(), $functions = array())
+ {
+ $loader = new Twig_Loader_Array($templates);
+ $twig = new Twig_Environment($loader, array_merge(array('debug' => true, 'cache' => false, 'autoescape' => false), $options));
+ $policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods, $properties, $functions);
+ $twig->addExtension(new Twig_Extension_Sandbox($policy, $sandboxed));
+
+ return $twig;
+ }
+}
+
+class FooObject
+{
+ public static $called = array('__toString' => 0, 'foo' => 0, 'getFooBar' => 0);
+
+ public $bar = 'bar';
+
+ public static function reset()
+ {
+ self::$called = array('__toString' => 0, 'foo' => 0, 'getFooBar' => 0);
+ }
+
+ public function __toString()
+ {
+ ++self::$called['__toString'];
+
+ return 'foo';
+ }
+
+ public function foo()
+ {
+ ++self::$called['foo'];
+
+ return 'foo';
+ }
+
+ public function getFooBar()
+ {
+ ++self::$called['getFooBar'];
+
+ return 'foobar';
+ }
+}
--- /dev/null
+<?php
+
+class Twig_Tests_FileCachingTest extends PHPUnit_Framework_TestCase
+{
+ protected $fileName;
+ protected $env;
+ protected $tmpDir;
+
+ public function setUp()
+ {
+ $this->tmpDir = sys_get_temp_dir().'/TwigTests';
+ if (!file_exists($this->tmpDir)) {
+ @mkdir($this->tmpDir, 0777, true);
+ }
+
+ if (!is_writable($this->tmpDir)) {
+ $this->markTestSkipped(sprintf('Unable to run the tests as "%s" is not writable.', $this->tmpDir));
+ }
+
+ $this->env = new Twig_Environment(new Twig_Loader_String(), array('cache' => $this->tmpDir));
+ }
+
+ public function tearDown()
+ {
+ if ($this->fileName) {
+ unlink($this->fileName);
+ }
+
+ $this->removeDir($this->tmpDir);
+ }
+
+ public function testWritingCacheFiles()
+ {
+ $name = 'This is just text.';
+ $template = $this->env->loadTemplate($name);
+ $cacheFileName = $this->env->getCacheFilename($name);
+
+ $this->assertTrue(file_exists($cacheFileName), 'Cache file does not exist.');
+ $this->fileName = $cacheFileName;
+ }
+
+ public function testClearingCacheFiles()
+ {
+ $name = 'I will be deleted.';
+ $template = $this->env->loadTemplate($name);
+ $cacheFileName = $this->env->getCacheFilename($name);
+
+ $this->assertTrue(file_exists($cacheFileName), 'Cache file does not exist.');
+ $this->env->clearCacheFiles();
+ $this->assertFalse(file_exists($cacheFileName), 'Cache file was not cleared.');
+ }
+
+ private function removeDir($target)
+ {
+ $fp = opendir($target);
+ while (false !== $file = readdir($fp)) {
+ if (in_array($file, array('.', '..'))) {
+ continue;
+ }
+
+ if (is_dir($target.'/'.$file)) {
+ self::removeDir($target.'/'.$file);
+ } else {
+ unlink($target.'/'.$file);
+ }
+ }
+ closedir($fp);
+ rmdir($target);
+ }
+}
--- /dev/null
+{% block content %}{% endblock %}
--- /dev/null
+{% extends 'base.html' %}
+{% block content %}
+ {{ foo.bar }}
+{% endblock %}
+{% block foo %}
+ {{ foo.bar }}
+{% endblock %}
--- /dev/null
+--TEST--
+Exception for an unclosed tag
+--TEMPLATE--
+{% block foo %}
+ {% if foo %}
+
+
+
+
+ {% for i in fo %}
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
+--EXCEPTION--
+Twig_Error_Syntax: Unexpected tag name "endblock" (expecting closing tag for the "if" tag defined near line 4) in "index.twig" at line 16
--- /dev/null
+--TEST--
+Twig supports array notation
+--TEMPLATE--
+{# empty array #}
+{{ []|join(',') }}
+
+{{ [1, 2]|join(',') }}
+{{ ['foo', "bar"]|join(',') }}
+{{ {0: 1, 'foo': 'bar'}|join(',') }}
+{{ {0: 1, 'foo': 'bar'}|keys|join(',') }}
+
+{{ {0: 1, foo: 'bar'}|join(',') }}
+{{ {0: 1, foo: 'bar'}|keys|join(',') }}
+
+{# nested arrays #}
+{% set a = [1, 2, [1, 2], {'foo': {'foo': 'bar'}}] %}
+{{ a[2]|join(',') }}
+{{ a[3]["foo"]|join(',') }}
+
+{# works even if [] is used inside the array #}
+{{ [foo[bar]]|join(',') }}
+
+{# elements can be any expression #}
+{{ ['foo'|upper, bar|upper, bar == foo]|join(',') }}
+
+{# arrays can have a trailing , like in PHP #}
+{{
+ [
+ 1,
+ 2,
+ ]|join(',')
+}}
+
+{# keys can be any expression #}
+{% set a = 1 %}
+{% set b = "foo" %}
+{% set ary = { (a): 'a', (b): 'b', 'c': 'c', (a ~ b): 'd' } %}
+{{ ary|keys|join(',') }}
+{{ ary|join(',') }}
+--DATA--
+return array('bar' => 'bar', 'foo' => array('bar' => 'bar'))
+--EXPECT--
+1,2
+foo,bar
+1,bar
+0,foo
+
+1,bar
+0,foo
+
+1,2
+bar
+
+bar
+
+FOO,BAR,
+
+1,2
+
+1,foo,c,1foo
+a,b,c,d
--- /dev/null
+--TEST--
+Twig supports method calls
+--TEMPLATE--
+{{ items.foo }}
+{{ items['foo'] }}
+{{ items[foo] }}
+{{ items[items[foo]] }}
+--DATA--
+return array('foo' => 'bar', 'items' => array('foo' => 'bar', 'bar' => 'foo'))
+--EXPECT--
+bar
+bar
+foo
+bar
--- /dev/null
+--TEST--
+Twig supports binary operations (+, -, *, /, ~, %, and, or)
+--TEMPLATE--
+{{ 1 + 1 }}
+{{ 2 - 1 }}
+{{ 2 * 2 }}
+{{ 2 / 2 }}
+{{ 3 % 2 }}
+{{ 1 and 1 }}
+{{ 1 and 0 }}
+{{ 0 and 1 }}
+{{ 0 and 0 }}
+{{ 1 or 1 }}
+{{ 1 or 0 }}
+{{ 0 or 1 }}
+{{ 0 or 0 }}
+{{ 0 or 1 and 0 }}
+{{ 1 or 0 and 1 }}
+{{ "foo" ~ "bar" }}
+{{ foo ~ "bar" }}
+{{ "foo" ~ bar }}
+{{ foo ~ bar }}
+{{ 20 // 7 }}
+--DATA--
+return array('foo' => 'bar', 'bar' => 'foo')
+--EXPECT--
+2
+1
+4
+1
+1
+1
+
+
+
+1
+1
+1
+
+
+1
+foobar
+barbar
+foofoo
+barfoo
+2
--- /dev/null
+--TEST--
+Twig supports bitwise operations
+--TEMPLATE--
+{{ 1 b-and 5 }}
+{{ 1 b-or 5 }}
+{{ 1 b-xor 5 }}
+{{ (1 and 0 b-or 0) is sameas(1 and (0 b-or 0)) ? 'ok' : 'ko' }}
+--DATA--
+return array()
+--EXPECT--
+1
+5
+4
+ok
--- /dev/null
+--TEST--
+Twig supports comparison operators (==, !=, <, >, >=, <=)
+--TEMPLATE--
+{{ 1 > 2 }}/{{ 1 > 1 }}/{{ 1 >= 2 }}/{{ 1 >= 1 }}
+{{ 1 < 2 }}/{{ 1 < 1 }}/{{ 1 <= 2 }}/{{ 1 <= 1 }}
+{{ 1 == 1 }}/{{ 1 == 2 }}
+{{ 1 != 1 }}/{{ 1 != 2 }}
+--DATA--
+return array()
+--EXPECT--
+///1
+1//1/1
+1/
+/1
--- /dev/null
+--TEST--
+Twig supports the .. operator
+--TEMPLATE--
+{% for i in 0..10 %}{{ i }} {% endfor %}
+
+{% for letter in 'a'..'z' %}{{ letter }} {% endfor %}
+
+{% for letter in 'a'|upper..'z'|upper %}{{ letter }} {% endfor %}
+
+{% for i in foo[0]..foo[1] %}{{ i }} {% endfor %}
+
+{% for i in 0 + 1 .. 10 - 1 %}{{ i }} {% endfor %}
+--DATA--
+return array('foo' => array(1, 10))
+--EXPECT--
+0 1 2 3 4 5 6 7 8 9 10
+a b c d e f g h i j k l m n o p q r s t u v w x y z
+A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
+1 2 3 4 5 6 7 8 9 10
+1 2 3 4 5 6 7 8 9
--- /dev/null
+--TEST--
+Twig supports grouping of expressions
+--TEMPLATE--
+{{ (2 + 2) / 2 }}
+--DATA--
+return array()
+--EXPECT--
+2
--- /dev/null
+--TEST--
+Twig supports literals
+--TEMPLATE--
+1 {{ true }}
+2 {{ TRUE }}
+3 {{ false }}
+4 {{ FALSE }}
+5 {{ none }}
+6 {{ NONE }}
+7 {{ null }}
+8 {{ NULL }}
+--DATA--
+return array()
+--EXPECT--
+1 1
+2 1
+3
+4
+5
+6
+7
+8
--- /dev/null
+--TEST--
+Twig supports __call() for attributes
+--TEMPLATE--
+{{ foo.foo }}
+{{ foo.bar }}
+--DATA--
+class TestClassForMagicCallAttributes
+{
+ public function getBar()
+ {
+ return 'bar_from_getbar';
+ }
+
+ public function __call($method, $arguments)
+ {
+ if ('foo' === $method)
+ {
+ return 'foo_from_call';
+ }
+
+ return false;
+ }
+}
+return array('foo' => new TestClassForMagicCallAttributes())
+--EXPECT--
+foo_from_call
+bar_from_getbar
--- /dev/null
+--TEST--
+Twig supports method calls
+--TEMPLATE--
+{{ items.foo.foo }}
+{{ items.foo.getFoo() }}
+{{ items.foo.bar }}
+{{ items.foo['bar'] }}
+{{ items.foo.bar('a', 43) }}
+{{ items.foo.bar(foo) }}
+{{ items.foo.self.foo() }}
+{{ items.foo.is }}
+{{ items.foo.in }}
+{{ items.foo.not }}
+--DATA--
+return array('foo' => 'bar', 'items' => array('foo' => new TwigTestFoo(), 'bar' => 'foo'))
+--CONFIG--
+return array('strict_variables' => false)
+--EXPECT--
+foo
+foo
+bar
+
+bar_a-43
+bar_bar
+foo
+is
+in
+not
--- /dev/null
+--TEST--
+Twig parses postfix expressions
+--TEMPLATE--
+{% import _self as macros %}
+
+{% macro foo() %}foo{% endmacro %}
+
+{{ 'a' }}
+{{ 'a'|upper }}
+{{ ('a')|upper }}
+{{ -1|upper }}
+{{ macros.foo() }}
+{{ (macros).foo() }}
+--DATA--
+return array();
+--EXPECT--
+a
+A
+A
+-1
+foo
+foo
--- /dev/null
+--TEST--
+Twig supports string interpolation
+--TEMPLATE--
+{{ "foo #{"foo #{bar} baz"} baz" }}
+{{ "foo #{bar}#{bar} baz" }}
+--DATA--
+return array('bar' => 'BAR');
+--EXPECT--
+foo foo BAR baz baz
+foo BARBAR baz
--- /dev/null
+--TEST--
+Twig supports the ternary operator
+--TEMPLATE--
+{{ 1 ? 'YES' : 'NO' }}
+{{ 0 ? 'YES' : 'NO' }}
+{{ 0 ? 'YES' : (1 ? 'YES1' : 'NO1') }}
+{{ 0 ? 'YES' : (0 ? 'YES1' : 'NO1') }}
+{{ 1 == 1 ? 'foo<br />':'' }}
+{{ foo ~ (bar ? ('-' ~ bar) : '') }}
+--DATA--
+return array('foo' => 'foo', 'bar' => 'bar')
+--EXPECT--
+YES
+NO
+YES1
+NO1
+foo<br />
+foo-bar
--- /dev/null
+--TEST--
+Twig supports the ternary operator
+--TEMPLATE--
+{{ 1 ? 'YES' }}
+{{ 0 ? 'YES' }}
+--DATA--
+return array()
+--EXPECT--
+YES
+
--- /dev/null
+--TEST--
+Twig supports the ternary operator
+--TEMPLATE--
+{{ 'YES' ?: 'NO' }}
+{{ 0 ?: 'NO' }}
+--DATA--
+return array()
+--EXPECT--
+YES
+NO
--- /dev/null
+--TEST--
+Twig supports unary operators (not, -, +)
+--TEMPLATE--
+{{ not 1 }}/{{ not 0 }}
+{{ +1 + 1 }}/{{ -1 - 1 }}
+{{ not (false or true) }}
+--DATA--
+return array()
+--EXPECT--
+/1
+2/-2
+
--- /dev/null
+--TEST--
+Twig unary operators precedence
+--TEMPLATE--
+{{ -1 - 1 }}
+{{ -1 - -1 }}
+{{ -1 * -1 }}
+{{ 4 / -1 * 5 }}
+--DATA--
+return array()
+--EXPECT--
+-2
+0
+1
+-20
--- /dev/null
+--TEST--
+"abs" filter
+--TEMPLATE--
+{{ (-5.5)|abs }}
+{{ (-5)|abs }}
+{{ (-0)|abs }}
+{{ 0|abs }}
+{{ 5|abs }}
+{{ 5.5|abs }}
+{{ number1|abs }}
+{{ number2|abs }}
+{{ number3|abs }}
+{{ number4|abs }}
+{{ number5|abs }}
+{{ number6|abs }}
+--DATA--
+return array('number1' => -5.5, 'number2' => -5, 'number3' => -0, 'number4' => 0, 'number5' => 5, 'number6' => 5.5)
+--EXPECT--
+5.5
+5
+0
+0
+5
+5.5
+5.5
+5
+0
+0
+5
+5.5
--- /dev/null
+--TEST--
+"batch" filter
+--TEMPLATE--
+{% for row in items|batch(3) %}
+ <div class=row>
+ {% for column in row %}
+ <div class=item>{{ column }}</div>
+ {% endfor %}
+ </div>
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'))
+--EXPECT--
+<div class=row>
+ <div class=item>a</div>
+ <div class=item>b</div>
+ <div class=item>c</div>
+ </div>
+ <div class=row>
+ <div class=item>d</div>
+ <div class=item>e</div>
+ <div class=item>f</div>
+ </div>
+ <div class=row>
+ <div class=item>g</div>
+ <div class=item>h</div>
+ <div class=item>i</div>
+ </div>
+ <div class=row>
+ <div class=item>j</div>
+ </div>
--- /dev/null
+--TEST--
+"batch" filter
+--TEMPLATE--
+{% for row in items|batch(3.1) %}
+ <div class=row>
+ {% for column in row %}
+ <div class=item>{{ column }}</div>
+ {% endfor %}
+ </div>
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'))
+--EXPECT--
+<div class=row>
+ <div class=item>a</div>
+ <div class=item>b</div>
+ <div class=item>c</div>
+ </div>
+ <div class=row>
+ <div class=item>d</div>
+ <div class=item>e</div>
+ <div class=item>f</div>
+ </div>
+ <div class=row>
+ <div class=item>g</div>
+ <div class=item>h</div>
+ <div class=item>i</div>
+ </div>
+ <div class=row>
+ <div class=item>j</div>
+ </div>
--- /dev/null
+--TEST--
+"batch" filter
+--TEMPLATE--
+<table>
+{% for row in items|batch(3, '') %}
+ <tr>
+ {% for column in row %}
+ <td>{{ column }}</td>
+ {% endfor %}
+ </tr>
+{% endfor %}
+</table>
+--DATA--
+return array('items' => array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'))
+--EXPECT--
+<table>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td>e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ <tr>
+ <td>j</td>
+ <td></td>
+ <td></td>
+ </tr>
+</table>
--- /dev/null
+--TEST--
+"batch" filter
+--TEMPLATE--
+<table>
+{% for row in items|batch(3, 'fill') %}
+ <tr>
+ {% for column in row %}
+ <td>{{ column }}</td>
+ {% endfor %}
+ </tr>
+{% endfor %}
+</table>
+--DATA--
+return array('items' => array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'))
+--EXPECT--
+<table>
+ <tr>
+ <td>a</td>
+ <td>b</td>
+ <td>c</td>
+ </tr>
+ <tr>
+ <td>d</td>
+ <td>e</td>
+ <td>f</td>
+ </tr>
+ <tr>
+ <td>g</td>
+ <td>h</td>
+ <td>i</td>
+ </tr>
+ <tr>
+ <td>j</td>
+ <td>fill</td>
+ <td>fill</td>
+ </tr>
+</table>
--- /dev/null
+--TEST--
+"convert_encoding" filter
+--CONDITION--
+function_exists('iconv') || function_exists('mb_convert_encoding')
+--TEMPLATE--
+{{ "愛していますか?"|convert_encoding('ISO-2022-JP', 'UTF-8')|convert_encoding('UTF-8', 'ISO-2022-JP') }}
+--DATA--
+return array()
+--EXPECT--
+愛していますか?
--- /dev/null
+--TEST--
+"date" filter
+--TEMPLATE--
+{{ date1|date }}
+{{ date1|date('d/m/Y') }}
+{{ date1|date('d/m/Y H:i:s', 'Asia/Hong_Kong') }}
+{{ date1|date('d/m/Y H:i:s P', 'Asia/Hong_Kong') }}
+{{ date1|date('d/m/Y H:i:s P', 'America/Chicago') }}
+{{ date1|date('e') }}
+{{ date1|date('d/m/Y H:i:s') }}
+
+{{ date2|date }}
+{{ date2|date('d/m/Y') }}
+{{ date2|date('d/m/Y H:i:s', 'Asia/Hong_Kong') }}
+{{ date2|date('d/m/Y H:i:s', timezone1) }}
+{{ date2|date('d/m/Y H:i:s') }}
+
+{{ date3|date }}
+{{ date3|date('d/m/Y') }}
+
+{{ date4|date }}
+{{ date4|date('d/m/Y') }}
+
+{{ date5|date }}
+{{ date5|date('d/m/Y') }}
+
+{{ date6|date('d/m/Y H:i:s P', 'Europe/Paris') }}
+{{ date6|date('d/m/Y H:i:s P', 'Asia/Hong_Kong') }}
+{{ date6|date('d/m/Y H:i:s P', false) }}
+{{ date6|date('e', 'Europe/Paris') }}
+{{ date6|date('e', false) }}
+
+{{ date7|date }}
+--DATA--
+date_default_timezone_set('Europe/Paris');
+return array(
+ 'date1' => mktime(13, 45, 0, 10, 4, 2010),
+ 'date2' => new DateTime('2010-10-04 13:45'),
+ 'date3' => '2010-10-04 13:45',
+ 'date4' => 1286199900, // DateTime::createFromFormat('Y-m-d H:i', '2010-10-04 13:45', new DateTimeZone('UTC'))->getTimestamp() -- A unixtimestamp is always GMT
+ 'date5' => -189291360, // DateTime::createFromFormat('Y-m-d H:i', '1964-01-02 03:04', new DateTimeZone('UTC'))->getTimestamp(),
+ 'date6' => new DateTime('2010-10-04 13:45', new DateTimeZone('America/New_York')),
+ 'date7' => '2010-01-28T15:00:00+05:00',
+ 'timezone1' => new DateTimeZone('America/New_York'),
+)
+--EXPECT--
+October 4, 2010 13:45
+04/10/2010
+04/10/2010 19:45:00
+04/10/2010 19:45:00 +08:00
+04/10/2010 06:45:00 -05:00
+Europe/Paris
+04/10/2010 13:45:00
+
+October 4, 2010 13:45
+04/10/2010
+04/10/2010 19:45:00
+04/10/2010 07:45:00
+04/10/2010 13:45:00
+
+October 4, 2010 13:45
+04/10/2010
+
+October 4, 2010 15:45
+04/10/2010
+
+January 2, 1964 04:04
+02/01/1964
+
+04/10/2010 19:45:00 +02:00
+05/10/2010 01:45:00 +08:00
+04/10/2010 13:45:00 -04:00
+Europe/Paris
+America/New_York
+
+January 28, 2010 11:00
--- /dev/null
+--TEST--
+"date" filter
+--TEMPLATE--
+{{ date1|date }}
+{{ date1|date('d/m/Y') }}
+--DATA--
+date_default_timezone_set('UTC');
+$twig->getExtension('core')->setDateFormat('Y-m-d', '%d days %h hours');
+return array(
+ 'date1' => mktime(13, 45, 0, 10, 4, 2010),
+)
+--EXPECT--
+2010-10-04
+04/10/2010
--- /dev/null
+--TEST--
+"date" filter (interval support as of PHP 5.3)
+--CONDITION--
+version_compare(phpversion(), '5.3.0', '>=')
+--TEMPLATE--
+{{ date2|date }}
+{{ date2|date('%d days') }}
+--DATA--
+date_default_timezone_set('UTC');
+$twig->getExtension('core')->setDateFormat('Y-m-d', '%d days %h hours');
+return array(
+ 'date2' => new DateInterval('P2D'),
+)
+--EXPECT--
+2 days 0 hours
+2 days
--- /dev/null
+--TEST--
+"date" filter (interval support as of PHP 5.3)
+--CONDITION--
+version_compare(phpversion(), '5.3.0', '>=')
+--TEMPLATE--
+{{ date1|date }}
+{{ date1|date('%d days %h hours') }}
+{{ date1|date('%d days %h hours', timezone1) }}
+--DATA--
+date_default_timezone_set('UTC');
+return array(
+ 'date1' => new DateInterval('P2D'),
+ // This should have no effect on DateInterval formatting
+ 'timezone1' => new DateTimeZone('America/New_York'),
+)
+--EXPECT--
+2 days
+2 days 0 hours
+2 days 0 hours
--- /dev/null
+--TEST--
+"date_modify" filter
+--TEMPLATE--
+{{ date1|date_modify('-1day')|date('Y-m-d H:i:s') }}
+{{ date2|date_modify('-1day')|date('Y-m-d H:i:s') }}
+--DATA--
+date_default_timezone_set('UTC');
+return array(
+ 'date1' => '2010-10-04 13:45',
+ 'date2' => new DateTime('2010-10-04 13:45'),
+)
+--EXPECT--
+2010-10-03 13:45:00
+2010-10-03 13:45:00
--- /dev/null
+--TEST--
+"date" filter
+--TEMPLATE--
+{{ date|date(format='d/m/Y H:i:s P', timezone='America/Chicago') }}
+{{ date|date(timezone='America/Chicago', format='d/m/Y H:i:s P') }}
+{{ date|date('d/m/Y H:i:s P', timezone='America/Chicago') }}
+--DATA--
+date_default_timezone_set('UTC');
+return array('date' => mktime(13, 45, 0, 10, 4, 2010))
+--EXPECT--
+04/10/2010 08:45:00 -05:00
+04/10/2010 08:45:00 -05:00
+04/10/2010 08:45:00 -05:00
--- /dev/null
+--TEST--
+"default" filter
+--TEMPLATE--
+Variable:
+{{ definedVar |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ zeroVar |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ emptyVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ nullVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ undefinedVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+Array access:
+{{ nested.definedVar |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ nested['definedVar'] |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ nested.zeroVar |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ nested.emptyVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ nested.nullVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ nested.undefinedVar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ nested['undefinedVar'] |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ undefinedVar.foo |default('default') is sameas('default') ? 'ok' : 'ko' }}
+Plain values:
+{{ 'defined' |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ 0 |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ '' |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ null |default('default') is sameas('default') ? 'ok' : 'ko' }}
+Precedence:
+{{ 'o' ~ nullVar |default('k') }}
+{{ 'o' ~ nested.nullVar |default('k') }}
+Object methods:
+{{ object.foo |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ object.undefinedMethod |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ object.getFoo() |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ object.getFoo('a') |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ object.undefinedMethod() |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ object.undefinedMethod('a') |default('default') is sameas('default') ? 'ok' : 'ko' }}
+Deep nested:
+{{ nested.undefinedVar.foo.bar |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ nested.definedArray.0 |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ nested['definedArray'][0] |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ object.self.foo |default('default') is sameas('default') ? 'ko' : 'ok' }}
+{{ object.self.undefinedMethod |default('default') is sameas('default') ? 'ok' : 'ko' }}
+{{ object.undefinedMethod.self |default('default') is sameas('default') ? 'ok' : 'ko' }}
+--DATA--
+return array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'emptyVar' => '',
+ 'nullVar' => null,
+ 'nested' => array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'emptyVar' => '',
+ 'nullVar' => null,
+ 'definedArray' => array(0),
+ ),
+ 'object' => new TwigTestFoo(),
+)
+--CONFIG--
+return array('strict_variables' => false)
+--EXPECT--
+Variable:
+ok
+ok
+ok
+ok
+ok
+Array access:
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+Plain values:
+ok
+ok
+ok
+ok
+Precedence:
+ok
+ok
+Object methods:
+ok
+ok
+ok
+ok
+ok
+ok
+Deep nested:
+ok
+ok
+ok
+ok
+ok
+ok
+--DATA--
+return array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'emptyVar' => '',
+ 'nullVar' => null,
+ 'nested' => array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'emptyVar' => '',
+ 'nullVar' => null,
+ 'definedArray' => array(0),
+ ),
+ 'object' => new TwigTestFoo(),
+)
+--CONFIG--
+return array('strict_variables' => true)
+--EXPECT--
+Variable:
+ok
+ok
+ok
+ok
+ok
+Array access:
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+Plain values:
+ok
+ok
+ok
+ok
+Precedence:
+ok
+ok
+Object methods:
+ok
+ok
+ok
+ok
+ok
+ok
+Deep nested:
+ok
+ok
+ok
+ok
+ok
+ok
--- /dev/null
+--TEST--
+dynamic filter
+--TEMPLATE--
+{{ 'bar'|foo_path }}
+{{ 'bar'|a_foo_b_bar }}
+--DATA--
+return array()
+--EXPECT--
+foo/bar
+a/b/bar
--- /dev/null
+--TEST--
+"escape" filter
+--TEMPLATE--
+{{ "foo <br />"|e }}
+--DATA--
+return array()
+--EXPECT--
+foo <br />
--- /dev/null
+--TEST--
+"escape" filter
+--TEMPLATE--
+{{ "愛していますか? <br />"|e }}
+--DATA--
+return array()
+--EXPECT--
+愛していますか? <br />
--- /dev/null
+--TEST--
+"first" filter
+--TEMPLATE--
+{{ [1, 2, 3, 4]|first }}
+{{ {a: 1, b: 2, c: 3, d: 4}|first }}
+{{ '1234'|first }}
+{{ arr|first }}
+--DATA--
+return array('arr' => new ArrayObject(array(1, 2, 3, 4)))
+--EXPECT--
+1
+1
+1
+1
--- /dev/null
+--TEST--
+"escape" filter
+--TEMPLATE--
+{% set foo %}
+ foo<br />
+{% endset %}
+
+{{ foo|e('html') -}}
+{{ foo|e('js') }}
+{% autoescape true %}
+ {{ foo }}
+{% endautoescape %}
+--DATA--
+return array()
+--EXPECT--
+ foo<br />
+\x20\x20\x20\x20foo\x3Cbr\x20\x2F\x3E\x0A
+ foo<br />
--- /dev/null
+--TEST--
+"format" filter
+--TEMPLATE--
+{{ string|format(foo, 3) }}
+--DATA--
+return array('string' => '%s/%d', 'foo' => 'bar')
+--EXPECT--
+bar/3
--- /dev/null
+--TEST--
+"join" filter
+--TEMPLATE--
+{{ ["foo", "bar"]|join(', ') }}
+{{ foo|join(', ') }}
+{{ bar|join(', ') }}
+--DATA--
+return array('foo' => new TwigTestFoo(), 'bar' => new ArrayObject(array(3, 4)))
+--EXPECT--
+foo, bar
+1, 2
+3, 4
--- /dev/null
+--TEST--
+"json_encode" filter
+--TEMPLATE--
+{{ "foo"|json_encode|raw }}
+{{ foo|json_encode|raw }}
+{{ [foo, "foo"]|json_encode|raw }}
+--DATA--
+return array('foo' => new Twig_Markup('foo', 'UTF-8'))
+--EXPECT--
+"foo"
+"foo"
+["foo","foo"]
--- /dev/null
+--TEST--
+"last" filter
+--TEMPLATE--
+{{ [1, 2, 3, 4]|last }}
+{{ {a: 1, b: 2, c: 3, d: 4}|last }}
+{{ '1234'|last }}
+{{ arr|last }}
+--DATA--
+return array('arr' => new ArrayObject(array(1, 2, 3, 4)))
+--EXPECT--
+4
+4
+4
+4
--- /dev/null
+--TEST--
+"length" filter
+--TEMPLATE--
+{{ array|length }}
+{{ string|length }}
+{{ number|length }}
+{{ markup|length }}
+--DATA--
+return array('array' => array(1, 4), 'string' => 'foo', 'number' => 1000, 'markup' => new Twig_Markup('foo', 'UTF-8'))
+--EXPECT--
+2
+3
+4
+3
--- /dev/null
+--TEST--
+"length" filter
+--CONDITION--
+function_exists('mb_get_info')
+--TEMPLATE--
+{{ string|length }}
+{{ markup|length }}
+--DATA--
+return array('string' => 'été', 'markup' => new Twig_Markup('foo', 'UTF-8'))
+--EXPECT--
+3
+3
--- /dev/null
+--TEST--
+"merge" filter
+--TEMPLATE--
+{{ items|merge({'bar': 'foo'})|join }}
+{{ items|merge({'bar': 'foo'})|keys|join }}
+{{ {'bar': 'foo'}|merge(items)|join }}
+{{ {'bar': 'foo'}|merge(items)|keys|join }}
+{{ numerics|merge([4, 5, 6])|join }}
+--DATA--
+return array('items' => array('foo' => 'bar'), 'numerics' => array(1, 2, 3))
+--EXPECT--
+barfoo
+foobar
+foobar
+barfoo
+123456
--- /dev/null
+--TEST--
+"nl2br" filter
+--TEMPLATE--
+{{ "I like Twig.\nYou will like it too.\n\nEverybody like it!"|nl2br }}
+{{ text|nl2br }}
+--DATA--
+return array('text' => "If you have some <strong>HTML</strong>\nit will be escaped.")
+--EXPECT--
+I like Twig.<br />
+You will like it too.<br />
+<br />
+Everybody like it!
+If you have some <strong>HTML</strong><br />
+it will be escaped.
--- /dev/null
+--TEST--
+"number_format" filter
+--TEMPLATE--
+{{ 20|number_format }}
+{{ 20.25|number_format }}
+{{ 20.25|number_format(2) }}
+{{ 20.25|number_format(2, ',') }}
+{{ 1020.25|number_format(2, ',') }}
+{{ 1020.25|number_format(2, ',', '.') }}
+--DATA--
+return array();
+--EXPECT--
+20
+20
+20.25
+20,25
+1,020,25
+1.020,25
--- /dev/null
+--TEST--
+"number_format" filter with defaults.
+--TEMPLATE--
+{{ 20|number_format }}
+{{ 20.25|number_format }}
+{{ 20.25|number_format(1) }}
+{{ 20.25|number_format(2, ',') }}
+{{ 1020.25|number_format }}
+{{ 1020.25|number_format(2, ',') }}
+{{ 1020.25|number_format(2, ',', '.') }}
+--DATA--
+$twig->getExtension('core')->setNumberFormat(2, '!', '=');
+return array();
+--EXPECT--
+20!00
+20!25
+20!3
+20,25
+1=020!25
+1=020,25
+1.020,25
--- /dev/null
+--TEST--
+"replace" filter
+--TEMPLATE--
+{{ "I like %this% and %that%."|replace({'%this%': "foo", '%that%': "bar"}) }}
+--DATA--
+return array()
+--EXPECT--
+I like foo and bar.
--- /dev/null
+--TEST--
+"reverse" filter
+--TEMPLATE--
+{{ [1, 2, 3, 4]|reverse|join('') }}
+{{ '1234évènement'|reverse }}
+{{ arr|reverse|join('') }}
+{{ {'a': 'c', 'b': 'a'}|reverse()|join(',') }}
+{{ {'a': 'c', 'b': 'a'}|reverse(preserveKeys=true)|join(glue=',') }}
+{{ {'a': 'c', 'b': 'a'}|reverse(preserve_keys=true)|join(glue=',') }}
+--DATA--
+return array('arr' => new ArrayObject(array(1, 2, 3, 4)))
+--EXPECT--
+4321
+tnemenèvé4321
+4321
+a,c
+a,c
+a,c
--- /dev/null
+--TEST--
+"slice" filter
+--TEMPLATE--
+{{ [1, 2, 3, 4][1:2]|join('') }}
+{{ {a: 1, b: 2, c: 3, d: 4}[1:2]|join('') }}
+{{ [1, 2, 3, 4][start:length]|join('') }}
+{{ [1, 2, 3, 4]|slice(1, 2)|join('') }}
+{{ [1, 2, 3, 4]|slice(1, 2)|keys|join('') }}
+{{ [1, 2, 3, 4]|slice(1, 2, true)|keys|join('') }}
+{{ {a: 1, b: 2, c: 3, d: 4}|slice(1, 2)|join('') }}
+{{ {a: 1, b: 2, c: 3, d: 4}|slice(1, 2)|keys|join('') }}
+{{ '1234'|slice(1, 2) }}
+{{ '1234'[1:2] }}
+{{ arr|slice(1, 2)|join('') }}
+{{ arr[1:2]|join('') }}
+
+{{ [1, 2, 3, 4]|slice(1)|join('') }}
+{{ [1, 2, 3, 4][1:]|join('') }}
+{{ '1234'|slice(1) }}
+{{ '1234'[1:] }}
+{{ '1234'[:1] }}
+--DATA--
+return array('start' => 1, 'length' => 2, 'arr' => new ArrayObject(array(1, 2, 3, 4)))
+--EXPECT--
+23
+23
+23
+23
+01
+12
+23
+bc
+23
+23
+23
+23
+
+234
+234
+234
+234
+1
--- /dev/null
+--TEST--
+"sort" filter
+--TEMPLATE--
+{{ array1|sort|join }}
+{{ array2|sort|join }}
+--DATA--
+return array('array1' => array(4, 1), 'array2' => array('foo', 'bar'))
+--EXPECT--
+14
+barfoo
--- /dev/null
+--TEST--
+"§" custom filter
+--TEMPLATE--
+{{ 'foo'|§ }}
+--DATA--
+return array()
+--EXPECT--
+§foo§
--- /dev/null
+--TEST--
+"split" filter
+--TEMPLATE--
+{{ "one,two,three,four,five"|split(',')|join('-') }}
+{{ foo|split(',')|join('-') }}
+{{ foo|split(',', 3)|join('-') }}
+{{ baz|split('')|join('-') }}
+{{ baz|split('', 2)|join('-') }}
+{{ foo|split(',', -2)|join('-') }}
+--DATA--
+return array('foo' => "one,two,three,four,five", 'baz' => '12345',)
+--EXPECT--
+one-two-three-four-five
+one-two-three-four-five
+one-two-three,four,five
+1-2-3-4-5
+12-34-5
+one-two-three
\ No newline at end of file
--- /dev/null
+--TEST--
+"trim" filter
+--TEMPLATE--
+{{ " I like Twig. "|trim }}
+{{ text|trim }}
+{{ " foo/"|trim("/") }}
+--DATA--
+return array('text' => " If you have some <strong>HTML</strong> it will be escaped. ")
+--EXPECT--
+I like Twig.
+If you have some <strong>HTML</strong> it will be escaped.
+ foo
--- /dev/null
+--TEST--
+"url_encode" filter
+--TEMPLATE--
+{{ {foo: "bar", number: 3, "spéßi%l": "e%c0d@d", "spa ce": ""}|url_encode }}
+{{ {foo: "bar", number: 3, "spéßi%l": "e%c0d@d", "spa ce": ""}|url_encode|raw }}
+{{ {}|url_encode|default("default") }}
+--DATA--
+return array()
+--EXPECT--
+foo=bar&number=3&sp%C3%A9%C3%9Fi%25l=e%25c0d%40d&spa+ce=
+foo=bar&number=3&sp%C3%A9%C3%9Fi%25l=e%25c0d%40d&spa+ce=
+default
--- /dev/null
+--TEST--
+"attribute" function
+--TEMPLATE--
+{{ attribute(obj, method) }}
+{{ attribute(array, item) }}
+{{ attribute(obj, "bar", ["a", "b"]) }}
+--DATA--
+return array('obj' => new TwigTestFoo(), 'method' => 'foo', 'array' => array('foo' => 'bar'), 'item' => 'foo')
+--EXPECT--
+foo
+bar
+bar_a-b
--- /dev/null
+--TEST--
+"block" function
+--TEMPLATE--
+{% extends 'base.twig' %}
+{% block bar %}BAR{% endblock %}
+--TEMPLATE(base.twig)--
+{% block foo %}{{ block('bar') }}{% endblock %}
+{% block bar %}BAR_BASE{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+BARBAR
--- /dev/null
+--TEST--
+"constant" function
+--TEMPLATE--
+{{ constant('DATE_W3C') == expect ? 'true' : 'false' }}
+{{ constant('ARRAY_AS_PROPS', object) }}
+--DATA--
+return array('expect' => DATE_W3C, 'object' => new ArrayObject(array('hi')));
+--EXPECT--
+true
+2
--- /dev/null
+--TEST--
+"cycle" function
+--TEMPLATE--
+{% for i in 0..6 %}
+{{ cycle(array1, i) }}-{{ cycle(array2, i) }}
+{% endfor %}
+--DATA--
+return array('array1' => array('odd', 'even'), 'array2' => array('apple', 'orange', 'citrus'))
+--EXPECT--
+odd-apple
+even-orange
+odd-citrus
+even-apple
+odd-orange
+even-citrus
+odd-apple
--- /dev/null
+--TEST--
+"date" function
+--TEMPLATE--
+{{ date() == date('now') ? 'OK' : 'KO' }}
+{{ date() > date('-1day') ? 'OK' : 'KO' }}
+{{ date(date1) == date('2010-10-04 13:45') ? 'OK' : 'KO' }}
+{{ date(date2) == date('2010-10-04 13:45') ? 'OK' : 'KO' }}
+{{ date(date3) == date('2010-10-04 13:45') ? 'OK' : 'KO' }}
+{{ date(date4) == date('2010-10-04 13:45') ? 'OK' : 'KO' }}
+{{ date(date5) == date('1964-01-02 03:04') ? 'OK' : 'KO' }}
+--DATA--
+date_default_timezone_set('UTC');
+return array(
+ 'date1' => mktime(13, 45, 0, 10, 4, 2010),
+ 'date2' => new DateTime('2010-10-04 13:45'),
+ 'date3' => '2010-10-04 13:45',
+ 'date4' => 1286199900, // DateTime::createFromFormat('Y-m-d H:i', '2010-10-04 13:45', new DateTimeZone('UTC'))->getTimestamp() -- A unixtimestamp is always GMT
+ 'date5' => -189291360, // DateTime::createFromFormat('Y-m-d H:i', '1964-01-02 03:04', new DateTimeZone('UTC'))->getTimestamp(),
+)
+--EXPECT--
+OK
+OK
+OK
+OK
+OK
+OK
+OK
--- /dev/null
+--TEST--
+"date" function
+--TEMPLATE--
+{{ date(date, "America/New_York")|date('d/m/Y H:i:s P', false) }}
+{{ date(timezone="America/New_York", date=date)|date('d/m/Y H:i:s P', false) }}
+--DATA--
+date_default_timezone_set('UTC');
+return array('date' => mktime(13, 45, 0, 10, 4, 2010))
+--EXPECT--
+04/10/2010 09:45:00 -04:00
+04/10/2010 09:45:00 -04:00
--- /dev/null
+--TEST--
+"dump" function
+--CONDITION--
+!extension_loaded('xdebug')
+--TEMPLATE--
+{{ dump('foo') }}
+{{ dump('foo', 'bar') }}
+--DATA--
+return array('foo' => 'foo', 'bar' => 'bar')
+--CONFIG--
+return array('debug' => true, 'autoescape' => false);
+--EXPECT--
+string(3) "foo"
+
+string(3) "foo"
+string(3) "bar"
--- /dev/null
+--TEST--
+"dump" function, xdebug is not loaded or xdebug <2.2-dev is loaded
+--CONDITION--
+!extension_loaded('xdebug') || (($r = new ReflectionExtension('xdebug')) && version_compare($r->getVersion(), '2.2-dev', '<'))
+--TEMPLATE--
+{{ dump() }}
+--DATA--
+return array('foo' => 'foo', 'bar' => 'bar')
+--CONFIG--
+return array('debug' => true, 'autoescape' => false);
+--EXPECT--
+array(3) {
+ ["foo"]=>
+ string(3) "foo"
+ ["bar"]=>
+ string(3) "bar"
+ ["global"]=>
+ string(6) "global"
+}
--- /dev/null
+--TEST--
+dynamic function
+--TEMPLATE--
+{{ foo_path('bar') }}
+{{ a_foo_b_bar('bar') }}
+--DATA--
+return array()
+--EXPECT--
+foo/bar
+a/b/bar
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+{% set tmp = include("foo.twig") %}
+
+FOO{{ tmp }}BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array()
+--EXPECT--
+FOO
+FOOBARBAR
--- /dev/null
+--TEST--
+"include" function is safe for auto-escaping
+--TEMPLATE--
+{{ include("foo.twig") }}
+--TEMPLATE(foo.twig)--
+<p>Test</p>
+--DATA--
+return array()
+--EXPECT--
+<p>Test</p>
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+FOO
+{{ include("foo.twig") }}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array()
+--EXPECT--
+FOO
+
+FOOBAR
+
+BAR
--- /dev/null
+--TEST--
+"include" function allows expressions for the template to include
+--TEMPLATE--
+FOO
+{{ include(foo) }}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array('foo' => 'foo.twig')
+--EXPECT--
+FOO
+
+FOOBAR
+
+BAR
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+{{ include(["foo.twig", "bar.twig"], ignore_missing = true) }}
+{{ include("foo.twig", ignore_missing = true) }}
+{{ include("foo.twig", ignore_missing = true, variables = {}) }}
+{{ include("foo.twig", ignore_missing = true, variables = {}, with_context = true) }}
+--DATA--
+return array()
+--EXPECT--
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+{{ include("foo.twig") }}
+--DATA--
+return array();
+--EXCEPTION--
+Twig_Error_Loader: Template "foo.twig" is not defined in "index.twig" at line 2.
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+{% extends "base.twig" %}
+
+{% block content %}
+ {{ parent() }}
+{% endblock %}
+--TEMPLATE(base.twig)--
+{% block content %}
+ {{ include("foo.twig") }}
+{% endblock %}
+--DATA--
+return array();
+--EXCEPTION--
+Twig_Error_Loader: Template "foo.twig" is not defined in "base.twig" at line 3.
--- /dev/null
+--TEST--
+"include" tag sandboxed
+--TEMPLATE--
+{{ include("foo.twig", sandboxed = true) }}
+--TEMPLATE(foo.twig)--
+{{ foo|e }}
+--DATA--
+return array()
+--EXCEPTION--
+Twig_Sandbox_SecurityError: Filter "e" is not allowed in "index.twig" at line 2.
--- /dev/null
+--TEST--
+"include" function accepts Twig_Template instance
+--TEMPLATE--
+{{ include(foo) }} FOO
+--TEMPLATE(foo.twig)--
+BAR
+--DATA--
+return array('foo' => $twig->loadTemplate('foo.twig'))
+--EXPECT--
+BAR FOO
--- /dev/null
+--TEST--
+"include" function
+--TEMPLATE--
+{{ include(["foo.twig", "bar.twig"]) }}
+{{- include(["bar.twig", "foo.twig"]) }}
+--TEMPLATE(foo.twig)--
+foo
+--DATA--
+return array()
+--EXPECT--
+foo
+foo
--- /dev/null
+--TEST--
+"include" function accept variables and with_context
+--TEMPLATE--
+{{ include("foo.twig") }}
+{{- include("foo.twig", with_context = false) }}
+{{- include("foo.twig", {'foo1': 'bar'}) }}
+{{- include("foo.twig", {'foo1': 'bar'}, with_context = false) }}
+--TEMPLATE(foo.twig)--
+{% for k, v in _context %}{{ k }},{% endfor %}
+--DATA--
+return array('foo' => 'bar')
+--EXPECT--
+foo,global,_parent,
+global,_parent,
+foo,global,foo1,_parent,
+foo1,global,_parent,
--- /dev/null
+--TEST--
+"include" function accept variables
+--TEMPLATE--
+{{ include("foo.twig", {'foo': 'bar'}) }}
+{{- include("foo.twig", vars) }}
+--TEMPLATE(foo.twig)--
+{{ foo }}
+--DATA--
+return array('vars' => array('foo' => 'bar'))
+--EXPECT--
+bar
+bar
--- /dev/null
+--TEST--
+"range" function
+--TEMPLATE--
+{{ range(low=0+1, high=10+0, step=2)|join(',') }}
+--DATA--
+return array()
+--EXPECT--
+1,3,5,7,9
--- /dev/null
+--TEST--
+"§" custom function
+--TEMPLATE--
+{{ §('foo') }}
+--DATA--
+return array()
+--EXPECT--
+§foo§
--- /dev/null
+--TEST--
+"template_from_string" function
+--TEMPLATE--
+{% include template_from_string(template) %}
+
+{% include template_from_string("Hello {{ name }}") %}
+--DATA--
+return array('name' => 'Fabien', 'template' => "Hello {{ name }}")
+--EXPECT--
+Hello Fabien
+Hello Fabien
--- /dev/null
+--TEST--
+macro
+--TEMPLATE--
+{% from _self import test %}
+
+{% macro test(a, b = 'bar') -%}
+{{ a }}{{ b }}
+{%- endmacro %}
+
+{{ test('foo') }}
+{{ test('bar', 'foo') }}
+--DATA--
+return array();
+--EXPECT--
+foobar
+barfoo
--- /dev/null
+--TEST--
+macro
+--TEMPLATE--
+{% import _self as macros %}
+
+{% macro foo(data) %}
+ {{ data }}
+{% endmacro %}
+
+{% macro bar() %}
+ <br />
+{% endmacro %}
+
+{{ macros.foo(macros.bar()) }}
+--DATA--
+return array();
+--EXPECT--
+<br />
--- /dev/null
+--TEST--
+macro
+--TEMPLATE--
+{% from _self import test %}
+
+{% macro test(this) -%}
+ {{ this }}
+{%- endmacro %}
+
+{{ test(this) }}
+--DATA--
+return array('this' => 'foo');
+--EXPECT--
+foo
--- /dev/null
+--TEST--
+macro
+--TEMPLATE--
+{% import _self as test %}
+{% from _self import test %}
+
+{% macro test(a, b) -%}
+ {{ a|default('a') }}<br />
+ {{- b|default('b') }}<br />
+{%- endmacro %}
+
+{{ test.test() }}
+{{ test() }}
+{{ test.test(1, "c") }}
+{{ test(1, "c") }}
+--DATA--
+return array();
+--EXPECT--
+a<br />b<br />
+a<br />b<br />
+1<br />c<br />
+1<br />c<br />
--- /dev/null
+--TEST--
+macro with a filter
+--TEMPLATE--
+{% import _self as test %}
+
+{% macro test() %}
+ {% filter escape %}foo<br />{% endfilter %}
+{% endmacro %}
+
+{{ test.test() }}
+--DATA--
+return array();
+--EXPECT--
+foo<br />
--- /dev/null
+--TEST--
+Twig outputs 0 nodes correctly
+--TEMPLATE--
+{{ foo }}0{{ foo }}
+--DATA--
+return array('foo' => 'foo')
+--EXPECT--
+foo0foo
--- /dev/null
+--TEST--
+Twig is able to deal with SimpleXMLElement instances as variables
+--CONDITION--
+version_compare(phpversion(), '5.3.0', '>=')
+--TEMPLATE--
+Hello '{{ images.image.0.group }}'!
+{{ images.children().count() }}
+{% for image in images %}
+ - {{ image.group }}
+{% endfor %}
+--DATA--
+return array('images' => new SimpleXMLElement('<images><image><group>foo</group></image><image><group>bar</group></image></images>'))
+--EXPECT--
+Hello 'foo'!
+2
+ - foo
+ - bar
--- /dev/null
+--TEST--
+Twig does not confuse strings with integers in getAttribute()
+--TEMPLATE--
+{{ hash['2e2'] }}
+--DATA--
+return array('hash' => array('2e2' => 'works'))
+--EXPECT--
+works
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping on its children
+--TEMPLATE--
+{% autoescape %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape 'html' %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape false %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape true %}
+{{ var }}<br />
+{% endautoescape %}
+{% autoescape false %}
+{{ var }}<br />
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br /><br />
+<br /><br />
+<br /><br />
+<br /><br />
+<br /><br />
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping on embedded blocks
+--TEMPLATE--
+{% autoescape 'html' %}
+ {% block foo %}
+ {{ var }}
+ {% endblock %}
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br />
--- /dev/null
+--TEST--
+"autoescape" tag does not double-escape
+--TEMPLATE--
+{% autoescape 'html' %}
+{{ var|escape }}
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br />
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping after calling functions
+--TEMPLATE--
+
+autoescape false
+{% autoescape false %}
+
+safe_br
+{{ safe_br() }}
+
+unsafe_br
+{{ unsafe_br() }}
+
+{% endautoescape %}
+
+autoescape 'html'
+{% autoescape 'html' %}
+
+safe_br
+{{ safe_br() }}
+
+unsafe_br
+{{ unsafe_br() }}
+
+unsafe_br()|raw
+{{ (unsafe_br())|raw }}
+
+safe_br()|escape
+{{ (safe_br())|escape }}
+
+safe_br()|raw
+{{ (safe_br())|raw }}
+
+unsafe_br()|escape
+{{ (unsafe_br())|escape }}
+
+{% endautoescape %}
+
+autoescape js
+{% autoescape 'js' %}
+
+safe_br
+{{ safe_br() }}
+
+{% endautoescape %}
+--DATA--
+return array()
+--EXPECT--
+
+autoescape false
+
+safe_br
+<br />
+
+unsafe_br
+<br />
+
+
+autoescape 'html'
+
+safe_br
+<br />
+
+unsafe_br
+<br />
+
+unsafe_br()|raw
+<br />
+
+safe_br()|escape
+<br />
+
+safe_br()|raw
+<br />
+
+unsafe_br()|escape
+<br />
+
+
+autoescape js
+
+safe_br
+\x3Cbr\x20\x2F\x3E
--- /dev/null
+--TEST--
+"autoescape" tag does not apply escaping on literals
+--TEMPLATE--
+{% autoescape 'html' %}
+
+1. Simple literal
+{{ "<br />" }}
+
+2. Conditional expression with only literals
+{{ true ? "<br />" : "<br>" }}
+
+3. Conditional expression with a variable
+{{ true ? "<br />" : someVar }}
+
+4. Nested conditionals with only literals
+{{ true ? (true ? "<br />" : "<br>") : "\n" }}
+
+5. Nested conditionals with a variable
+{{ true ? (true ? "<br />" : someVar) : "\n" }}
+
+6. Nested conditionals with a variable marked safe
+{{ true ? (true ? "<br />" : someVar|raw) : "\n" }}
+
+{% endautoescape %}
+--DATA--
+return array()
+--EXPECT--
+
+1. Simple literal
+<br />
+
+2. Conditional expression with only literals
+<br />
+
+3. Conditional expression with a variable
+<br />
+
+4. Nested conditionals with only literals
+<br />
+
+5. Nested conditionals with a variable
+<br />
+
+6. Nested conditionals with a variable marked safe
+<br />
--- /dev/null
+--TEST--
+"autoescape" tags can be nested at will
+--TEMPLATE--
+{{ var }}
+{% autoescape 'html' %}
+ {{ var }}
+ {% autoescape false %}
+ {{ var }}
+ {% autoescape 'html' %}
+ {{ var }}
+ {% endautoescape %}
+ {{ var }}
+ {% endautoescape %}
+ {{ var }}
+{% endautoescape %}
+{{ var }}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br />
+ <br />
+ <br />
+ <br />
+ <br />
+ <br />
+<br />
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping to object method calls
+--TEMPLATE--
+{% autoescape 'html' %}
+{{ user.name }}
+{{ user.name|lower }}
+{{ user }}
+{% endautoescape %}
+--DATA--
+class UserForAutoEscapeTest
+{
+ public function getName()
+ {
+ return 'Fabien<br />';
+ }
+
+ public function __toString()
+ {
+ return 'Fabien<br />';
+ }
+}
+return array('user' => new UserForAutoEscapeTest())
+--EXPECT--
+Fabien<br />
+fabien<br />
+Fabien<br />
--- /dev/null
+--TEST--
+"autoescape" tag does not escape when raw is used as a filter
+--TEMPLATE--
+{% autoescape 'html' %}
+{{ var|raw }}
+{% endautoescape %}
+--DATA--
+return array('var' => '<br />')
+--EXPECT--
+<br />
--- /dev/null
+--TEST--
+"autoescape" tag accepts an escaping strategy
+--TEMPLATE--
+{% autoescape true js %}{{ var }}{% endautoescape %}
+
+{% autoescape true html %}{{ var }}{% endautoescape %}
+
+{% autoescape 'js' %}{{ var }}{% endautoescape %}
+
+{% autoescape 'html' %}{{ var }}{% endautoescape %}
+--DATA--
+return array('var' => '<br />"')
+--EXPECT--
+\x3Cbr\x20\x2F\x3E\x22
+<br />"
+\x3Cbr\x20\x2F\x3E\x22
+<br />"
--- /dev/null
+--TEST--
+escape types
+--TEMPLATE--
+
+1. autoescape 'html' |escape('js')
+
+{% autoescape 'html' %}
+<a onclick="alert("{{ msg|escape('js') }}")"></a>
+{% endautoescape %}
+
+2. autoescape 'html' |escape('js')
+
+{% autoescape 'html' %}
+<a onclick="alert("{{ msg|escape('js') }}")"></a>
+{% endautoescape %}
+
+3. autoescape 'js' |escape('js')
+
+{% autoescape 'js' %}
+<a onclick="alert("{{ msg|escape('js') }}")"></a>
+{% endautoescape %}
+
+4. no escape
+
+{% autoescape false %}
+<a onclick="alert("{{ msg }}")"></a>
+{% endautoescape %}
+
+5. |escape('js')|escape('html')
+
+{% autoescape false %}
+<a onclick="alert("{{ msg|escape('js')|escape('html') }}")"></a>
+{% endautoescape %}
+
+6. autoescape 'html' |escape('js')|escape('html')
+
+{% autoescape 'html' %}
+<a onclick="alert("{{ msg|escape('js')|escape('html') }}")"></a>
+{% endautoescape %}
+
+--DATA--
+return array('msg' => "<>\n'\"")
+--EXPECT--
+
+1. autoescape 'html' |escape('js')
+
+<a onclick="alert("\x3C\x3E\x0A\x27\x22")"></a>
+
+2. autoescape 'html' |escape('js')
+
+<a onclick="alert("\x3C\x3E\x0A\x27\x22")"></a>
+
+3. autoescape 'js' |escape('js')
+
+<a onclick="alert("\x3C\x3E\x0A\x27\x22")"></a>
+
+4. no escape
+
+<a onclick="alert("<>
+'"")"></a>
+
+5. |escape('js')|escape('html')
+
+<a onclick="alert("\x3C\x3E\x0A\x27\x22")"></a>
+
+6. autoescape 'html' |escape('js')|escape('html')
+
+<a onclick="alert("\x3C\x3E\x0A\x27\x22")"></a>
+
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping after calling filters
+--TEMPLATE--
+{% autoescape 'html' %}
+
+(escape_and_nl2br is an escaper filter)
+
+1. Don't escape escaper filter output
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped )
+{{ var|escape_and_nl2br }}
+
+2. Don't escape escaper filter output
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped, |raw is redundant )
+{{ var|escape_and_nl2br|raw }}
+
+3. Explicit escape
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is explicitly escaped by |escape )
+{{ var|escape_and_nl2br|escape }}
+
+4. Escape non-escaper filter output
+( var is upper-cased by |upper,
+ the output is auto-escaped )
+{{ var|upper }}
+
+5. Escape if last filter is not an escaper
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is upper-cased by |upper,
+ the output is auto-escaped as |upper is not an escaper )
+{{ var|escape_and_nl2br|upper }}
+
+6. Don't escape escaper filter output
+( var is upper cased by upper,
+ the output is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped as |escape_and_nl2br is an escaper )
+{{ var|upper|escape_and_nl2br }}
+
+7. Escape if last filter is not an escaper
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is auto-escaped )
+{{ "<b>%s</b>"|format(var) }}
+
+8. Escape if last filter is not an escaper
+( the output of |format is "<b>" ~ var ~ "</b>",
+ |raw is redundant,
+ the output is auto-escaped )
+{{ "<b>%s</b>"|raw|format(var) }}
+
+9. Don't escape escaper filter output
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is not escaped due to |raw filter at the end )
+{{ "<b>%s</b>"|format(var)|raw }}
+
+10. Don't escape escaper filter output
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is not escaped due to |raw filter at the end,
+ the |raw filter on var is redundant )
+{{ "<b>%s</b>"|format(var|raw)|raw }}
+
+{% endautoescape %}
+--DATA--
+return array('var' => "<Fabien>\nTwig")
+--EXPECT--
+
+(escape_and_nl2br is an escaper filter)
+
+1. Don't escape escaper filter output
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped )
+<Fabien><br />
+Twig
+
+2. Don't escape escaper filter output
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped, |raw is redundant )
+<Fabien><br />
+Twig
+
+3. Explicit escape
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is explicitly escaped by |escape )
+&lt;Fabien&gt;<br />
+Twig
+
+4. Escape non-escaper filter output
+( var is upper-cased by |upper,
+ the output is auto-escaped )
+<FABIEN>
+TWIG
+
+5. Escape if last filter is not an escaper
+( var is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is upper-cased by |upper,
+ the output is auto-escaped as |upper is not an escaper )
+&LT;FABIEN&GT;<BR />
+TWIG
+
+6. Don't escape escaper filter output
+( var is upper cased by upper,
+ the output is escaped by |escape_and_nl2br, line-breaks are added,
+ the output is not escaped as |escape_and_nl2br is an escaper )
+<FABIEN><br />
+TWIG
+
+7. Escape if last filter is not an escaper
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is auto-escaped )
+<b><Fabien>
+Twig</b>
+
+8. Escape if last filter is not an escaper
+( the output of |format is "<b>" ~ var ~ "</b>",
+ |raw is redundant,
+ the output is auto-escaped )
+<b><Fabien>
+Twig</b>
+
+9. Don't escape escaper filter output
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is not escaped due to |raw filter at the end )
+<b><Fabien>
+Twig</b>
+
+10. Don't escape escaper filter output
+( the output of |format is "<b>" ~ var ~ "</b>",
+ the output is not escaped due to |raw filter at the end,
+ the |raw filter on var is redundant )
+<b><Fabien>
+Twig</b>
--- /dev/null
+--TEST--
+"autoescape" tag do not applies escaping on filter arguments
+--TEMPLATE--
+{% autoescape 'html' %}
+{{ var|nl2br("<br />") }}
+{{ var|nl2br("<br />"|escape) }}
+{{ var|nl2br(sep) }}
+{{ var|nl2br(sep|raw) }}
+{{ var|nl2br(sep|escape) }}
+{% endautoescape %}
+--DATA--
+return array('var' => "<Fabien>\nTwig", 'sep' => '<br />')
+--EXPECT--
+<Fabien><br />
+Twig
+<Fabien><br />
+Twig
+<Fabien><br />
+Twig
+<Fabien><br />
+Twig
+<Fabien><br />
+Twig
--- /dev/null
+--TEST--
+"autoescape" tag applies escaping after calling filters, and before calling pre_escape filters
+--TEMPLATE--
+{% autoescape 'html' %}
+
+(nl2br is pre_escaped for "html" and declared safe for "html")
+
+1. Pre-escape and don't post-escape
+( var|escape|nl2br )
+{{ var|nl2br }}
+
+2. Don't double-pre-escape
+( var|escape|nl2br )
+{{ var|escape|nl2br }}
+
+3. Don't escape safe values
+( var|raw|nl2br )
+{{ var|raw|nl2br }}
+
+4. Don't escape safe values
+( var|escape|nl2br|nl2br )
+{{ var|nl2br|nl2br }}
+
+5. Re-escape values that are escaped for an other contexts
+( var|escape_something|escape|nl2br )
+{{ var|escape_something|nl2br }}
+
+6. Still escape when using filters not declared safe
+( var|escape|nl2br|upper|escape )
+{{ var|nl2br|upper }}
+
+{% endautoescape %}
+--DATA--
+return array('var' => "<Fabien>\nTwig")
+--EXPECT--
+
+(nl2br is pre_escaped for "html" and declared safe for "html")
+
+1. Pre-escape and don't post-escape
+( var|escape|nl2br )
+<Fabien><br />
+Twig
+
+2. Don't double-pre-escape
+( var|escape|nl2br )
+<Fabien><br />
+Twig
+
+3. Don't escape safe values
+( var|raw|nl2br )
+<Fabien><br />
+Twig
+
+4. Don't escape safe values
+( var|escape|nl2br|nl2br )
+<Fabien><br /><br />
+Twig
+
+5. Re-escape values that are escaped for an other contexts
+( var|escape_something|escape|nl2br )
+<FABIEN><br />
+TWIG
+
+6. Still escape when using filters not declared safe
+( var|escape|nl2br|upper|escape )
+&LT;FABIEN&GT;<BR />
+TWIG
+
--- /dev/null
+--TEST--
+"autoescape" tag handles filters preserving the safety
+--TEMPLATE--
+{% autoescape 'html' %}
+
+(preserves_safety is preserving safety for "html")
+
+1. Unsafe values are still unsafe
+( var|preserves_safety|escape )
+{{ var|preserves_safety }}
+
+2. Safe values are still safe
+( var|escape|preserves_safety )
+{{ var|escape|preserves_safety }}
+
+3. Re-escape values that are escaped for an other contexts
+( var|escape_something|preserves_safety|escape )
+{{ var|escape_something|preserves_safety }}
+
+4. Still escape when using filters not declared safe
+( var|escape|preserves_safety|replace({'FABIEN': 'FABPOT'})|escape )
+{{ var|escape|preserves_safety|replace({'FABIEN': 'FABPOT'}) }}
+
+{% endautoescape %}
+--DATA--
+return array('var' => "<Fabien>\nTwig")
+--EXPECT--
+
+(preserves_safety is preserving safety for "html")
+
+1. Unsafe values are still unsafe
+( var|preserves_safety|escape )
+<FABIEN>
+TWIG
+
+2. Safe values are still safe
+( var|escape|preserves_safety )
+<FABIEN>
+TWIG
+
+3. Re-escape values that are escaped for an other contexts
+( var|escape_something|preserves_safety|escape )
+<FABIEN>
+TWIG
+
+4. Still escape when using filters not declared safe
+( var|escape|preserves_safety|replace({'FABIEN': 'FABPOT'})|escape )
+&LT;FABPOT&GT;
+TWIG
+
--- /dev/null
+--TEST--
+"block" tag
+--TEMPLATE--
+{% block title1 %}FOO{% endblock %}
+{% block title2 foo|lower %}
+--TEMPLATE(foo.twig)--
+{% block content %}{% endblock %}
+--DATA--
+return array('foo' => 'bar')
+--EXPECT--
+FOObar
--- /dev/null
+--TEST--
+"block" tag
+--TEMPLATE--
+{% block content %}
+ {% block content %}
+ {% endblock %}
+{% endblock %}
+--DATA--
+return array()
+--EXCEPTION--
+Twig_Error_Syntax: The block 'content' has already been defined line 2 in "index.twig" at line 3
--- /dev/null
+--TEST--
+"§" special chars in a block name
+--TEMPLATE--
+{% block § %}
+§
+{% endblock § %}
+--DATA--
+return array()
+--EXPECT--
+§
--- /dev/null
+--TEST--
+"embed" tag
+--TEMPLATE--
+FOO
+{% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ block1extended
+ {% endblock %}
+{% endembed %}
+
+BAR
+--TEMPLATE(foo.twig)--
+A
+{% block c1 %}
+ block1
+{% endblock %}
+B
+{% block c2 %}
+ block2
+{% endblock %}
+C
+--DATA--
+return array()
+--EXPECT--
+FOO
+
+A
+ block1
+
+ block1extended
+ B
+ block2
+C
+BAR
--- /dev/null
+--TEST--
+"embed" tag
+--TEMPLATE(index.twig)--
+FOO
+{% embed "foo.twig" %}
+ {% block c1 %}
+ {{ nothing }}
+ {% endblock %}
+{% endembed %}
+BAR
+--TEMPLATE(foo.twig)--
+{% block c1 %}{% endblock %}
+--DATA--
+return array()
+--EXCEPTION--
+Twig_Error_Runtime: Variable "nothing" does not exist in "index.twig" at line 5
--- /dev/null
+--TEST--
+"embed" tag
+--TEMPLATE--
+FOO
+{% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ block1extended
+ {% endblock %}
+{% endembed %}
+
+{% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ block1extended
+ {% endblock %}
+{% endembed %}
+
+BAR
+--TEMPLATE(foo.twig)--
+A
+{% block c1 %}
+ block1
+{% endblock %}
+B
+{% block c2 %}
+ block2
+{% endblock %}
+C
+--DATA--
+return array()
+--EXPECT--
+FOO
+
+A
+ block1
+
+ block1extended
+ B
+ block2
+C
+
+A
+ block1
+
+ block1extended
+ B
+ block2
+C
+BAR
--- /dev/null
+--TEST--
+"embed" tag
+--TEMPLATE--
+{% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ {% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ block1extended
+ {% endblock %}
+ {% endembed %}
+
+ {% endblock %}
+{% endembed %}
+--TEMPLATE(foo.twig)--
+A
+{% block c1 %}
+ block1
+{% endblock %}
+B
+{% block c2 %}
+ block2
+{% endblock %}
+C
+--DATA--
+return array()
+--EXPECT--
+A
+ block1
+
+
+A
+ block1
+
+ block1extended
+ B
+ block2
+C
+ B
+ block2
+C
--- /dev/null
+--TEST--
+"embed" tag
+--TEMPLATE--
+{% extends "base.twig" %}
+
+{% block c1 %}
+ {{ parent() }}
+ blockc1baseextended
+{% endblock %}
+
+{% block c2 %}
+ {{ parent() }}
+
+ {% embed "foo.twig" %}
+ {% block c1 %}
+ {{ parent() }}
+ block1extended
+ {% endblock %}
+ {% endembed %}
+{% endblock %}
+--TEMPLATE(base.twig)--
+A
+{% block c1 %}
+ blockc1base
+{% endblock %}
+{% block c2 %}
+ blockc2base
+{% endblock %}
+B
+--TEMPLATE(foo.twig)--
+A
+{% block c1 %}
+ block1
+{% endblock %}
+B
+{% block c2 %}
+ block2
+{% endblock %}
+C
+--DATA--
+return array()
+--EXPECT--
+A
+ blockc1base
+
+ blockc1baseextended
+ blockc2base
+
+
+
+A
+ block1
+
+ block1extended
+ B
+ block2
+CB
\ No newline at end of file
--- /dev/null
+--TEST--
+"filter" tag applies a filter on its children
+--TEMPLATE--
+{% filter upper %}
+Some text with a {{ var }}
+{% endfilter %}
+--DATA--
+return array('var' => 'var')
+--EXPECT--
+SOME TEXT WITH A VAR
--- /dev/null
+--TEST--
+"filter" tag applies a filter on its children
+--TEMPLATE--
+{% filter json_encode|raw %}test{% endfilter %}
+--DATA--
+return array()
+--EXPECT--
+"test"
--- /dev/null
+--TEST--
+"filter" tags accept multiple chained filters
+--TEMPLATE--
+{% filter lower|title %}
+ {{ var }}
+{% endfilter %}
+--DATA--
+return array('var' => 'VAR')
+--EXPECT--
+ Var
--- /dev/null
+--TEST--
+"filter" tags can be nested at will
+--TEMPLATE--
+{% filter lower|title %}
+ {{ var }}
+ {% filter upper %}
+ {{ var }}
+ {% endfilter %}
+ {{ var }}
+{% endfilter %}
+--DATA--
+return array('var' => 'var')
+--EXPECT--
+ Var
+ Var
+ Var
--- /dev/null
+--TEST--
+"filter" tag applies the filter on "for" tags
+--TEMPLATE--
+{% filter upper %}
+{% for item in items %}
+{{ item }}
+{% endfor %}
+{% endfilter %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+A
+B
--- /dev/null
+--TEST--
+"filter" tag applies the filter on "if" tags
+--TEMPLATE--
+{% filter upper %}
+{% if items %}
+{{ items|join(', ') }}
+{% endif %}
+
+{% if items.3 is defined %}
+FOO
+{% else %}
+{{ items.1 }}
+{% endif %}
+
+{% if items.3 is defined %}
+FOO
+{% elseif items.1 %}
+{{ items.0 }}
+{% endif %}
+
+{% endfilter %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+A, B
+
+B
+
+A
--- /dev/null
+--TEST--
+"for" tag takes a condition
+--TEMPLATE--
+{% for i in 1..5 if i is odd -%}
+ {{ loop.index }}.{{ i }}{{ foo.bar }}
+{% endfor %}
+--DATA--
+return array('foo' => array('bar' => 'X'))
+--CONFIG--
+return array('strict_variables' => false)
+--EXPECT--
+1.1X
+2.3X
+3.5X
--- /dev/null
+--TEST--
+"for" tag keeps the context safe
+--TEMPLATE--
+{% for item in items %}
+ {% for item in items %}
+ * {{ item }}
+ {% endfor %}
+ * {{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * a
+ * b
+ * a
+ * a
+ * b
+ * b
--- /dev/null
+--TEST--
+"for" tag can use an "else" clause
+--TEMPLATE--
+{% for item in items %}
+ * {{ item }}
+{% else %}
+ no item
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * a
+ * b
+--DATA--
+return array('items' => array())
+--EXPECT--
+ no item
+--DATA--
+return array()
+--CONFIG--
+return array('strict_variables' => false)
+--EXPECT--
+ no item
--- /dev/null
+--TEST--
+"for" tag does not reset inner variables
+--TEMPLATE--
+{% for i in 1..2 %}
+ {% for j in 0..2 %}
+ {{k}}{% set k = k+1 %} {{ loop.parent.loop.index }}
+ {% endfor %}
+{% endfor %}
+--DATA--
+return array('k' => 0)
+--EXPECT--
+ 0 1
+ 1 1
+ 2 1
+ 3 2
+ 4 2
+ 5 2
--- /dev/null
+--TEST--
+"for" tag can iterate over keys
+--TEMPLATE--
+{% for key in items|keys %}
+ * {{ key }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * 0
+ * 1
--- /dev/null
+--TEST--
+"for" tag can iterate over keys and values
+--TEMPLATE--
+{% for key, item in items %}
+ * {{ key }}/{{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * 0/a
+ * 1/b
--- /dev/null
+--TEST--
+"for" tag adds a loop variable to the context
+--TEMPLATE--
+{% for item in items %}
+ * {{ loop.index }}/{{ loop.index0 }}
+ * {{ loop.revindex }}/{{ loop.revindex0 }}
+ * {{ loop.first }}/{{ loop.last }}/{{ loop.length }}
+
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * 1/0
+ * 2/1
+ * 1//2
+
+ * 2/1
+ * 1/0
+ * /1/2
--- /dev/null
+--TEST--
+"for" tag adds a loop variable to the context locally
+--TEMPLATE--
+{% for item in items %}
+{% endfor %}
+{% if loop is not defined %}WORKS{% endif %}
+--DATA--
+return array('items' => array())
+--EXPECT--
+WORKS
--- /dev/null
+--TEST--
+"for" tag
+--TEMPLATE--
+{% for i, item in items if i > 0 %}
+ {{ loop.last }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXCEPTION--
+Twig_Error_Syntax: The "loop.last" variable is not defined when looping with a condition in "index.twig" at line 3
--- /dev/null
+--TEST--
+"for" tag
+--TEMPLATE--
+{% for i, item in items if loop.last > 0 %}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXCEPTION--
+Twig_Error_Syntax: The "loop" variable cannot be used in a looping condition in "index.twig" at line 2
--- /dev/null
+--TEST--
+"for" tag can use an "else" clause
+--TEMPLATE--
+{% for item in items %}
+ {% for item in items1 %}
+ * {{ item }}
+ {% else %}
+ no {{ item }}
+ {% endfor %}
+{% else %}
+ no item1
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'), 'items1' => array())
+--EXPECT--
+no a
+ no b
--- /dev/null
+--TEST--
+"for" tag iterates over iterable objects
+--TEMPLATE--
+{% for item in items %}
+ * {{ item }}
+ * {{ loop.index }}/{{ loop.index0 }}
+ * {{ loop.first }}
+
+{% endfor %}
+
+{% for key, value in items %}
+ * {{ key }}/{{ value }}
+{% endfor %}
+
+{% for key in items|keys %}
+ * {{ key }}
+{% endfor %}
+--DATA--
+class ItemsIterator implements Iterator
+{
+ protected $values = array('foo' => 'bar', 'bar' => 'foo');
+ public function current() { return current($this->values); }
+ public function key() { return key($this->values); }
+ public function next() { return next($this->values); }
+ public function rewind() { return reset($this->values); }
+ public function valid() { return false !== current($this->values); }
+}
+return array('items' => new ItemsIterator())
+--EXPECT--
+ * bar
+ * 1/0
+ * 1
+
+ * foo
+ * 2/1
+ *
+
+
+ * foo/bar
+ * bar/foo
+
+ * foo
+ * bar
--- /dev/null
+--TEST--
+"for" tag iterates over iterable and countable objects
+--TEMPLATE--
+{% for item in items %}
+ * {{ item }}
+ * {{ loop.index }}/{{ loop.index0 }}
+ * {{ loop.revindex }}/{{ loop.revindex0 }}
+ * {{ loop.first }}/{{ loop.last }}/{{ loop.length }}
+
+{% endfor %}
+
+{% for key, value in items %}
+ * {{ key }}/{{ value }}
+{% endfor %}
+
+{% for key in items|keys %}
+ * {{ key }}
+{% endfor %}
+--DATA--
+class ItemsIteratorCountable implements Iterator, Countable
+{
+ protected $values = array('foo' => 'bar', 'bar' => 'foo');
+ public function current() { return current($this->values); }
+ public function key() { return key($this->values); }
+ public function next() { return next($this->values); }
+ public function rewind() { return reset($this->values); }
+ public function valid() { return false !== current($this->values); }
+ public function count() { return count($this->values); }
+}
+return array('items' => new ItemsIteratorCountable())
+--EXPECT--
+ * bar
+ * 1/0
+ * 2/1
+ * 1//2
+
+ * foo
+ * 2/1
+ * 1/0
+ * /1/2
+
+
+ * foo/bar
+ * bar/foo
+
+ * foo
+ * bar
--- /dev/null
+--TEST--
+"for" tags can be nested
+--TEMPLATE--
+{% for key, item in items %}
+* {{ key }} ({{ loop.length }}):
+{% for value in item %}
+ * {{ value }} ({{ loop.length }})
+{% endfor %}
+{% endfor %}
+--DATA--
+return array('items' => array('a' => array('a1', 'a2', 'a3'), 'b' => array('b1')))
+--EXPECT--
+* a (2):
+ * a1 (3)
+ * a2 (3)
+ * a3 (3)
+* b (2):
+ * b1 (1)
--- /dev/null
+--TEST--
+"for" tag iterates over item values
+--TEMPLATE--
+{% for item in items %}
+ * {{ item }}
+{% endfor %}
+--DATA--
+return array('items' => array('a', 'b'))
+--EXPECT--
+ * a
+ * b
--- /dev/null
+--TEST--
+global variables
+--TEMPLATE--
+{% include "included.twig" %}
+{% from "included.twig" import foobar %}
+{{ foobar() }}
+--TEMPLATE(included.twig)--
+{% macro foobar() %}
+called foobar
+{% endmacro %}
+--DATA--
+return array();
+--EXPECT--
+called foobar
--- /dev/null
+--TEST--
+"if" creates a condition
+--TEMPLATE--
+{% if a is defined %}
+ {{ a }}
+{% elseif b is defined %}
+ {{ b }}
+{% else %}
+ NOTHING
+{% endif %}
+--DATA--
+return array('a' => 'a')
+--EXPECT--
+ a
+--DATA--
+return array('b' => 'b')
+--EXPECT--
+ b
+--DATA--
+return array()
+--EXPECT--
+ NOTHING
--- /dev/null
+--TEST--
+"if" takes an expression as a test
+--TEMPLATE--
+{% if a < 2 %}
+ A1
+{% elseif a > 10 %}
+ A2
+{% else %}
+ A3
+{% endif %}
+--DATA--
+return array('a' => 1)
+--EXPECT--
+ A1
+--DATA--
+return array('a' => 12)
+--EXPECT--
+ A2
+--DATA--
+return array('a' => 7)
+--EXPECT--
+ A3
--- /dev/null
+--TEST--
+"include" tag
+--TEMPLATE--
+FOO
+{% include "foo.twig" %}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array()
+--EXPECT--
+FOO
+
+FOOBAR
+BAR
--- /dev/null
+--TEST--
+"include" tag allows expressions for the template to include
+--TEMPLATE--
+FOO
+{% include foo %}
+
+BAR
+--TEMPLATE(foo.twig)--
+FOOBAR
+--DATA--
+return array('foo' => 'foo.twig')
+--EXPECT--
+FOO
+
+FOOBAR
+BAR
--- /dev/null
+--TEST--
+"include" tag
+--TEMPLATE--
+{% include ["foo.twig", "bar.twig"] ignore missing %}
+{% include "foo.twig" ignore missing %}
+{% include "foo.twig" ignore missing with {} %}
+{% include "foo.twig" ignore missing with {} only %}
+--DATA--
+return array()
+--EXPECT--
--- /dev/null
+--TEST--
+"include" tag
+--TEMPLATE--
+{% include "foo.twig" %}
+--DATA--
+return array();
+--EXCEPTION--
+Twig_Error_Loader: Template "foo.twig" is not defined in "index.twig" at line 2.
--- /dev/null
+--TEST--
+"include" tag
+--TEMPLATE--
+{% extends "base.twig" %}
+
+{% block content %}
+ {{ parent() }}
+{% endblock %}
+--TEMPLATE(base.twig)--
+{% block content %}
+ {% include "foo.twig" %}
+{% endblock %}
+--DATA--
+return array();
+--EXCEPTION--
+Twig_Error_Loader: Template "foo.twig" is not defined in "base.twig" at line 3.
--- /dev/null
+--TEST--
+"include" tag accept variables and only
+--TEMPLATE--
+{% include "foo.twig" %}
+{% include "foo.twig" only %}
+{% include "foo.twig" with {'foo1': 'bar'} %}
+{% include "foo.twig" with {'foo1': 'bar'} only %}
+--TEMPLATE(foo.twig)--
+{% for k, v in _context %}{{ k }},{% endfor %}
+--DATA--
+return array('foo' => 'bar')
+--EXPECT--
+foo,global,_parent,
+global,_parent,
+foo,global,foo1,_parent,
+foo1,global,_parent,
--- /dev/null
+--TEST--
+"include" tag accepts Twig_Template instance
+--TEMPLATE--
+{% include foo %} FOO
+--TEMPLATE(foo.twig)--
+BAR
+--DATA--
+return array('foo' => $twig->loadTemplate('foo.twig'))
+--EXPECT--
+BAR FOO
--- /dev/null
+--TEST--
+"include" tag
+--TEMPLATE--
+{% include ["foo.twig", "bar.twig"] %}
+{% include ["bar.twig", "foo.twig"] %}
+--TEMPLATE(foo.twig)--
+foo
+--DATA--
+return array()
+--EXPECT--
+foo
+foo
--- /dev/null
+--TEST--
+"include" tag accept variables
+--TEMPLATE--
+{% include "foo.twig" with {'foo': 'bar'} %}
+{% include "foo.twig" with vars %}
+--TEMPLATE(foo.twig)--
+{{ foo }}
+--DATA--
+return array('vars' => array('foo' => 'bar'))
+--EXPECT--
+bar
+bar
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "foo.twig" %}
+
+{% block content %}
+FOO
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+FOO
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends standalone ? foo : 'bar.twig' %}
+
+{% block content %}{{ parent() }}FOO{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}FOO{% endblock %}
+--TEMPLATE(bar.twig)--
+{% block content %}BAR{% endblock %}
+--DATA--
+return array('foo' => 'foo.twig', 'standalone' => true)
+--EXPECT--
+FOOFOO
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends foo %}
+
+{% block content %}
+FOO
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}{% endblock %}
+--DATA--
+return array('foo' => 'foo.twig')
+--EXPECT--
+FOO
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "foo.twig" %}
+--TEMPLATE(foo.twig)--
+{% block content %}FOO{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+FOO
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends ["foo.twig", "bar.twig"] %}
+--TEMPLATE(bar.twig)--
+{% block content %}
+foo
+{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+foo
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "layout.twig" %}{% block content %}{{ parent() }}index {% endblock %}
+--TEMPLATE(layout.twig)--
+{% extends "base.twig" %}{% block content %}{{ parent() }}layout {% endblock %}
+--TEMPLATE(base.twig)--
+{% block content %}base {% endblock %}
+--DATA--
+return array()
+--EXPECT--
+base layout index
--- /dev/null
+--TEST--
+"block" tag
+--TEMPLATE--
+{% extends "foo.twig" %}
+
+{% block content %}
+ {% block subcontent %}
+ {% block subsubcontent %}
+ SUBSUBCONTENT
+ {% endblock %}
+ {% endblock %}
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}
+ {% block subcontent %}
+ SUBCONTENT
+ {% endblock %}
+{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+SUBSUBCONTENT
--- /dev/null
+--TEST--
+"block" tag
+--TEMPLATE--
+{% block content %}
+ CONTENT
+ {%- block subcontent -%}
+ SUBCONTENT
+ {%- endblock -%}
+ ENDCONTENT
+{% endblock %}
+--TEMPLATE(foo.twig)--
+--DATA--
+return array()
+--EXPECT--
+CONTENTSUBCONTENTENDCONTENT
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "layout.twig" %}
+{% block inside %}INSIDE{% endblock inside %}
+--TEMPLATE(layout.twig)--
+{% extends "base.twig" %}
+{% block body %}
+ {% block inside '' %}
+{% endblock body %}
+--TEMPLATE(base.twig)--
+{% block body '' %}
+--DATA--
+return array()
+--EXPECT--
+INSIDE
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "foo.twig" %}
+
+{% block content %}{{ parent() }}FOO{{ parent() }}{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}BAR{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+BARFOOBAR
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends foo ? 'foo.twig' : 'bar.twig' %}
+--TEMPLATE(foo.twig)--
+FOO
+--TEMPLATE(bar.twig)--
+BAR
+--DATA--
+return array('foo' => true)
+--EXPECT--
+FOO
+--DATA--
+return array('foo' => false)
+--EXPECT--
+BAR
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% block content %}
+ {% extends "foo.twig" %}
+{% endblock %}
+--EXCEPTION--
+Twig_Error_Syntax: Cannot extend from a block in "index.twig" at line 3
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "base.twig" %}
+{% block content %}{% include "included.twig" %}{% endblock %}
+
+{% block footer %}Footer{% endblock %}
+--TEMPLATE(included.twig)--
+{% extends "base.twig" %}
+{% block content %}Included Content{% endblock %}
+--TEMPLATE(base.twig)--
+{% block content %}Default Content{% endblock %}
+
+{% block footer %}Default Footer{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+Included Content
+Default Footer
+Footer
--- /dev/null
+--TEST--
+"extends" tag
+--TEMPLATE--
+{% extends "foo.twig" %}
+
+{% block content %}
+ {% block inside %}
+ INSIDE OVERRIDDEN
+ {% endblock %}
+
+ BEFORE
+ {{ parent() }}
+ AFTER
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}
+ BAR
+{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+
+INSIDE OVERRIDDEN
+
+ BEFORE
+ BAR
+
+ AFTER
--- /dev/null
+--TEST--
+"parent" tag
+--TEMPLATE--
+{% block content %}
+ {{ parent() }}
+{% endblock %}
+--EXCEPTION--
+Twig_Error_Syntax: Calling "parent" on a template that does not extend nor "use" another template is forbidden in "index.twig" at line 3
--- /dev/null
+--TEST--
+"parent" tag
+--TEMPLATE--
+{% use 'foo.twig' %}
+
+{% block content %}
+ {{ parent() }}
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}BAR{% endblock %}
+--DATA--
+return array()
+--EXPECT--
+BAR
--- /dev/null
+--TEST--
+"extends" tag accepts Twig_Template instance
+--TEMPLATE--
+{% extends foo %}
+
+{% block content %}
+{{ parent() }}FOO
+{% endblock %}
+--TEMPLATE(foo.twig)--
+{% block content %}BAR{% endblock %}
+--DATA--
+return array('foo' => $twig->loadTemplate('foo.twig'))
+--EXPECT--
+BARFOO
--- /dev/null
+--TEST--
+"parent" function
+--TEMPLATE--
+{% extends "parent.twig" %}
+
+{% use "use1.twig" %}
+{% use "use2.twig" %}
+
+{% block content_parent %}
+ {{ parent() }}
+{% endblock %}
+
+{% block content_use1 %}
+ {{ parent() }}
+{% endblock %}
+
+{% block content_use2 %}
+ {{ parent() }}
+{% endblock %}
+
+{% block content %}
+ {{ block('content_use1_only') }}
+ {{ block('content_use2_only') }}
+{% endblock %}
+--TEMPLATE(parent.twig)--
+{% block content_parent 'content_parent' %}
+{% block content_use1 'content_parent' %}
+{% block content_use2 'content_parent' %}
+{% block content '' %}
+--TEMPLATE(use1.twig)--
+{% block content_use1 'content_use1' %}
+{% block content_use2 'content_use1' %}
+{% block content_use1_only 'content_use1_only' %}
+--TEMPLATE(use2.twig)--
+{% block content_use2 'content_use2' %}
+{% block content_use2_only 'content_use2_only' %}
+--DATA--
+return array()
+--EXPECT--
+ content_parent
+ content_use1
+ content_use2
+ content_use1_only
+ content_use2_only
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% import _self as macros %}
+
+{{ macros.input('username') }}
+{{ macros.input('password', null, 'password', 1) }}
+
+{% macro input(name, value, type, size) %}
+ <input type="{{ type|default("text") }}" name="{{ name }}" value="{{ value|e|default('') }}" size="{{ size|default(20) }}">
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+ <input type="text" name="username" value="" size="20">
+
+ <input type="password" name="password" value="" size="1">
--- /dev/null
+--TEST--
+"macro" tag supports name for endmacro
+--TEMPLATE--
+{% import _self as macros %}
+
+{{ macros.foo() }}
+{{ macros.bar() }}
+
+{% macro foo() %}foo{% endmacro %}
+{% macro bar() %}bar{% endmacro bar %}
+--DATA--
+return array()
+--EXPECT--
+foo
+bar
+
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% import 'forms.twig' as forms %}
+
+{{ forms.input('username') }}
+{{ forms.input('password', null, 'password', 1) }}
+--TEMPLATE(forms.twig)--
+{% macro input(name, value, type, size) %}
+ <input type="{{ type|default("text") }}" name="{{ name }}" value="{{ value|e|default('') }}" size="{{ size|default(20) }}">
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+ <input type="text" name="username" value="" size="20">
+
+ <input type="password" name="password" value="" size="1">
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% from 'forms.twig' import foo %}
+{% from 'forms.twig' import foo as foobar, bar %}
+
+{{ foo('foo') }}
+{{ foobar('foo') }}
+{{ bar('foo') }}
+--TEMPLATE(forms.twig)--
+{% macro foo(name) %}foo{{ name }}{% endmacro %}
+{% macro bar(name) %}bar{{ name }}{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+foofoo
+foofoo
+barfoo
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% from 'forms.twig' import foo %}
+
+{{ foo('foo') }}
+{{ foo() }}
+--TEMPLATE(forms.twig)--
+{% macro foo(name) %}{{ name|default('foo') }}{{ global }}{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+fooglobal
+fooglobal
--- /dev/null
+--TEST--
+"macro" tag
+--TEMPLATE--
+{% import _self as forms %}
+
+{{ forms.input('username') }}
+{{ forms.input('password', null, 'password', 1) }}
+
+{% macro input(name, value, type, size) %}
+ <input type="{{ type|default("text") }}" name="{{ name }}" value="{{ value|e|default('') }}" size="{{ size|default(20) }}">
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+ <input type="text" name="username" value="" size="20">
+
+ <input type="password" name="password" value="" size="1">
--- /dev/null
+--TEST--
+"§" as a macro name
+--TEMPLATE--
+{% import _self as macros %}
+
+{{ macros.§('foo') }}
+
+{% macro §(foo) %}
+ §{{ foo }}§
+{% endmacro %}
+--DATA--
+return array()
+--EXPECT--
+§foo§
--- /dev/null
+--TEST--
+"raw" tag
+--TEMPLATE--
+{% raw %}
+{{ foo }}
+{% endraw %}
+--DATA--
+return array()
+--EXPECT--
+{{ foo }}
--- /dev/null
+--TEST--
+"raw" tag
+--TEMPLATE--
+{% raw %}
+{{ foo }}
+{% endverbatim %}
+--DATA--
+return array()
+--EXCEPTION--
+Twig_Error_Syntax: Unexpected end of file: Unclosed "raw" block in "index.twig" at line 2
--- /dev/null
+--TEST--
+"raw" tag
+--TEMPLATE--
+1***
+
+{%- raw %}
+ {{ 'bla' }}
+{% endraw %}
+
+1***
+2***
+
+{%- raw -%}
+ {{ 'bla' }}
+{% endraw %}
+
+2***
+3***
+
+{%- raw -%}
+ {{ 'bla' }}
+{% endraw -%}
+
+3***
+4***
+
+{%- raw -%}
+ {{ 'bla' }}
+{%- endraw %}
+
+4***
+5***
+
+{%- raw -%}
+ {{ 'bla' }}
+{%- endraw -%}
+
+5***
+--DATA--
+return array()
+--EXPECT--
+1***
+ {{ 'bla' }}
+
+
+1***
+2***{{ 'bla' }}
+
+
+2***
+3***{{ 'bla' }}
+3***
+4***{{ 'bla' }}
+
+4***
+5***{{ 'bla' }}5***
--- /dev/null
+--TEST--
+sandbox tag
+--TEMPLATE--
+{%- sandbox %}
+ {%- include "foo.twig" %}
+ a
+{%- endsandbox %}
+--TEMPLATE(foo.twig)--
+foo
+--EXCEPTION--
+Twig_Error_Syntax: Only "include" tags are allowed within a "sandbox" section in "index.twig" at line 4
--- /dev/null
+--TEST--
+sandbox tag
+--TEMPLATE--
+{%- sandbox %}
+ {%- include "foo.twig" %}
+
+ {% if 1 %}
+ {%- include "foo.twig" %}
+ {% endif %}
+{%- endsandbox %}
+--TEMPLATE(foo.twig)--
+foo
+--EXCEPTION--
+Twig_Error_Syntax: Only "include" tags are allowed within a "sandbox" section in "index.twig" at line 5
--- /dev/null
+--TEST--
+sandbox tag
+--TEMPLATE--
+{%- sandbox %}
+ {%- include "foo.twig" %}
+{%- endsandbox %}
+
+{%- sandbox %}
+ {%- include "foo.twig" %}
+ {%- include "foo.twig" %}
+{%- endsandbox %}
+
+{%- sandbox %}{% include "foo.twig" %}{% endsandbox %}
+--TEMPLATE(foo.twig)--
+foo
+--DATA--
+return array()
+--EXPECT--
+foo
+foo
+foo
+foo
--- /dev/null
+--TEST--
+"set" tag
+--TEMPLATE--
+{% set foo = 'foo' %}
+{% set bar = 'foo<br />' %}
+
+{{ foo }}
+{{ bar }}
+
+{% set foo, bar = 'foo', 'bar' %}
+
+{{ foo }}{{ bar }}
+--DATA--
+return array()
+--EXPECT--
+foo
+foo<br />
+
+
+foobar
--- /dev/null
+--TEST--
+"set" tag block empty capture
+--TEMPLATE--
+{% set foo %}{% endset %}
+
+{% if foo %}FAIL{% endif %}
+--DATA--
+return array()
+--EXPECT--
--- /dev/null
+--TEST--
+"set" tag block capture
+--TEMPLATE--
+{% set foo %}f<br />o<br />o{% endset %}
+
+{{ foo }}
+--DATA--
+return array()
+--EXPECT--
+f<br />o<br />o
--- /dev/null
+--TEST--
+"set" tag
+--TEMPLATE--
+{% set foo, bar = 'foo' ~ 'bar', 'bar' ~ 'foo' %}
+
+{{ foo }}
+{{ bar }}
+--DATA--
+return array()
+--EXPECT--
+foobar
+barfoo
--- /dev/null
+--TEST--
+"spaceless" tag removes whites between HTML tags
+--TEMPLATE--
+{% spaceless %}
+
+ <div> <div> foo </div> </div>
+
+{% endspaceless %}
+--DATA--
+return array()
+--EXPECT--
+<div><div> foo </div></div>
--- /dev/null
+--TEST--
+"§" custom tag
+--TEMPLATE--
+{% § %}
+--DATA--
+return array()
+--EXPECT--
+§
--- /dev/null
+--TEST--
+Whitespace trimming on tags.
+--TEMPLATE--
+{{ 5 * '{#-'|length }}
+{{ '{{-'|length * 5 + '{%-'|length }}
+
+Trim on control tag:
+{% for i in range(1, 9) -%}
+ {{ i }}
+{%- endfor %}
+
+
+Trim on output tag:
+{% for i in range(1, 9) %}
+ {{- i -}}
+{% endfor %}
+
+
+Trim comments:
+
+{#- Invisible -#}
+
+After the comment.
+
+Trim leading space:
+{% if leading %}
+
+ {{- leading }}
+{% endif %}
+
+{%- if leading %}
+ {{- leading }}
+
+{%- endif %}
+
+
+Trim trailing space:
+{% if trailing -%}
+ {{ trailing -}}
+
+{% endif -%}
+
+Combined:
+
+{%- if both -%}
+<ul>
+ <li> {{- both -}} </li>
+</ul>
+
+{%- endif -%}
+
+end
+--DATA--
+return array('leading' => 'leading space', 'trailing' => 'trailing space', 'both' => 'both')
+--EXPECT--
+15
+18
+
+Trim on control tag:
+123456789
+
+Trim on output tag:
+123456789
+
+Trim comments:After the comment.
+
+Trim leading space:
+leading space
+leading space
+
+Trim trailing space:
+trailing spaceCombined:<ul>
+ <li>both</li>
+</ul>end
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "blocks.twig" with content as foo %}
+
+{{ block('foo') }}
+--TEMPLATE(blocks.twig)--
+{% block content 'foo' %}
+--DATA--
+return array()
+--EXPECT--
+foo
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "blocks.twig" %}
+
+{{ block('content') }}
+--TEMPLATE(blocks.twig)--
+{% block content 'foo' %}
+--DATA--
+return array()
+--EXPECT--
+foo
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "foo.twig" %}
+
+{{ block('content') }}
+{{ block('foo') }}
+{{ block('bar') }}
+--TEMPLATE(foo.twig)--
+{% use "bar.twig" %}
+
+{% block content 'foo' %}
+{% block foo 'foo' %}
+--TEMPLATE(bar.twig)--
+{% block content 'bar' %}
+{% block bar 'bar' %}
+--DATA--
+return array()
+--EXPECT--
+foo
+foo
+bar
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "foo.twig" %}
+--TEMPLATE(foo.twig)--
+{% use "bar.twig" %}
+--TEMPLATE(bar.twig)--
+--DATA--
+return array()
+--EXPECT--
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "foo.twig" %}
+{% use "bar.twig" %}
+
+{{ block('content') }}
+{{ block('foo') }}
+{{ block('bar') }}
+--TEMPLATE(foo.twig)--
+{% block content 'foo' %}
+{% block foo 'foo' %}
+--TEMPLATE(bar.twig)--
+{% block content 'bar' %}
+{% block bar 'bar' %}
+--DATA--
+return array()
+--EXPECT--
+bar
+foo
+bar
--- /dev/null
+--TEST--
+"use" tag
+--TEMPLATE--
+{% use "foo.twig" with content as foo_content %}
+{% use "bar.twig" %}
+
+{{ block('content') }}
+{{ block('foo') }}
+{{ block('bar') }}
+{{ block('foo_content') }}
+--TEMPLATE(foo.twig)--
+{% block content 'foo' %}
+{% block foo 'foo' %}
+--TEMPLATE(bar.twig)--
+{% block content 'bar' %}
+{% block bar 'bar' %}
+--DATA--
+return array()
+--EXPECT--
+bar
+foo
+bar
+foo
--- /dev/null
+--TEST--
+"verbatim" tag
+--TEMPLATE--
+{% verbatim %}
+{{ foo }}
+{% endverbatim %}
+--DATA--
+return array()
+--EXPECT--
+{{ foo }}
--- /dev/null
+--TEST--
+"verbatim" tag
+--TEMPLATE--
+{% verbatim %}
+{{ foo }}
+{% endraw %}
+--DATA--
+return array()
+--EXCEPTION--
+Twig_Error_Syntax: Unexpected end of file: Unclosed "verbatim" block in "index.twig" at line 2
--- /dev/null
+--TEST--
+"verbatim" tag
+--TEMPLATE--
+1***
+
+{%- verbatim %}
+ {{ 'bla' }}
+{% endverbatim %}
+
+1***
+2***
+
+{%- verbatim -%}
+ {{ 'bla' }}
+{% endverbatim %}
+
+2***
+3***
+
+{%- verbatim -%}
+ {{ 'bla' }}
+{% endverbatim -%}
+
+3***
+4***
+
+{%- verbatim -%}
+ {{ 'bla' }}
+{%- endverbatim %}
+
+4***
+5***
+
+{%- verbatim -%}
+ {{ 'bla' }}
+{%- endverbatim -%}
+
+5***
+--DATA--
+return array()
+--EXPECT--
+1***
+ {{ 'bla' }}
+
+
+1***
+2***{{ 'bla' }}
+
+
+2***
+3***{{ 'bla' }}
+3***
+4***{{ 'bla' }}
+
+4***
+5***{{ 'bla' }}5***
--- /dev/null
+--TEST--
+array index test
+--TEMPLATE--
+{% for key, value in days %}
+{{ key }}
+{% endfor %}
+--DATA--
+return array('days' => array(
+ 1 => array('money' => 9),
+ 2 => array('money' => 21),
+ 3 => array('money' => 38),
+ 4 => array('money' => 6),
+ 18 => array('money' => 6),
+ 19 => array('money' => 3),
+ 31 => array('money' => 11),
+));
+--EXPECT--
+1
+2
+3
+4
+18
+19
+31
--- /dev/null
+--TEST--
+"const" test
+--TEMPLATE--
+{{ 8 is constant('E_NOTICE') ? 'ok' : 'no' }}
+{{ 'bar' is constant('TwigTestFoo::BAR_NAME') ? 'ok' : 'no' }}
+{{ value is constant('TwigTestFoo::BAR_NAME') ? 'ok' : 'no' }}
+{{ 2 is constant('ARRAY_AS_PROPS', object) ? 'ok' : 'no' }}
+--DATA--
+return array('value' => 'bar', 'object' => new ArrayObject(array('hi')));
+--EXPECT--
+ok
+ok
+ok
+ok
\ No newline at end of file
--- /dev/null
+--TEST--
+"defined" test
+--TEMPLATE--
+{{ definedVar is defined ? 'ok' : 'ko' }}
+{{ definedVar is not defined ? 'ko' : 'ok' }}
+{{ undefinedVar is defined ? 'ko' : 'ok' }}
+{{ undefinedVar is not defined ? 'ok' : 'ko' }}
+{{ zeroVar is defined ? 'ok' : 'ko' }}
+{{ nullVar is defined ? 'ok' : 'ko' }}
+{{ nested.definedVar is defined ? 'ok' : 'ko' }}
+{{ nested['definedVar'] is defined ? 'ok' : 'ko' }}
+{{ nested.definedVar is not defined ? 'ko' : 'ok' }}
+{{ nested.undefinedVar is defined ? 'ko' : 'ok' }}
+{{ nested['undefinedVar'] is defined ? 'ko' : 'ok' }}
+{{ nested.undefinedVar is not defined ? 'ok' : 'ko' }}
+{{ nested.zeroVar is defined ? 'ok' : 'ko' }}
+{{ nested.nullVar is defined ? 'ok' : 'ko' }}
+{{ nested.definedArray.0 is defined ? 'ok' : 'ko' }}
+{{ nested['definedArray'][0] is defined ? 'ok' : 'ko' }}
+{{ object.foo is defined ? 'ok' : 'ko' }}
+{{ object.undefinedMethod is defined ? 'ko' : 'ok' }}
+{{ object.getFoo() is defined ? 'ok' : 'ko' }}
+{{ object.getFoo('a') is defined ? 'ok' : 'ko' }}
+{{ object.undefinedMethod() is defined ? 'ko' : 'ok' }}
+{{ object.undefinedMethod('a') is defined ? 'ko' : 'ok' }}
+{{ object.self.foo is defined ? 'ok' : 'ko' }}
+{{ object.self.undefinedMethod is defined ? 'ko' : 'ok' }}
+{{ object.undefinedMethod.self is defined ? 'ko' : 'ok' }}
+--DATA--
+return array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'nullVar' => null,
+ 'nested' => array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'nullVar' => null,
+ 'definedArray' => array(0),
+ ),
+ 'object' => new TwigTestFoo(),
+);
+--EXPECT--
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+--DATA--
+return array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'nullVar' => null,
+ 'nested' => array(
+ 'definedVar' => 'defined',
+ 'zeroVar' => 0,
+ 'nullVar' => null,
+ 'definedArray' => array(0),
+ ),
+ 'object' => new TwigTestFoo(),
+);
+--CONFIG--
+return array('strict_variables' => false)
+--EXPECT--
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
+ok
--- /dev/null
+--TEST--
+"empty" test
+--TEMPLATE--
+{{ foo is empty ? 'ok' : 'ko' }}
+{{ bar is empty ? 'ok' : 'ko' }}
+{{ foobar is empty ? 'ok' : 'ko' }}
+{{ array is empty ? 'ok' : 'ko' }}
+{{ zero is empty ? 'ok' : 'ko' }}
+{{ string is empty ? 'ok' : 'ko' }}
+{{ countable_empty is empty ? 'ok' : 'ko' }}
+{{ countable_not_empty is empty ? 'ok' : 'ko' }}
+{{ markup_empty is empty ? 'ok' : 'ko' }}
+{{ markup_not_empty is empty ? 'ok' : 'ko' }}
+--DATA--
+
+class CountableStub implements Countable
+{
+ private $items;
+
+ public function __construct(array $items)
+ {
+ $this->items = $items;
+ }
+
+ public function count()
+ {
+ return count($this->items);
+ }
+}
+return array(
+ 'foo' => '', 'bar' => null, 'foobar' => false, 'array' => array(), 'zero' => 0, 'string' => '0',
+ 'countable_empty' => new CountableStub(array()), 'countable_not_empty' => new CountableStub(array(1, 2)),
+ 'markup_empty' => new Twig_Markup('', 'UTF-8'), 'markup_not_empty' => new Twig_Markup('test', 'UTF-8'),
+);
+--EXPECT--
+ok
+ok
+ok
+ok
+ko
+ko
+ok
+ko
+ok
+ko
--- /dev/null
+--TEST--
+"even" test
+--TEMPLATE--
+{{ 1 is even ? 'ko' : 'ok' }}
+{{ 2 is even ? 'ok' : 'ko' }}
+{{ 1 is not even ? 'ok' : 'ko' }}
+{{ 2 is not even ? 'ko' : 'ok' }}
+--DATA--
+return array()
+--EXPECT--
+ok
+ok
+ok
+ok
--- /dev/null
+--TEST--
+Twig supports the in operator
+--TEMPLATE--
+{% if bar in foo %}
+TRUE
+{% endif %}
+{% if not (bar in foo) %}
+{% else %}
+TRUE
+{% endif %}
+{% if bar not in foo %}
+{% else %}
+TRUE
+{% endif %}
+{% if 'a' in bar %}
+TRUE
+{% endif %}
+{% if 'c' not in bar %}
+TRUE
+{% endif %}
+{% if '' not in bar %}
+TRUE
+{% endif %}
+{% if '' in '' %}
+TRUE
+{% endif %}
+{% if '0' not in '' %}
+TRUE
+{% endif %}
+{% if 'a' not in '0' %}
+TRUE
+{% endif %}
+{% if '0' in '0' %}
+TRUE
+{% endif %}
+--DATA--
+return array('bar' => 'bar', 'foo' => array('bar' => 'bar'))
+--EXPECT--
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
+TRUE
--- /dev/null
+--TEST--
+Twig supports the in operator when using objects
+--TEMPLATE--
+{% if object in object_list %}
+TRUE
+{% endif %}
+--DATA--
+$foo = new TwigTestFoo();
+$foo1 = new TwigTestFoo();
+
+$foo->position = $foo1;
+$foo1->position = $foo;
+
+return array(
+ 'object' => $foo,
+ 'object_list' => array($foo1, $foo),
+);
+--EXPECT--
+TRUE
--- /dev/null
+--TEST--
+"iterable" test
+--TEMPLATE--
+{{ foo is iterable ? 'ok' : 'ko' }}
+{{ traversable is iterable ? 'ok' : 'ko' }}
+{{ obj is iterable ? 'ok' : 'ko' }}
+{{ val is iterable ? 'ok' : 'ko' }}
+--DATA--
+return array(
+ 'foo' => array(),
+ 'traversable' => new ArrayIterator(array()),
+ 'obj' => new stdClass(),
+ 'val' => 'test',
+);
+--EXPECT--
+ok
+ok
+ko
+ko
\ No newline at end of file
--- /dev/null
+--TEST--
+"odd" test
+--TEMPLATE--
+{{ 1 is odd ? 'ok' : 'ko' }}
+{{ 2 is odd ? 'ko' : 'ok' }}
+--DATA--
+return array()
+--EXPECT--
+ok
+ok
\ No newline at end of file
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+// This function is defined to check that escaping strategies
+// like html works even if a function with the same name is defined.
+function html()
+{
+ return 'foo';
+}
+
+class Twig_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
+{
+ public function getExtensions()
+ {
+ $policy = new Twig_Sandbox_SecurityPolicy(array(), array(), array(), array(), array());
+
+ return array(
+ new Twig_Extension_Debug(),
+ new Twig_Extension_Sandbox($policy, false),
+ new Twig_Extension_StringLoader(),
+ new TwigTestExtension(),
+ );
+ }
+
+ public function getFixturesDir()
+ {
+ return dirname(__FILE__).'/Fixtures/';
+ }
+}
+
+function test_foo($value = 'foo')
+{
+ return $value;
+}
+
+class TwigTestFoo implements Iterator
+{
+ const BAR_NAME = 'bar';
+
+ public $position = 0;
+ public $array = array(1, 2);
+
+ public function bar($param1 = null, $param2 = null)
+ {
+ return 'bar'.($param1 ? '_'.$param1 : '').($param2 ? '-'.$param2 : '');
+ }
+
+ public function getFoo()
+ {
+ return 'foo';
+ }
+
+ public function getSelf()
+ {
+ return $this;
+ }
+
+ public function is()
+ {
+ return 'is';
+ }
+
+ public function in()
+ {
+ return 'in';
+ }
+
+ public function not()
+ {
+ return 'not';
+ }
+
+ public function strToLower($value)
+ {
+ return strtolower($value);
+ }
+
+ public function rewind()
+ {
+ $this->position = 0;
+ }
+
+ public function current()
+ {
+ return $this->array[$this->position];
+ }
+
+ public function key()
+ {
+ return 'a';
+ }
+
+ public function next()
+ {
+ ++$this->position;
+ }
+
+ public function valid()
+ {
+ return isset($this->array[$this->position]);
+ }
+}
+
+class TwigTestTokenParser_§ extends Twig_TokenParser
+{
+ public function parse(Twig_Token $token)
+ {
+ $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Twig_Node_Print(new Twig_Node_Expression_Constant('§', -1), -1);
+ }
+
+ public function getTag()
+ {
+ return '§';
+ }
+}
+
+class TwigTestExtension extends Twig_Extension
+{
+ public function getTokenParsers()
+ {
+ return array(
+ new TwigTestTokenParser_§(),
+ );
+ }
+
+ public function getFilters()
+ {
+ return array(
+ '§' => new Twig_Filter_Method($this, '§Filter'),
+ 'escape_and_nl2br' => new Twig_Filter_Method($this, 'escape_and_nl2br', array('needs_environment' => true, 'is_safe' => array('html'))),
+ 'nl2br' => new Twig_Filter_Method($this, 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
+ 'escape_something' => new Twig_Filter_Method($this, 'escape_something', array('is_safe' => array('something'))),
+ 'preserves_safety' => new Twig_Filter_Method($this, 'preserves_safety', array('preserves_safety' => array('html'))),
+ '*_path' => new Twig_Filter_Method($this, 'dynamic_path'),
+ '*_foo_*_bar' => new Twig_Filter_Method($this, 'dynamic_foo'),
+ );
+ }
+
+ public function getFunctions()
+ {
+ return array(
+ '§' => new Twig_Function_Method($this, '§Function'),
+ 'safe_br' => new Twig_Function_Method($this, 'br', array('is_safe' => array('html'))),
+ 'unsafe_br' => new Twig_Function_Method($this, 'br'),
+ '*_path' => new Twig_Function_Method($this, 'dynamic_path'),
+ '*_foo_*_bar' => new Twig_Function_Method($this, 'dynamic_foo'),
+ );
+ }
+
+ public function §Filter($value)
+ {
+ return "§{$value}§";
+ }
+
+ public function §Function($value)
+ {
+ return "§{$value}§";
+ }
+
+ /**
+ * nl2br which also escapes, for testing escaper filters
+ */
+ public function escape_and_nl2br($env, $value, $sep = '<br />')
+ {
+ return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep);
+ }
+
+ /**
+ * nl2br only, for testing filters with pre_escape
+ */
+ public function nl2br($value, $sep = '<br />')
+ {
+ // not secure if $value contains html tags (not only entities)
+ // don't use
+ return str_replace("\n", "$sep\n", $value);
+ }
+
+ public function dynamic_path($element, $item)
+ {
+ return $element.'/'.$item;
+ }
+
+ public function dynamic_foo($foo, $bar, $item)
+ {
+ return $foo.'/'.$bar.'/'.$item;
+ }
+
+ public function escape_something($value)
+ {
+ return strtoupper($value);
+ }
+
+ public function preserves_safety($value)
+ {
+ return strtoupper($value);
+ }
+
+ public function br()
+ {
+ return '<br />';
+ }
+
+ public function getName()
+ {
+ return 'integration_test';
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Tests_LexerTest extends PHPUnit_Framework_TestCase
+{
+ public function testNameLabelForTag()
+ {
+ $template = '{% § %}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ $stream->expect(Twig_Token::BLOCK_START_TYPE);
+ $this->assertSame('§', $stream->expect(Twig_Token::NAME_TYPE)->getValue());
+ }
+
+ public function testNameLabelForFunction()
+ {
+ $template = '{{ §() }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $this->assertSame('§', $stream->expect(Twig_Token::NAME_TYPE)->getValue());
+ }
+
+ public function testBracketsNesting()
+ {
+ $template = '{{ {"a":{"b":"c"}} }}';
+
+ $this->assertEquals(2, $this->countToken($template, Twig_Token::PUNCTUATION_TYPE, '{'));
+ $this->assertEquals(2, $this->countToken($template, Twig_Token::PUNCTUATION_TYPE, '}'));
+ }
+
+ protected function countToken($template, $type, $value = null)
+ {
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ $count = 0;
+ $tokens = array();
+ while (!$stream->isEOF()) {
+ $token = $stream->next();
+ if ($type === $token->getType()) {
+ if (null === $value || $value === $token->getValue()) {
+ ++$count;
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ public function testLineDirective()
+ {
+ $template = "foo\n"
+ . "bar\n"
+ . "{% line 10 %}\n"
+ . "{{\n"
+ . "baz\n"
+ . "}}\n";
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ // foo\nbar\n
+ $this->assertSame(1, $stream->expect(Twig_Token::TEXT_TYPE)->getLine());
+ // \n (after {% line %})
+ $this->assertSame(10, $stream->expect(Twig_Token::TEXT_TYPE)->getLine());
+ // {{
+ $this->assertSame(11, $stream->expect(Twig_Token::VAR_START_TYPE)->getLine());
+ // baz
+ $this->assertSame(12, $stream->expect(Twig_Token::NAME_TYPE)->getLine());
+ }
+
+ public function testLineDirectiveInline()
+ {
+ $template = "foo\n"
+ . "bar{% line 10 %}{{\n"
+ . "baz\n"
+ . "}}\n";
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ // foo\nbar
+ $this->assertSame(1, $stream->expect(Twig_Token::TEXT_TYPE)->getLine());
+ // {{
+ $this->assertSame(10, $stream->expect(Twig_Token::VAR_START_TYPE)->getLine());
+ // baz
+ $this->assertSame(11, $stream->expect(Twig_Token::NAME_TYPE)->getLine());
+ }
+
+ public function testLongComments()
+ {
+ $template = '{# '.str_repeat('*', 100000).' #}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $lexer->tokenize($template);
+
+ // should not throw an exception
+ }
+
+ public function testLongRaw()
+ {
+ $template = '{% raw %}'.str_repeat('*', 100000).'{% endraw %}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ // should not throw an exception
+ }
+
+ public function testLongVar()
+ {
+ $template = '{{ '.str_repeat('x', 100000).' }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ // should not throw an exception
+ }
+
+ public function testLongBlock()
+ {
+ $template = '{% '.str_repeat('x', 100000).' %}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+
+ // should not throw an exception
+ }
+
+ public function testBigNumbers()
+ {
+ $template = '{{ 922337203685477580700 }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $node = $stream->next();
+ $node = $stream->next();
+ $this->assertEquals(922337203685477580700, $node->getValue());
+ }
+
+ public function testStringWithEscapedDelimiter()
+ {
+ $tests = array(
+ "{{ 'foo \' bar' }}" => 'foo \' bar',
+ '{{ "foo \" bar" }}' => "foo \" bar",
+ );
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ foreach ($tests as $template => $expected) {
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, $expected);
+ }
+ }
+
+ public function testStringWithInterpolation()
+ {
+ $template = 'foo {{ "bar #{ baz + 1 }" }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::TEXT_TYPE, 'foo ');
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'bar ');
+ $stream->expect(Twig_Token::INTERPOLATION_START_TYPE);
+ $stream->expect(Twig_Token::NAME_TYPE, 'baz');
+ $stream->expect(Twig_Token::OPERATOR_TYPE, '+');
+ $stream->expect(Twig_Token::NUMBER_TYPE, '1');
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $stream->expect(Twig_Token::VAR_END_TYPE);
+ }
+
+ public function testStringWithEscapedInterpolation()
+ {
+ $template = '{{ "bar \#{baz+1}" }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'bar #{baz+1}');
+ $stream->expect(Twig_Token::VAR_END_TYPE);
+ }
+
+ public function testStringWithHash()
+ {
+ $template = '{{ "bar # baz" }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'bar # baz');
+ $stream->expect(Twig_Token::VAR_END_TYPE);
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unclosed """
+ */
+ public function testStringWithUnterminatedInterpolation()
+ {
+ $template = '{{ "bar #{x" }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ }
+
+ public function testStringWithNestedInterpolations()
+ {
+ $template = '{{ "bar #{ "foo#{bar}" }" }}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'bar ');
+ $stream->expect(Twig_Token::INTERPOLATION_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'foo');
+ $stream->expect(Twig_Token::INTERPOLATION_START_TYPE);
+ $stream->expect(Twig_Token::NAME_TYPE, 'bar');
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $stream->expect(Twig_Token::VAR_END_TYPE);
+ }
+
+ public function testStringWithNestedInterpolationsInBlock()
+ {
+ $template = '{% foo "bar #{ "foo#{bar}" }" %}';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::BLOCK_START_TYPE);
+ $stream->expect(Twig_Token::NAME_TYPE, 'foo');
+ $stream->expect(Twig_Token::STRING_TYPE, 'bar ');
+ $stream->expect(Twig_Token::INTERPOLATION_START_TYPE);
+ $stream->expect(Twig_Token::STRING_TYPE, 'foo');
+ $stream->expect(Twig_Token::INTERPOLATION_START_TYPE);
+ $stream->expect(Twig_Token::NAME_TYPE, 'bar');
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $stream->expect(Twig_Token::BLOCK_END_TYPE);
+ }
+
+ public function testOperatorEndingWithALetterAtTheEndOfALine()
+ {
+ $template = "{{ 1 and\n0}}";
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ $stream->expect(Twig_Token::VAR_START_TYPE);
+ $stream->expect(Twig_Token::NUMBER_TYPE, 1);
+ $stream->expect(Twig_Token::OPERATOR_TYPE, 'and');
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unclosed "variable" at line 3
+ */
+ public function testUnterminatedVariable()
+ {
+ $template = '
+
+{{
+
+bar
+
+
+';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unclosed "block" at line 3
+ */
+ public function testUnterminatedBlock()
+ {
+ $template = '
+
+{%
+
+bar
+
+
+';
+
+ $lexer = new Twig_Lexer(new Twig_Environment());
+ $stream = $lexer->tokenize($template);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Loader_ArrayTest extends PHPUnit_Framework_TestCase
+{
+ public function testGetSource()
+ {
+ $loader = new Twig_Loader_Array(array('foo' => 'bar'));
+
+ $this->assertEquals('bar', $loader->getSource('foo'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Loader
+ */
+ public function testGetSourceWhenTemplateDoesNotExist()
+ {
+ $loader = new Twig_Loader_Array(array());
+
+ $loader->getSource('foo');
+ }
+
+ public function testGetCacheKey()
+ {
+ $loader = new Twig_Loader_Array(array('foo' => 'bar'));
+
+ $this->assertEquals('bar', $loader->getCacheKey('foo'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Loader
+ */
+ public function testGetCacheKeyWhenTemplateDoesNotExist()
+ {
+ $loader = new Twig_Loader_Array(array());
+
+ $loader->getCacheKey('foo');
+ }
+
+ public function testSetTemplate()
+ {
+ $loader = new Twig_Loader_Array(array());
+ $loader->setTemplate('foo', 'bar');
+
+ $this->assertEquals('bar', $loader->getSource('foo'));
+ }
+
+ public function testIsFresh()
+ {
+ $loader = new Twig_Loader_Array(array('foo' => 'bar'));
+ $this->assertTrue($loader->isFresh('foo', time()));
+ }
+
+ /**
+ * @expectedException Twig_Error_Loader
+ */
+ public function testIsFreshWhenTemplateDoesNotExist()
+ {
+ $loader = new Twig_Loader_Array(array());
+
+ $loader->isFresh('foo', time());
+ }
+
+ public function testTemplateReference()
+ {
+ $name = new Twig_Test_Loader_TemplateReference('foo');
+ $loader = new Twig_Loader_Array(array('foo' => 'bar'));
+
+ $loader->getCacheKey($name);
+ $loader->getSource($name);
+ $loader->isFresh($name, time());
+ $loader->setTemplate($name, 'foobar');
+ }
+}
+
+class Twig_Test_Loader_TemplateReference
+{
+ private $name;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function __toString()
+ {
+ return $this->name;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Loader_ChainTest extends PHPUnit_Framework_TestCase
+{
+ public function testGetSource()
+ {
+ $loader = new Twig_Loader_Chain(array(
+ new Twig_Loader_Array(array('foo' => 'bar')),
+ new Twig_Loader_Array(array('foo' => 'foobar', 'bar' => 'foo')),
+ ));
+
+ $this->assertEquals('bar', $loader->getSource('foo'));
+ $this->assertEquals('foo', $loader->getSource('bar'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Loader
+ */
+ public function testGetSourceWhenTemplateDoesNotExist()
+ {
+ $loader = new Twig_Loader_Chain(array());
+
+ $loader->getSource('foo');
+ }
+
+ public function testGetCacheKey()
+ {
+ $loader = new Twig_Loader_Chain(array(
+ new Twig_Loader_Array(array('foo' => 'bar')),
+ new Twig_Loader_Array(array('foo' => 'foobar', 'bar' => 'foo')),
+ ));
+
+ $this->assertEquals('bar', $loader->getCacheKey('foo'));
+ $this->assertEquals('foo', $loader->getCacheKey('bar'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Loader
+ */
+ public function testGetCacheKeyWhenTemplateDoesNotExist()
+ {
+ $loader = new Twig_Loader_Chain(array());
+
+ $loader->getCacheKey('foo');
+ }
+
+ public function testAddLoader()
+ {
+ $loader = new Twig_Loader_Chain();
+ $loader->addLoader(new Twig_Loader_Array(array('foo' => 'bar')));
+
+ $this->assertEquals('bar', $loader->getSource('foo'));
+ }
+
+ public function testExists()
+ {
+ $loader1 = $this->getMock('Twig_Loader_Array', array('exists', 'getSource'), array(), '', false);
+ $loader1->expects($this->once())->method('exists')->will($this->returnValue(false));
+ $loader1->expects($this->never())->method('getSource');
+
+ $loader2 = $this->getMock('Twig_LoaderInterface');
+ $loader2->expects($this->once())->method('getSource')->will($this->returnValue('content'));
+
+ $loader = new Twig_Loader_Chain();
+ $loader->addLoader($loader1);
+ $loader->addLoader($loader2);
+
+ $this->assertTrue($loader->exists('foo'));
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Loader_FilesystemTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getSecurityTests
+ */
+ public function testSecurity($template)
+ {
+ $loader = new Twig_Loader_Filesystem(array(dirname(__FILE__).'/../Fixtures'));
+
+ try {
+ $loader->getCacheKey($template);
+ $this->fail();
+ } catch (Twig_Error_Loader $e) {
+ $this->assertNotContains('Unable to find template', $e->getMessage());
+ }
+ }
+
+ public function getSecurityTests()
+ {
+ return array(
+ array("AutoloaderTest\0.php"),
+ array('..\\AutoloaderTest.php'),
+ array('..\\\\\\AutoloaderTest.php'),
+ array('../AutoloaderTest.php'),
+ array('..////AutoloaderTest.php'),
+ array('./../AutoloaderTest.php'),
+ array('.\\..\\AutoloaderTest.php'),
+ array('././././././../AutoloaderTest.php'),
+ array('.\\./.\\./.\\./../AutoloaderTest.php'),
+ array('foo/../../AutoloaderTest.php'),
+ array('foo\\..\\..\\AutoloaderTest.php'),
+ array('foo/../bar/../../AutoloaderTest.php'),
+ array('foo/bar/../../../AutoloaderTest.php'),
+ array('filters/../../AutoloaderTest.php'),
+ array('filters//..//..//AutoloaderTest.php'),
+ array('filters\\..\\..\\AutoloaderTest.php'),
+ array('filters\\\\..\\\\..\\\\AutoloaderTest.php'),
+ array('filters\\//../\\/\\..\\AutoloaderTest.php'),
+ array('/../AutoloaderTest.php'),
+ );
+ }
+
+ public function testPaths()
+ {
+ $basePath = dirname(__FILE__).'/Fixtures';
+
+ $loader = new Twig_Loader_Filesystem(array($basePath.'/normal', $basePath.'/normal_bis'));
+ $loader->setPaths(array($basePath.'/named', $basePath.'/named_bis'), 'named');
+ $loader->addPath($basePath.'/named_ter', 'named');
+ $loader->addPath($basePath.'/normal_ter');
+ $loader->prependPath($basePath.'/normal_final');
+ $loader->prependPath($basePath.'/named_final', 'named');
+
+ $this->assertEquals(array(
+ $basePath.'/normal_final',
+ $basePath.'/normal',
+ $basePath.'/normal_bis',
+ $basePath.'/normal_ter',
+ ), $loader->getPaths());
+ $this->assertEquals(array(
+ $basePath.'/named_final',
+ $basePath.'/named',
+ $basePath.'/named_bis',
+ $basePath.'/named_ter',
+ ), $loader->getPaths('named'));
+
+ $this->assertEquals("path (final)\n", $loader->getSource('index.html'));
+ $this->assertEquals("path (final)\n", $loader->getSource('@__main__/index.html'));
+ $this->assertEquals("named path (final)\n", $loader->getSource('@named/index.html'));
+ }
+
+ public function testEmptyConstructor()
+ {
+ $loader = new Twig_Loader_Filesystem();
+ $this->assertEquals(array(), $loader->getPaths());
+ }
+
+ public function testGetNamespaces()
+ {
+ $loader = new Twig_Loader_Filesystem(sys_get_temp_dir());
+ $this->assertEquals(array(Twig_Loader_Filesystem::MAIN_NAMESPACE), $loader->getNamespaces());
+
+ $loader->addPath(sys_get_temp_dir(), 'named');
+ $this->assertEquals(array(Twig_Loader_Filesystem::MAIN_NAMESPACE, 'named'), $loader->getNamespaces());
+ }
+}
--- /dev/null
+named path
--- /dev/null
+named path (bis)
--- /dev/null
+named path (final)
--- /dev/null
+named path (ter)
--- /dev/null
+path (bis)
--- /dev/null
+path (final)
--- /dev/null
+path (ter)
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_NativeExtensionTest extends PHPUnit_Framework_TestCase
+{
+ public function testGetProperties()
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String(), array(
+ 'debug' => true,
+ 'cache' => false,
+ 'autoescape' => false
+ ));
+
+ $d1 = new DateTime();
+ $d2 = new DateTime();
+ $output = $twig->render('{{ d1.date }}{{ d2.date }}', compact('d1', 'd2'));
+
+ // If it fails, PHP will crash.
+ $this->assertEquals($output, $d1->date . $d2->date);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_AutoEscapeTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_AutoEscape::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node(array(new Twig_Node_Text('foo', 1)));
+ $node = new Twig_Node_AutoEscape(true, $body, 1);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ $this->assertEquals(true, $node->getAttribute('value'));
+ }
+
+ /**
+ * @covers Twig_Node_AutoEscape::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $body = new Twig_Node(array(new Twig_Node_Text('foo', 1)));
+ $node = new Twig_Node_AutoEscape(true, $body, 1);
+
+ return array(
+ array($node, "// line 1\necho \"foo\";"),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_BlockReferenceTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_BlockReference::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_BlockReference('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_BlockReference::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ return array(
+ array(new Twig_Node_BlockReference('foo', 1), <<<EOF
+// line 1
+\$this->displayBlock('foo', \$context, \$blocks);
+EOF
+ ),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_BlockTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Block::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $node = new Twig_Node_Block('foo', $body, 1);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Block::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $node = new Twig_Node_Block('foo', $body, 1);
+
+ return array(
+ array($node, <<<EOF
+// line 1
+public function block_foo(\$context, array \$blocks = array())
+{
+ echo "foo";
+}
+EOF
+ ),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_DoTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Do::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $node = new Twig_Node_Do($expr, 1);
+
+ $this->assertEquals($expr, $node->getNode('expr'));
+ }
+
+ /**
+ * @covers Twig_Node_Do::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $node = new Twig_Node_Do($expr, 1);
+ $tests[] = array($node, "// line 1\n\"foo\";");
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_ArrayTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Array::__construct
+ */
+ public function testConstructor()
+ {
+ $elements = array(new Twig_Node_Expression_Constant('foo', 1), $foo = new Twig_Node_Expression_Constant('bar', 1));
+ $node = new Twig_Node_Expression_Array($elements, 1);
+
+ $this->assertEquals($foo, $node->getNode(1));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Array::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $elements = array(
+ new Twig_Node_Expression_Constant('foo', 1),
+ new Twig_Node_Expression_Constant('bar', 1),
+
+ new Twig_Node_Expression_Constant('bar', 1),
+ new Twig_Node_Expression_Constant('foo', 1),
+ );
+ $node = new Twig_Node_Expression_Array($elements, 1);
+
+ return array(
+ array($node, 'array("foo" => "bar", "bar" => "foo")'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_AssignNameTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_AssignName::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_Expression_AssignName('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_AssignName::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $node = new Twig_Node_Expression_AssignName('foo', 1);
+
+ return array(
+ array($node, '$context["foo"]'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_AddTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Add::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Add($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Add::compile
+ * @covers Twig_Node_Expression_Binary_Add::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Add($left, $right, 1);
+
+ return array(
+ array($node, '(1 + 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_AndTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_And::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_And($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_And::compile
+ * @covers Twig_Node_Expression_Binary_And::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_And($left, $right, 1);
+
+ return array(
+ array($node, '(1 && 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_ConcatTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Concat::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Concat($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Concat::compile
+ * @covers Twig_Node_Expression_Binary_Concat::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Concat($left, $right, 1);
+
+ return array(
+ array($node, '(1 . 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_DivTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Div::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Div($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Div::compile
+ * @covers Twig_Node_Expression_Binary_Div::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Div($left, $right, 1);
+
+ return array(
+ array($node, '(1 / 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_FloorDivTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_FloorDiv::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_FloorDiv($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_FloorDiv::compile
+ * @covers Twig_Node_Expression_Binary_FloorDiv::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_FloorDiv($left, $right, 1);
+
+ return array(
+ array($node, 'intval(floor((1 / 2)))'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_ModTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Mod::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Mod($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Mod::compile
+ * @covers Twig_Node_Expression_Binary_Mod::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Mod($left, $right, 1);
+
+ return array(
+ array($node, '(1 % 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_MulTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Mul::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Mul($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Mul::compile
+ * @covers Twig_Node_Expression_Binary_Mul::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Mul($left, $right, 1);
+
+ return array(
+ array($node, '(1 * 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_OrTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Or::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Or($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Or::compile
+ * @covers Twig_Node_Expression_Binary_Or::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Or($left, $right, 1);
+
+ return array(
+ array($node, '(1 || 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Binary_SubTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Binary_Sub::__construct
+ */
+ public function testConstructor()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Sub($left, $right, 1);
+
+ $this->assertEquals($left, $node->getNode('left'));
+ $this->assertEquals($right, $node->getNode('right'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Binary_Sub::compile
+ * @covers Twig_Node_Expression_Binary_Sub::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $left = new Twig_Node_Expression_Constant(1, 1);
+ $right = new Twig_Node_Expression_Constant(2, 1);
+ $node = new Twig_Node_Expression_Binary_Sub($left, $right, 1);
+
+ return array(
+ array($node, '(1 - 2)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_CallTest extends PHPUnit_Framework_TestCase
+{
+ public function testGetArguments()
+ {
+ $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'date'));
+ $this->assertEquals(array('U'), $node->getArguments('date', array('format' => 'U')));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Positional arguments cannot be used after named arguments for function "date".
+ */
+ public function testGetArgumentsWhenPositionalArgumentsAfterNamedArguments()
+ {
+ $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'date'));
+ $node->getArguments('date', array('timestamp' => 123456, 'Y-m-d'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Argument "format" is defined twice for function "date".
+ */
+ public function testGetArgumentsWhenArgumentIsDefinedTwice()
+ {
+ $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'date'));
+ $node->getArguments('date', array('Y-m-d', 'format' => 'U'));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unknown argument "unknown" for function "date".
+ */
+ public function testGetArgumentsWithWrongNamedArgumentName()
+ {
+ $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'date'));
+ $node->getArguments('date', array('Y-m-d', 'unknown' => ''));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unknown arguments "unknown1", "unknown2" for function "date".
+ */
+ public function testGetArgumentsWithWrongNamedArgumentNames()
+ {
+ $node = new Twig_Tests_Node_Expression_Call(array(), array('type' => 'function', 'name' => 'date'));
+ $node->getArguments('date', array('Y-m-d', 'unknown1' => '', 'unknown2' => ''));
+ }
+}
+
+class Twig_Tests_Node_Expression_Call extends Twig_Node_Expression_Call
+{
+ public function getArguments($callable, $arguments)
+ {
+ return parent::getArguments($callable, $arguments);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_ConditionalTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Conditional::__construct
+ */
+ public function testConstructor()
+ {
+ $expr1 = new Twig_Node_Expression_Constant(1, 1);
+ $expr2 = new Twig_Node_Expression_Constant(2, 1);
+ $expr3 = new Twig_Node_Expression_Constant(3, 1);
+ $node = new Twig_Node_Expression_Conditional($expr1, $expr2, $expr3, 1);
+
+ $this->assertEquals($expr1, $node->getNode('expr1'));
+ $this->assertEquals($expr2, $node->getNode('expr2'));
+ $this->assertEquals($expr3, $node->getNode('expr3'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Conditional::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr1 = new Twig_Node_Expression_Constant(1, 1);
+ $expr2 = new Twig_Node_Expression_Constant(2, 1);
+ $expr3 = new Twig_Node_Expression_Constant(3, 1);
+ $node = new Twig_Node_Expression_Conditional($expr1, $expr2, $expr3, 1);
+ $tests[] = array($node, '((1) ? (2) : (3))');
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_ConstantTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Constant::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_Expression_Constant('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('value'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Constant::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $node = new Twig_Node_Expression_Constant('foo', 1);
+ $tests[] = array($node, '"foo"');
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_FilterTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Filter::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $name = new Twig_Node_Expression_Constant('upper', 1);
+ $args = new Twig_Node();
+ $node = new Twig_Node_Expression_Filter($expr, $name, $args, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ $this->assertEquals($name, $node->getNode('filter'));
+ $this->assertEquals($args, $node->getNode('arguments'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Filter::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $node = $this->createFilter($expr, 'upper');
+ $node = $this->createFilter($node, 'number_format', array(new Twig_Node_Expression_Constant(2, 1), new Twig_Node_Expression_Constant('.', 1), new Twig_Node_Expression_Constant(',', 1)));
+
+ if (function_exists('mb_get_info')) {
+ $tests[] = array($node, 'twig_number_format_filter($this->env, twig_upper_filter($this->env, "foo"), 2, ".", ",")');
+ } else {
+ $tests[] = array($node, 'twig_number_format_filter($this->env, strtoupper("foo"), 2, ".", ",")');
+ }
+
+ // named arguments
+ $date = new Twig_Node_Expression_Constant(0, 1);
+ $node = $this->createFilter($date, 'date', array(
+ 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+ 'format' => new Twig_Node_Expression_Constant('d/m/Y H:i:s P', 1),
+ ));
+ $tests[] = array($node, 'twig_date_format_filter($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")');
+
+ // skip an optional argument
+ $date = new Twig_Node_Expression_Constant(0, 1);
+ $node = $this->createFilter($date, 'date', array(
+ 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+ ));
+ $tests[] = array($node, 'twig_date_format_filter($this->env, 0, null, "America/Chicago")');
+
+ // underscores vs camelCase for named arguments
+ $string = new Twig_Node_Expression_Constant('abc', 1);
+ $node = $this->createFilter($string, 'reverse', array(
+ 'preserve_keys' => new Twig_Node_Expression_Constant(true, 1),
+ ));
+ $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)');
+ $node = $this->createFilter($string, 'reverse', array(
+ 'preserveKeys' => new Twig_Node_Expression_Constant(true, 1),
+ ));
+ $tests[] = array($node, 'twig_reverse_filter($this->env, "abc", true)');
+
+ // filter as an anonymous function
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ $node = $this->createFilter(new Twig_Node_Expression_Constant('foo', 1), 'anonymous');
+ $tests[] = array($node, 'call_user_func_array($this->env->getFilter(\'anonymous\')->getCallable(), array("foo"))');
+ }
+
+ return $tests;
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unknown argument "foobar" for filter "date".
+ */
+ public function testCompileWithWrongNamedArgumentName()
+ {
+ $date = new Twig_Node_Expression_Constant(0, 1);
+ $node = $this->createFilter($date, 'date', array(
+ 'foobar' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+ ));
+
+ $compiler = $this->getCompiler();
+ $compiler->compile($node);
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Value for argument "from" is required for filter "replace".
+ */
+ public function testCompileWithMissingNamedArgument()
+ {
+ $value = new Twig_Node_Expression_Constant(0, 1);
+ $node = $this->createFilter($value, 'replace', array(
+ 'to' => new Twig_Node_Expression_Constant('foo', 1),
+ ));
+
+ $compiler = $this->getCompiler();
+ $compiler->compile($node);
+ }
+
+ protected function createFilter($node, $name, array $arguments = array())
+ {
+ $name = new Twig_Node_Expression_Constant($name, 1);
+ $arguments = new Twig_Node($arguments);
+
+ return new Twig_Node_Expression_Filter($node, $name, $arguments, 1);
+ }
+
+ protected function getEnvironment()
+ {
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ return include 'PHP53/FilterInclude.php';
+ }
+
+ return parent::getEnvironment();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_FunctionTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Function::__construct
+ */
+ public function testConstructor()
+ {
+ $name = 'function';
+ $args = new Twig_Node();
+ $node = new Twig_Node_Expression_Function($name, $args, 1);
+
+ $this->assertEquals($name, $node->getAttribute('name'));
+ $this->assertEquals($args, $node->getNode('arguments'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Function::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $environment = new Twig_Environment();
+ $environment->addFunction('foo', new Twig_Function_Function('foo', array()));
+ $environment->addFunction('bar', new Twig_Function_Function('bar', array('needs_environment' => true)));
+ $environment->addFunction('foofoo', new Twig_Function_Function('foofoo', array('needs_context' => true)));
+ $environment->addFunction('foobar', new Twig_Function_Function('foobar', array('needs_environment' => true, 'needs_context' => true)));
+
+ $tests = array();
+
+ $node = $this->createFunction('foo');
+ $tests[] = array($node, 'foo()', $environment);
+
+ $node = $this->createFunction('foo', array(new Twig_Node_Expression_Constant('bar', 1), new Twig_Node_Expression_Constant('foobar', 1)));
+ $tests[] = array($node, 'foo("bar", "foobar")', $environment);
+
+ $node = $this->createFunction('bar');
+ $tests[] = array($node, 'bar($this->env)', $environment);
+
+ $node = $this->createFunction('bar', array(new Twig_Node_Expression_Constant('bar', 1)));
+ $tests[] = array($node, 'bar($this->env, "bar")', $environment);
+
+ $node = $this->createFunction('foofoo');
+ $tests[] = array($node, 'foofoo($context)', $environment);
+
+ $node = $this->createFunction('foofoo', array(new Twig_Node_Expression_Constant('bar', 1)));
+ $tests[] = array($node, 'foofoo($context, "bar")', $environment);
+
+ $node = $this->createFunction('foobar');
+ $tests[] = array($node, 'foobar($this->env, $context)', $environment);
+
+ $node = $this->createFunction('foobar', array(new Twig_Node_Expression_Constant('bar', 1)));
+ $tests[] = array($node, 'foobar($this->env, $context, "bar")', $environment);
+
+ // named arguments
+ $node = $this->createFunction('date', array(
+ 'timezone' => new Twig_Node_Expression_Constant('America/Chicago', 1),
+ 'date' => new Twig_Node_Expression_Constant(0, 1),
+ ));
+ $tests[] = array($node, 'twig_date_converter($this->env, 0, "America/Chicago")');
+
+ // function as an anonymous function
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ $node = $this->createFunction('anonymous', array(new Twig_Node_Expression_Constant('foo', 1)));
+ $tests[] = array($node, 'call_user_func_array($this->env->getFunction(\'anonymous\')->getCallable(), array("foo"))');
+ }
+
+ return $tests;
+ }
+
+ protected function createFunction($name, array $arguments = array())
+ {
+ return new Twig_Node_Expression_Function($name, new Twig_Node($arguments), 1);
+ }
+
+ protected function getEnvironment()
+ {
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ return include 'PHP53/FunctionInclude.php';
+ }
+
+ return parent::getEnvironment();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_GetAttrTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_GetAttr::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Name('foo', 1);
+ $attr = new Twig_Node_Expression_Constant('bar', 1);
+ $args = new Twig_Node_Expression_Array(array(), 1);
+ $args->addElement(new Twig_Node_Expression_Name('foo', 1));
+ $args->addElement(new Twig_Node_Expression_Constant('bar', 1));
+ $node = new Twig_Node_Expression_GetAttr($expr, $attr, $args, Twig_TemplateInterface::ARRAY_CALL, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ $this->assertEquals($attr, $node->getNode('attribute'));
+ $this->assertEquals($args, $node->getNode('arguments'));
+ $this->assertEquals(Twig_TemplateInterface::ARRAY_CALL, $node->getAttribute('type'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_GetAttr::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr = new Twig_Node_Expression_Name('foo', 1);
+ $attr = new Twig_Node_Expression_Constant('bar', 1);
+ $args = new Twig_Node_Expression_Array(array(), 1);
+ $node = new Twig_Node_Expression_GetAttr($expr, $attr, $args, Twig_TemplateInterface::ANY_CALL, 1);
+ $tests[] = array($node, sprintf('%s%s, "bar")', $this->getAttributeGetter(), $this->getVariableGetter('foo')));
+
+ $node = new Twig_Node_Expression_GetAttr($expr, $attr, $args, Twig_TemplateInterface::ARRAY_CALL, 1);
+ $tests[] = array($node, sprintf('%s%s, "bar", array(), "array")', $this->getAttributeGetter(), $this->getVariableGetter('foo')));
+
+ $args = new Twig_Node_Expression_Array(array(), 1);
+ $args->addElement(new Twig_Node_Expression_Name('foo', 1));
+ $args->addElement(new Twig_Node_Expression_Constant('bar', 1));
+ $node = new Twig_Node_Expression_GetAttr($expr, $attr, $args, Twig_TemplateInterface::METHOD_CALL, 1);
+ $tests[] = array($node, sprintf('%s%s, "bar", array(0 => %s, 1 => "bar"), "method")', $this->getAttributeGetter(), $this->getVariableGetter('foo'), $this->getVariableGetter('foo')));
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_NameTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Name::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_Expression_Name('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Name::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $node = new Twig_Node_Expression_Name('foo', 1);
+ $self = new Twig_Node_Expression_Name('_self', 1);
+ $context = new Twig_Node_Expression_Name('_context', 1);
+
+ $env = new Twig_Environment(null, array('strict_variables' => true));
+ $env1 = new Twig_Environment(null, array('strict_variables' => false));
+
+ return array(
+ version_compare(PHP_VERSION, '5.4.0') >= 0 ? array($node, '(isset($context["foo"]) ? $context["foo"] : $this->getContext($context, "foo"))', $env) : array($node, '$this->getContext($context, "foo")', $env),
+ array($node, $this->getVariableGetter('foo'), $env1),
+ array($self, '$this'),
+ array($context, '$context'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+$env = new Twig_Environment();
+$env->addFilter(new Twig_SimpleFilter('anonymous', function () {}));
+
+return $env;
--- /dev/null
+<?php
+
+$env = new Twig_Environment();
+$env->addFunction(new Twig_SimpleFunction('anonymous', function () {}));
+
+return $env;
--- /dev/null
+<?php
+
+$env = new Twig_Environment();
+$env->addTest(new Twig_SimpleTest('anonymous', function () {}));
+
+return $env;
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_ParentTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Parent::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_Expression_Parent('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Parent::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+ $tests[] = array(new Twig_Node_Expression_Parent('foo', 1), '$this->renderParentBlock("foo", $context, $blocks)');
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_TestTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Test::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $name = new Twig_Node_Expression_Constant('null', 1);
+ $args = new Twig_Node();
+ $node = new Twig_Node_Expression_Test($expr, $name, $args, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ $this->assertEquals($args, $node->getNode('arguments'));
+ $this->assertEquals($name, $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Test::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $node = new Twig_Node_Expression_Test_Null($expr, 'null', new Twig_Node(array()), 1);
+ $tests[] = array($node, '(null === "foo")');
+
+ // test as an anonymous function
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ $node = $this->createTest(new Twig_Node_Expression_Constant('foo', 1), 'anonymous', array(new Twig_Node_Expression_Constant('foo', 1)));
+ $tests[] = array($node, 'call_user_func_array($this->env->getTest(\'anonymous\')->getCallable(), array("foo", "foo"))');
+ }
+
+ return $tests;
+ }
+
+ protected function createTest($node, $name, array $arguments = array())
+ {
+ return new Twig_Node_Expression_Test($node, $name, new Twig_Node($arguments), 1);
+ }
+
+ protected function getEnvironment()
+ {
+ if (version_compare(phpversion(), '5.3.0', '>=')) {
+ return include 'PHP53/TestInclude.php';
+ }
+
+ return parent::getEnvironment();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Unary_NegTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Unary_Neg::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Neg($expr, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Unary_Neg::compile
+ * @covers Twig_Node_Expression_Unary_Neg::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $node = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Neg($node, 1);
+
+ return array(
+ array($node, '(-1)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Unary_NotTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Unary_Not::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Not($expr, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Unary_Not::compile
+ * @covers Twig_Node_Expression_Unary_Not::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $node = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Not($node, 1);
+
+ return array(
+ array($node, '(!1)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_Expression_Unary_PosTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Expression_Unary_Pos::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Pos($expr, 1);
+
+ $this->assertEquals($expr, $node->getNode('node'));
+ }
+
+ /**
+ * @covers Twig_Node_Expression_Unary_Pos::compile
+ * @covers Twig_Node_Expression_Unary_Pos::operator
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $node = new Twig_Node_Expression_Constant(1, 1);
+ $node = new Twig_Node_Expression_Unary_Pos($node, 1);
+
+ return array(
+ array($node, '(+1)'),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_ForTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_For::__construct
+ */
+ public function testConstructor()
+ {
+ $keyTarget = new Twig_Node_Expression_AssignName('key', 1);
+ $valueTarget = new Twig_Node_Expression_AssignName('item', 1);
+ $seq = new Twig_Node_Expression_Name('items', 1);
+ $ifexpr = new Twig_Node_Expression_Constant(true, 1);
+ $body = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1)), array(), 1);
+ $else = null;
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', false);
+
+ $this->assertEquals($keyTarget, $node->getNode('key_target'));
+ $this->assertEquals($valueTarget, $node->getNode('value_target'));
+ $this->assertEquals($seq, $node->getNode('seq'));
+ $this->assertTrue($node->getAttribute('ifexpr'));
+ $this->assertEquals('Twig_Node_If', get_class($node->getNode('body')));
+ $this->assertEquals($body, $node->getNode('body')->getNode('tests')->getNode(1)->getNode(0));
+ $this->assertEquals(null, $node->getNode('else'));
+
+ $else = new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1);
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', false);
+ $this->assertEquals($else, $node->getNode('else'));
+ }
+
+ /**
+ * @covers Twig_Node_For::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $keyTarget = new Twig_Node_Expression_AssignName('key', 1);
+ $valueTarget = new Twig_Node_Expression_AssignName('item', 1);
+ $seq = new Twig_Node_Expression_Name('items', 1);
+ $ifexpr = null;
+ $body = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1)), array(), 1);
+ $else = null;
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', false);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context['_parent'] = (array) \$context;
+\$context['_seq'] = twig_ensure_traversable({$this->getVariableGetter('items')});
+foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) {
+ echo {$this->getVariableGetter('foo')};
+}
+\$_parent = \$context['_parent'];
+unset(\$context['_seq'], \$context['_iterated'], \$context['key'], \$context['item'], \$context['_parent'], \$context['loop']);
+\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
+EOF
+ );
+
+ $keyTarget = new Twig_Node_Expression_AssignName('k', 1);
+ $valueTarget = new Twig_Node_Expression_AssignName('v', 1);
+ $seq = new Twig_Node_Expression_Name('values', 1);
+ $ifexpr = null;
+ $body = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1)), array(), 1);
+ $else = null;
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', true);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context['_parent'] = (array) \$context;
+\$context['_seq'] = twig_ensure_traversable({$this->getVariableGetter('values')});
+\$context['loop'] = array(
+ 'parent' => \$context['_parent'],
+ 'index0' => 0,
+ 'index' => 1,
+ 'first' => true,
+);
+if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_seq'] instanceof Countable)) {
+ \$length = count(\$context['_seq']);
+ \$context['loop']['revindex0'] = \$length - 1;
+ \$context['loop']['revindex'] = \$length;
+ \$context['loop']['length'] = \$length;
+ \$context['loop']['last'] = 1 === \$length;
+}
+foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
+ echo {$this->getVariableGetter('foo')};
+ ++\$context['loop']['index0'];
+ ++\$context['loop']['index'];
+ \$context['loop']['first'] = false;
+ if (isset(\$context['loop']['length'])) {
+ --\$context['loop']['revindex0'];
+ --\$context['loop']['revindex'];
+ \$context['loop']['last'] = 0 === \$context['loop']['revindex0'];
+ }
+}
+\$_parent = \$context['_parent'];
+unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
+\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
+EOF
+ );
+
+ $keyTarget = new Twig_Node_Expression_AssignName('k', 1);
+ $valueTarget = new Twig_Node_Expression_AssignName('v', 1);
+ $seq = new Twig_Node_Expression_Name('values', 1);
+ $ifexpr = new Twig_Node_Expression_Constant(true, 1);
+ $body = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1)), array(), 1);
+ $else = null;
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', true);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context['_parent'] = (array) \$context;
+\$context['_seq'] = twig_ensure_traversable({$this->getVariableGetter('values')});
+\$context['loop'] = array(
+ 'parent' => \$context['_parent'],
+ 'index0' => 0,
+ 'index' => 1,
+ 'first' => true,
+);
+foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
+ if (true) {
+ echo {$this->getVariableGetter('foo')};
+ ++\$context['loop']['index0'];
+ ++\$context['loop']['index'];
+ \$context['loop']['first'] = false;
+ }
+}
+\$_parent = \$context['_parent'];
+unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
+\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
+EOF
+ );
+
+ $keyTarget = new Twig_Node_Expression_AssignName('k', 1);
+ $valueTarget = new Twig_Node_Expression_AssignName('v', 1);
+ $seq = new Twig_Node_Expression_Name('values', 1);
+ $ifexpr = null;
+ $body = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1)), array(), 1);
+ $else = new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1);
+ $node = new Twig_Node_For($keyTarget, $valueTarget, $seq, $ifexpr, $body, $else, 1);
+ $node->setAttribute('with_loop', true);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context['_parent'] = (array) \$context;
+\$context['_seq'] = twig_ensure_traversable({$this->getVariableGetter('values')});
+\$context['_iterated'] = false;
+\$context['loop'] = array(
+ 'parent' => \$context['_parent'],
+ 'index0' => 0,
+ 'index' => 1,
+ 'first' => true,
+);
+if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_seq'] instanceof Countable)) {
+ \$length = count(\$context['_seq']);
+ \$context['loop']['revindex0'] = \$length - 1;
+ \$context['loop']['revindex'] = \$length;
+ \$context['loop']['length'] = \$length;
+ \$context['loop']['last'] = 1 === \$length;
+}
+foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
+ echo {$this->getVariableGetter('foo')};
+ \$context['_iterated'] = true;
+ ++\$context['loop']['index0'];
+ ++\$context['loop']['index'];
+ \$context['loop']['first'] = false;
+ if (isset(\$context['loop']['length'])) {
+ --\$context['loop']['revindex0'];
+ --\$context['loop']['revindex'];
+ \$context['loop']['last'] = 0 === \$context['loop']['revindex0'];
+ }
+}
+if (!\$context['_iterated']) {
+ echo {$this->getVariableGetter('foo')};
+}
+\$_parent = \$context['_parent'];
+unset(\$context['_seq'], \$context['_iterated'], \$context['k'], \$context['v'], \$context['_parent'], \$context['loop']);
+\$context = array_intersect_key(\$context, \$_parent) + \$_parent;
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_IfTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_If::__construct
+ */
+ public function testConstructor()
+ {
+ $t = new Twig_Node(array(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1),
+ ), array(), 1);
+ $else = null;
+ $node = new Twig_Node_If($t, $else, 1);
+
+ $this->assertEquals($t, $node->getNode('tests'));
+ $this->assertEquals(null, $node->getNode('else'));
+
+ $else = new Twig_Node_Print(new Twig_Node_Expression_Name('bar', 1), 1);
+ $node = new Twig_Node_If($t, $else, 1);
+ $this->assertEquals($else, $node->getNode('else'));
+ }
+
+ /**
+ * @covers Twig_Node_If::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $t = new Twig_Node(array(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1),
+ ), array(), 1);
+ $else = null;
+ $node = new Twig_Node_If($t, $else, 1);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+if (true) {
+ echo {$this->getVariableGetter('foo')};
+}
+EOF
+ );
+
+ $t = new Twig_Node(array(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1),
+ new Twig_Node_Expression_Constant(false, 1),
+ new Twig_Node_Print(new Twig_Node_Expression_Name('bar', 1), 1),
+ ), array(), 1);
+ $else = null;
+ $node = new Twig_Node_If($t, $else, 1);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+if (true) {
+ echo {$this->getVariableGetter('foo')};
+} elseif (false) {
+ echo {$this->getVariableGetter('bar')};
+}
+EOF
+ );
+
+ $t = new Twig_Node(array(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Print(new Twig_Node_Expression_Name('foo', 1), 1),
+ ), array(), 1);
+ $else = new Twig_Node_Print(new Twig_Node_Expression_Name('bar', 1), 1);
+ $node = new Twig_Node_If($t, $else, 1);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+if (true) {
+ echo {$this->getVariableGetter('foo')};
+} else {
+ echo {$this->getVariableGetter('bar')};
+}
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_ImportTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Import::__construct
+ */
+ public function testConstructor()
+ {
+ $macro = new Twig_Node_Expression_Constant('foo.twig', 1);
+ $var = new Twig_Node_Expression_AssignName('macro', 1);
+ $node = new Twig_Node_Import($macro, $var, 1);
+
+ $this->assertEquals($macro, $node->getNode('expr'));
+ $this->assertEquals($var, $node->getNode('var'));
+ }
+
+ /**
+ * @covers Twig_Node_Import::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $macro = new Twig_Node_Expression_Constant('foo.twig', 1);
+ $var = new Twig_Node_Expression_AssignName('macro', 1);
+ $node = new Twig_Node_Import($macro, $var, 1);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context["macro"] = \$this->env->loadTemplate("foo.twig");
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_IncludeTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Include::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant('foo.twig', 1);
+ $node = new Twig_Node_Include($expr, null, false, false, 1);
+
+ $this->assertEquals(null, $node->getNode('variables'));
+ $this->assertEquals($expr, $node->getNode('expr'));
+ $this->assertFalse($node->getAttribute('only'));
+
+ $vars = new Twig_Node_Expression_Array(array(new Twig_Node_Expression_Constant('foo', 1), new Twig_Node_Expression_Constant(true, 1)), 1);
+ $node = new Twig_Node_Include($expr, $vars, true, false, 1);
+ $this->assertEquals($vars, $node->getNode('variables'));
+ $this->assertTrue($node->getAttribute('only'));
+ }
+
+ /**
+ * @covers Twig_Node_Include::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $expr = new Twig_Node_Expression_Constant('foo.twig', 1);
+ $node = new Twig_Node_Include($expr, null, false, false, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$this->env->loadTemplate("foo.twig")->display(\$context);
+EOF
+ );
+
+ $expr = new Twig_Node_Expression_Conditional(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Expression_Constant('foo', 1),
+ new Twig_Node_Expression_Constant('foo', 1),
+ 0
+ );
+ $node = new Twig_Node_Include($expr, null, false, false, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$template = \$this->env->resolveTemplate(((true) ? ("foo") : ("foo")));
+\$template->display(\$context);
+EOF
+ );
+
+ $expr = new Twig_Node_Expression_Constant('foo.twig', 1);
+ $vars = new Twig_Node_Expression_Array(array(new Twig_Node_Expression_Constant('foo', 1), new Twig_Node_Expression_Constant(true, 1)), 1);
+ $node = new Twig_Node_Include($expr, $vars, false, false, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$this->env->loadTemplate("foo.twig")->display(array_merge(\$context, array("foo" => true)));
+EOF
+ );
+
+ $node = new Twig_Node_Include($expr, $vars, true, false, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$this->env->loadTemplate("foo.twig")->display(array("foo" => true));
+EOF
+ );
+
+ $node = new Twig_Node_Include($expr, $vars, true, true, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+try {
+ \$this->env->loadTemplate("foo.twig")->display(array("foo" => true));
+} catch (Twig_Error_Loader \$e) {
+ // ignore missing template
+}
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_MacroTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Macro::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $arguments = new Twig_Node(array(new Twig_Node_Expression_Name('foo', 1)), array(), 1);
+ $node = new Twig_Node_Macro('foo', $body, $arguments, 1);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ $this->assertEquals($arguments, $node->getNode('arguments'));
+ $this->assertEquals('foo', $node->getAttribute('name'));
+ }
+
+ /**
+ * @covers Twig_Node_Macro::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $arguments = new Twig_Node(array(
+ 'foo' => new Twig_Node_Expression_Constant(null, 1),
+ 'bar' => new Twig_Node_Expression_Constant('Foo', 1),
+ ), array(), 1);
+ $node = new Twig_Node_Macro('foo', $body, $arguments, 1);
+
+ return array(
+ array($node, <<<EOF
+// line 1
+public function getfoo(\$_foo = null, \$_bar = "Foo")
+{
+ \$context = \$this->env->mergeGlobals(array(
+ "foo" => \$_foo,
+ "bar" => \$_bar,
+ ));
+
+ \$blocks = array();
+
+ ob_start();
+ try {
+ echo "foo";
+ } catch (Exception \$e) {
+ ob_end_clean();
+
+ throw \$e;
+ }
+
+ return ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());
+}
+EOF
+ ),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_ModuleTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Module::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $parent = new Twig_Node_Expression_Constant('layout.twig', 1);
+ $blocks = new Twig_Node();
+ $macros = new Twig_Node();
+ $traits = new Twig_Node();
+ $filename = 'foo.twig';
+ $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ $this->assertEquals($blocks, $node->getNode('blocks'));
+ $this->assertEquals($macros, $node->getNode('macros'));
+ $this->assertEquals($parent, $node->getNode('parent'));
+ $this->assertEquals($filename, $node->getAttribute('filename'));
+ }
+
+ /**
+ * @covers Twig_Node_Module::compile
+ * @covers Twig_Node_Module::compileTemplate
+ * @covers Twig_Node_Module::compileMacros
+ * @covers Twig_Node_Module::compileClassHeader
+ * @covers Twig_Node_Module::compileDisplayHeader
+ * @covers Twig_Node_Module::compileDisplayBody
+ * @covers Twig_Node_Module::compileDisplayFooter
+ * @covers Twig_Node_Module::compileClassFooter
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String());
+
+ $tests = array();
+
+ $body = new Twig_Node_Text('foo', 1);
+ $extends = null;
+ $blocks = new Twig_Node();
+ $macros = new Twig_Node();
+ $traits = new Twig_Node();
+ $filename = 'foo.twig';
+
+ $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $tests[] = array($node, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+ public function __construct(Twig_Environment \$env)
+ {
+ parent::__construct(\$env);
+
+ \$this->parent = false;
+
+ \$this->blocks = array(
+ );
+ }
+
+ protected function doDisplay(array \$context, array \$blocks = array())
+ {
+ // line 1
+ echo "foo";
+ }
+
+ public function getTemplateName()
+ {
+ return "foo.twig";
+ }
+
+ public function getDebugInfo()
+ {
+ return array ( 19 => 1,);
+ }
+}
+EOF
+ , $twig);
+
+ $import = new Twig_Node_Import(new Twig_Node_Expression_Constant('foo.twig', 1), new Twig_Node_Expression_AssignName('macro', 1), 1);
+
+ $body = new Twig_Node(array($import));
+ $extends = new Twig_Node_Expression_Constant('layout.twig', 1);
+
+ $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $tests[] = array($node, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+ public function __construct(Twig_Environment \$env)
+ {
+ parent::__construct(\$env);
+
+ \$this->parent = \$this->env->loadTemplate("layout.twig");
+
+ \$this->blocks = array(
+ );
+ }
+
+ protected function doGetParent(array \$context)
+ {
+ return "layout.twig";
+ }
+
+ protected function doDisplay(array \$context, array \$blocks = array())
+ {
+ // line 1
+ \$context["macro"] = \$this->env->loadTemplate("foo.twig");
+ \$this->parent->display(\$context, array_merge(\$this->blocks, \$blocks));
+ }
+
+ public function getTemplateName()
+ {
+ return "foo.twig";
+ }
+
+ public function isTraitable()
+ {
+ return false;
+ }
+
+ public function getDebugInfo()
+ {
+ return array ( 24 => 1,);
+ }
+}
+EOF
+ , $twig);
+
+ $body = new Twig_Node();
+ $extends = new Twig_Node_Expression_Conditional(
+ new Twig_Node_Expression_Constant(true, 1),
+ new Twig_Node_Expression_Constant('foo', 1),
+ new Twig_Node_Expression_Constant('foo', 1),
+ 0
+ );
+
+ $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $tests[] = array($node, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+ protected function doGetParent(array \$context)
+ {
+ return \$this->env->resolveTemplate(((true) ? ("foo") : ("foo")));
+ }
+
+ protected function doDisplay(array \$context, array \$blocks = array())
+ {
+ \$this->getParent(\$context)->display(\$context, array_merge(\$this->blocks, \$blocks));
+ }
+
+ public function getTemplateName()
+ {
+ return "foo.twig";
+ }
+
+ public function isTraitable()
+ {
+ return false;
+ }
+
+ public function getDebugInfo()
+ {
+ return array ();
+ }
+}
+EOF
+ , $twig);
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_PrintTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Print::__construct
+ */
+ public function testConstructor()
+ {
+ $expr = new Twig_Node_Expression_Constant('foo', 1);
+ $node = new Twig_Node_Print($expr, 1);
+
+ $this->assertEquals($expr, $node->getNode('expr'));
+ }
+
+ /**
+ * @covers Twig_Node_Print::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+ $tests[] = array(new Twig_Node_Print(new Twig_Node_Expression_Constant('foo', 1), 1), "// line 1\necho \"foo\";");
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_SandboxTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Sandbox::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $node = new Twig_Node_Sandbox($body, 1);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ }
+
+ /**
+ * @covers Twig_Node_Sandbox::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $body = new Twig_Node_Text('foo', 1);
+ $node = new Twig_Node_Sandbox($body, 1);
+
+ $tests[] = array($node, <<<EOF
+// line 1
+\$sandbox = \$this->env->getExtension('sandbox');
+if (!\$alreadySandboxed = \$sandbox->isSandboxed()) {
+ \$sandbox->enableSandbox();
+}
+echo "foo";
+if (!\$alreadySandboxed) {
+ \$sandbox->disableSandbox();
+}
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_SandboxedModuleTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_SandboxedModule::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node_Text('foo', 1);
+ $parent = new Twig_Node_Expression_Constant('layout.twig', 1);
+ $blocks = new Twig_Node();
+ $macros = new Twig_Node();
+ $traits = new Twig_Node();
+ $filename = 'foo.twig';
+ $node = new Twig_Node_Module($body, $parent, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
+
+ $this->assertEquals($body, $node->getNode('body'));
+ $this->assertEquals($blocks, $node->getNode('blocks'));
+ $this->assertEquals($macros, $node->getNode('macros'));
+ $this->assertEquals($parent, $node->getNode('parent'));
+ $this->assertEquals($filename, $node->getAttribute('filename'));
+ }
+
+ /**
+ * @covers Twig_Node_SandboxedModule::compile
+ * @covers Twig_Node_SandboxedModule::compileDisplayBody
+ * @covers Twig_Node_SandboxedModule::compileDisplayFooter
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String());
+
+ $tests = array();
+
+ $body = new Twig_Node_Text('foo', 1);
+ $extends = null;
+ $blocks = new Twig_Node();
+ $macros = new Twig_Node();
+ $traits = new Twig_Node();
+ $filename = 'foo.twig';
+
+ $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
+
+ $tests[] = array($node, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+ public function __construct(Twig_Environment \$env)
+ {
+ parent::__construct(\$env);
+
+ \$this->parent = false;
+
+ \$this->blocks = array(
+ );
+ }
+
+ protected function doDisplay(array \$context, array \$blocks = array())
+ {
+ \$this->checkSecurity();
+ // line 1
+ echo "foo";
+ }
+
+ protected function checkSecurity()
+ {
+ \$this->env->getExtension('sandbox')->checkSecurity(
+ array('upper'),
+ array('for'),
+ array('cycle')
+ );
+ }
+
+ public function getTemplateName()
+ {
+ return "foo.twig";
+ }
+
+ public function getDebugInfo()
+ {
+ return array ( 20 => 1,);
+ }
+}
+EOF
+ , $twig);
+
+ $body = new Twig_Node();
+ $extends = new Twig_Node_Expression_Constant('layout.twig', 1);
+ $blocks = new Twig_Node();
+ $macros = new Twig_Node();
+ $traits = new Twig_Node();
+ $filename = 'foo.twig';
+
+ $node = new Twig_Node_Module($body, $extends, $blocks, $macros, $traits, new Twig_Node(array()), $filename);
+ $node = new Twig_Node_SandboxedModule($node, array('for'), array('upper'), array('cycle'));
+
+ $tests[] = array($node, <<<EOF
+<?php
+
+/* foo.twig */
+class __TwigTemplate_be925a7b06dda0dfdbd18a1509f7eb34 extends Twig_Template
+{
+ public function __construct(Twig_Environment \$env)
+ {
+ parent::__construct(\$env);
+
+ \$this->parent = \$this->env->loadTemplate("layout.twig");
+
+ \$this->blocks = array(
+ );
+ }
+
+ protected function doGetParent(array \$context)
+ {
+ return "layout.twig";
+ }
+
+ protected function doDisplay(array \$context, array \$blocks = array())
+ {
+ \$this->checkSecurity();
+ \$this->parent->display(\$context, array_merge(\$this->blocks, \$blocks));
+ }
+
+ protected function checkSecurity()
+ {
+ \$this->env->getExtension('sandbox')->checkSecurity(
+ array('upper'),
+ array('for'),
+ array('cycle')
+ );
+ }
+
+ public function getTemplateName()
+ {
+ return "foo.twig";
+ }
+
+ public function isTraitable()
+ {
+ return false;
+ }
+
+ public function getDebugInfo()
+ {
+ return array ();
+ }
+}
+EOF
+ , $twig);
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_SandboxedPrintTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_SandboxedPrint::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_SandboxedPrint($expr = new Twig_Node_Expression_Constant('foo', 1), 1);
+
+ $this->assertEquals($expr, $node->getNode('expr'));
+ }
+
+ /**
+ * @covers Twig_Node_SandboxedPrint::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $tests[] = array(new Twig_Node_SandboxedPrint(new Twig_Node_Expression_Constant('foo', 1), 1), <<<EOF
+// line 1
+echo \$this->env->getExtension('sandbox')->ensureToStringAllowed("foo");
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_SetTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Set::__construct
+ */
+ public function testConstructor()
+ {
+ $names = new Twig_Node(array(new Twig_Node_Expression_AssignName('foo', 1)), array(), 1);
+ $values = new Twig_Node(array(new Twig_Node_Expression_Constant('foo', 1)), array(), 1);
+ $node = new Twig_Node_Set(false, $names, $values, 1);
+
+ $this->assertEquals($names, $node->getNode('names'));
+ $this->assertEquals($values, $node->getNode('values'));
+ $this->assertEquals(false, $node->getAttribute('capture'));
+ }
+
+ /**
+ * @covers Twig_Node_Set::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+
+ $names = new Twig_Node(array(new Twig_Node_Expression_AssignName('foo', 1)), array(), 1);
+ $values = new Twig_Node(array(new Twig_Node_Expression_Constant('foo', 1)), array(), 1);
+ $node = new Twig_Node_Set(false, $names, $values, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context["foo"] = "foo";
+EOF
+ );
+
+ $names = new Twig_Node(array(new Twig_Node_Expression_AssignName('foo', 1)), array(), 1);
+ $values = new Twig_Node(array(new Twig_Node_Print(new Twig_Node_Expression_Constant('foo', 1), 1)), array(), 1);
+ $node = new Twig_Node_Set(true, $names, $values, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+ob_start();
+echo "foo";
+\$context["foo"] = ('' === \$tmp = ob_get_clean()) ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());
+EOF
+ );
+
+ $names = new Twig_Node(array(new Twig_Node_Expression_AssignName('foo', 1)), array(), 1);
+ $values = new Twig_Node_Text('foo', 1);
+ $node = new Twig_Node_Set(true, $names, $values, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+\$context["foo"] = ('' === \$tmp = "foo") ? '' : new Twig_Markup(\$tmp, \$this->env->getCharset());
+EOF
+ );
+
+ $names = new Twig_Node(array(new Twig_Node_Expression_AssignName('foo', 1), new Twig_Node_Expression_AssignName('bar', 1)), array(), 1);
+ $values = new Twig_Node(array(new Twig_Node_Expression_Constant('foo', 1), new Twig_Node_Expression_Name('bar', 1)), array(), 1);
+ $node = new Twig_Node_Set(false, $names, $values, 1);
+ $tests[] = array($node, <<<EOF
+// line 1
+list(\$context["foo"], \$context["bar"]) = array("foo", {$this->getVariableGetter('bar')});
+EOF
+ );
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_SpacelessTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Spaceless::__construct
+ */
+ public function testConstructor()
+ {
+ $body = new Twig_Node(array(new Twig_Node_Text('<div> <div> foo </div> </div>', 1)));
+ $node = new Twig_Node_Spaceless($body, 1);
+
+ $this->assertEquals($body, $node->getNode('body'));
+ }
+
+ /**
+ * @covers Twig_Node_Spaceless::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $body = new Twig_Node(array(new Twig_Node_Text('<div> <div> foo </div> </div>', 1)));
+ $node = new Twig_Node_Spaceless($body, 1);
+
+ return array(
+ array($node, <<<EOF
+// line 1
+ob_start();
+echo "<div> <div> foo </div> </div>";
+echo trim(preg_replace('/>\s+</', '><', ob_get_clean()));
+EOF
+ ),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Node_TextTest extends Twig_Test_NodeTestCase
+{
+ /**
+ * @covers Twig_Node_Text::__construct
+ */
+ public function testConstructor()
+ {
+ $node = new Twig_Node_Text('foo', 1);
+
+ $this->assertEquals('foo', $node->getAttribute('data'));
+ }
+
+ /**
+ * @covers Twig_Node_Text::compile
+ * @dataProvider getTests
+ */
+ public function testCompile($node, $source, $environment = null)
+ {
+ parent::testCompile($node, $source, $environment);
+ }
+
+ public function getTests()
+ {
+ $tests = array();
+ $tests[] = array(new Twig_Node_Text('foo', 1), "// line 1\necho \"foo\";");
+
+ return $tests;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Tests_NodeVisitor_OptimizerTest extends PHPUnit_Framework_TestCase
+{
+ public function testRenderBlockOptimizer()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+
+ $stream = $env->parse($env->tokenize('{{ block("foo") }}', 'index'));
+
+ $node = $stream->getNode('body')->getNode(0);
+
+ $this->assertEquals('Twig_Node_Expression_BlockReference', get_class($node));
+ $this->assertTrue($node->getAttribute('output'));
+ }
+
+ public function testRenderParentBlockOptimizer()
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+
+ $stream = $env->parse($env->tokenize('{% extends "foo" %}{% block content %}{{ parent() }}{% endblock %}', 'index'));
+
+ $node = $stream->getNode('blocks')->getNode('content')->getNode(0)->getNode('body');
+
+ $this->assertEquals('Twig_Node_Expression_Parent', get_class($node));
+ $this->assertTrue($node->getAttribute('output'));
+ }
+
+ public function testRenderVariableBlockOptimizer()
+ {
+ if (version_compare(phpversion(), '5.4.0RC1', '>=')) {
+ return;
+ }
+
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false, 'autoescape' => false));
+ $stream = $env->parse($env->tokenize('{{ block(name|lower) }}', 'index'));
+
+ $node = $stream->getNode('body')->getNode(0)->getNode(1);
+
+ $this->assertEquals('Twig_Node_Expression_BlockReference', get_class($node));
+ $this->assertTrue($node->getAttribute('output'));
+ }
+
+ /**
+ * @dataProvider getTestsForForOptimizer
+ */
+ public function testForOptimizer($template, $expected)
+ {
+ $env = new Twig_Environment(new Twig_Loader_String(), array('cache' => false));
+
+ $stream = $env->parse($env->tokenize($template, 'index'));
+
+ foreach ($expected as $target => $withLoop) {
+ $this->assertTrue($this->checkForConfiguration($stream, $target, $withLoop), sprintf('variable %s is %soptimized', $target, $withLoop ? 'not ' : ''));
+ }
+ }
+
+ public function getTestsForForOptimizer()
+ {
+ return array(
+ array('{% for i in foo %}{% endfor %}', array('i' => false)),
+
+ array('{% for i in foo %}{{ loop.index }}{% endfor %}', array('i' => true)),
+
+ array('{% for i in foo %}{% for j in foo %}{% endfor %}{% endfor %}', array('i' => false, 'j' => false)),
+
+ array('{% for i in foo %}{% include "foo" %}{% endfor %}', array('i' => true)),
+
+ array('{% for i in foo %}{% include "foo" only %}{% endfor %}', array('i' => false)),
+
+ array('{% for i in foo %}{% include "foo" with { "foo": "bar" } only %}{% endfor %}', array('i' => false)),
+
+ array('{% for i in foo %}{% include "foo" with { "foo": loop.index } only %}{% endfor %}', array('i' => true)),
+
+ array('{% for i in foo %}{% for j in foo %}{{ loop.index }}{% endfor %}{% endfor %}', array('i' => false, 'j' => true)),
+
+ array('{% for i in foo %}{% for j in foo %}{{ loop.parent.loop.index }}{% endfor %}{% endfor %}', array('i' => true, 'j' => true)),
+
+ array('{% for i in foo %}{% set l = loop %}{% for j in foo %}{{ l.index }}{% endfor %}{% endfor %}', array('i' => true, 'j' => false)),
+
+ array('{% for i in foo %}{% for j in foo %}{{ foo.parent.loop.index }}{% endfor %}{% endfor %}', array('i' => false, 'j' => false)),
+
+ array('{% for i in foo %}{% for j in foo %}{{ loop["parent"].loop.index }}{% endfor %}{% endfor %}', array('i' => true, 'j' => true)),
+ );
+ }
+
+ public function checkForConfiguration(Twig_NodeInterface $node = null, $target, $withLoop)
+ {
+ if (null === $node) {
+ return;
+ }
+
+ foreach ($node as $n) {
+ if ($n instanceof Twig_Node_For) {
+ if ($target === $n->getNode('value_target')->getAttribute('name')) {
+ return $withLoop == $n->getAttribute('with_loop');
+ }
+ }
+
+ $ret = $this->checkForConfiguration($n, $target, $withLoop);
+ if (null !== $ret) {
+ return $ret;
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Tests_ParserTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @expectedException Twig_Error_Syntax
+ */
+ public function testSetMacroThrowsExceptionOnReservedMethods()
+ {
+ $parser = $this->getParser();
+ $parser->setMacro('display', $this->getMock('Twig_Node_Macro', array(), array(), '', null));
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage Unknown tag name "foo". Did you mean "for" at line 1
+ */
+ public function testUnknownTag()
+ {
+ $stream = new Twig_TokenStream(array(
+ new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', 1),
+ new Twig_Token(Twig_Token::NAME_TYPE, 'foo', 1),
+ new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', 1),
+ new Twig_Token(Twig_Token::EOF_TYPE, '', 1),
+ ));
+ $parser = new Twig_Parser(new Twig_Environment());
+ $parser->parse($stream);
+ }
+
+ /**
+ * @dataProvider getFilterBodyNodesData
+ */
+ public function testFilterBodyNodes($input, $expected)
+ {
+ $parser = $this->getParser();
+
+ $this->assertEquals($expected, $parser->filterBodyNodes($input));
+ }
+
+ public function getFilterBodyNodesData()
+ {
+ return array(
+ array(
+ new Twig_Node(array(new Twig_Node_Text(' ', 1))),
+ new Twig_Node(array()),
+ ),
+ array(
+ $input = new Twig_Node(array(new Twig_Node_Set(false, new Twig_Node(), new Twig_Node(), 1))),
+ $input,
+ ),
+ array(
+ $input = new Twig_Node(array(new Twig_Node_Set(true, new Twig_Node(), new Twig_Node(array(new Twig_Node(array(new Twig_Node_Text('foo', 1))))), 1))),
+ $input,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider getFilterBodyNodesDataThrowsException
+ * @expectedException Twig_Error_Syntax
+ */
+ public function testFilterBodyNodesThrowsException($input)
+ {
+ $parser = $this->getParser();
+
+ $parser->filterBodyNodes($input);
+ }
+
+ public function getFilterBodyNodesDataThrowsException()
+ {
+ return array(
+ array(new Twig_Node_Text('foo', 1)),
+ array(new Twig_Node(array(new Twig_Node(array(new Twig_Node_Text('foo', 1)))))),
+ );
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedExceptionMessage A template that extends another one cannot have a body but a byte order mark (BOM) has been detected; it must be removed at line 1.
+ */
+ public function testFilterBodyNodesWithBOM()
+ {
+ $parser = $this->getParser();
+ $parser->filterBodyNodes(new Twig_Node_Text(chr(0xEF).chr(0xBB).chr(0xBF), 1));
+ }
+
+ public function testParseIsReentrant()
+ {
+ $twig = new Twig_Environment(null, array(
+ 'autoescape' => false,
+ 'optimizations' => 0,
+ ));
+ $twig->addTokenParser(new TestTokenParser());
+
+ $parser = new Twig_Parser($twig);
+
+ $parser->parse(new Twig_TokenStream(array(
+ new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', 1),
+ new Twig_Token(Twig_Token::NAME_TYPE, 'test', 1),
+ new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', 1),
+ new Twig_Token(Twig_Token::VAR_START_TYPE, '', 1),
+ new Twig_Token(Twig_Token::NAME_TYPE, 'foo', 1),
+ new Twig_Token(Twig_Token::VAR_END_TYPE, '', 1),
+ new Twig_Token(Twig_Token::EOF_TYPE, '', 1),
+ )));
+
+ $this->assertEquals(null, $parser->getParent());
+ }
+
+ // The getVarName() must not depend on the template loaders,
+ // If this test does not throw any exception, that's good.
+ // see https://github.com/symfony/symfony/issues/4218
+ public function testGetVarName()
+ {
+ $twig = new Twig_Environment(null, array(
+ 'autoescape' => false,
+ 'optimizations' => 0,
+ ));
+
+ $twig->parse($twig->tokenize(<<<EOF
+{% from _self import foo %}
+
+{% macro foo() %}
+ {{ foo }}
+{% endmacro %}
+EOF
+ ));
+ }
+
+ protected function getParser()
+ {
+ $parser = new TestParser(new Twig_Environment());
+ $parser->setParent(new Twig_Node());
+ $parser->stream = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock();
+
+ return $parser;
+ }
+}
+
+class TestParser extends Twig_Parser
+{
+ public $stream;
+
+ public function filterBodyNodes(Twig_NodeInterface $node)
+ {
+ return parent::filterBodyNodes($node);
+ }
+}
+
+class TestTokenParser extends Twig_TokenParser
+{
+ public function parse(Twig_Token $token)
+ {
+ // simulate the parsing of another template right in the middle of the parsing of the current template
+ $this->parser->parse(new Twig_TokenStream(array(
+ new Twig_Token(Twig_Token::BLOCK_START_TYPE, '', 1),
+ new Twig_Token(Twig_Token::NAME_TYPE, 'extends', 1),
+ new Twig_Token(Twig_Token::STRING_TYPE, 'base', 1),
+ new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', 1),
+ new Twig_Token(Twig_Token::EOF_TYPE, '', 1),
+ )));
+
+ $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Twig_Node(array());
+ }
+
+ public function getTag()
+ {
+ return 'test';
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Tests_TemplateTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getAttributeExceptions
+ */
+ public function testGetAttributeExceptions($template, $message, $useExt)
+ {
+ $name = 'index_'.($useExt ? 1 : 0);
+ $templates = array(
+ $name => $template.$useExt, // appending $useExt makes the template content unique
+ );
+
+ $env = new Twig_Environment(new Twig_Loader_Array($templates), array('strict_variables' => true));
+ if (!$useExt) {
+ $env->addNodeVisitor(new CExtDisablingNodeVisitor());
+ }
+ $template = $env->loadTemplate($name);
+
+ $context = array(
+ 'string' => 'foo',
+ 'array' => array('foo' => 'foo'),
+ 'array_access' => new Twig_TemplateArrayAccessObject(),
+ 'magic_exception' => new Twig_TemplateMagicPropertyObjectWithException(),
+ );
+
+ try {
+ $template->render($context);
+ $this->fail('Accessing an invalid attribute should throw an exception.');
+ } catch (Twig_Error_Runtime $e) {
+ $this->assertSame(sprintf($message, $name), $e->getMessage());
+ }
+ }
+
+ public function getAttributeExceptions()
+ {
+ $tests = array(
+ array('{{ string["a"] }}', 'Impossible to access a key ("a") on a string variable ("foo") in "%s" at line 1', false),
+ array('{{ array["a"] }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1', false),
+ array('{{ array_access["a"] }}', 'Key "a" in object (with ArrayAccess) of type "Twig_TemplateArrayAccessObject" does not exist in "%s" at line 1', false),
+ array('{{ string.a }}', 'Impossible to access an attribute ("a") on a string variable ("foo") in "%s" at line 1', false),
+ array('{{ string.a() }}', 'Impossible to invoke a method ("a") on a string variable ("foo") in "%s" at line 1', false),
+ array('{{ array.a }}', 'Key "a" for array with keys "foo" does not exist in "%s" at line 1', false),
+ array('{{ attribute(array, -10) }}', 'Key "-10" for array with keys "foo" does not exist in "%s" at line 1', false),
+ array('{{ array_access.a }}', 'Method "a" for object "Twig_TemplateArrayAccessObject" does not exist in "%s" at line 1', false),
+ array('{% macro foo(obj) %}{{ obj.missing_method() }}{% endmacro %}{{ _self.foo(array_access) }}', 'Method "missing_method" for object "Twig_TemplateArrayAccessObject" does not exist in "%s" at line 1', false),
+ array('{{ magic_exception.test }}', 'An exception has been thrown during the rendering of a template ("Hey! Don\'t try to isset me!") in "%s" at line 1.', false),
+ );
+
+ if (function_exists('twig_template_get_attributes')) {
+ foreach (array_slice($tests, 0) as $test) {
+ $test[2] = true;
+ $tests[] = $test;
+ }
+ }
+
+ return $tests;
+ }
+
+ /**
+ * @dataProvider getGetAttributeWithSandbox
+ */
+ public function testGetAttributeWithSandbox($object, $item, $allowed, $useExt)
+ {
+ $twig = new Twig_Environment();
+ $policy = new Twig_Sandbox_SecurityPolicy(array(), array(), array(/*method*/), array(/*prop*/), array());
+ $twig->addExtension(new Twig_Extension_Sandbox($policy, !$allowed));
+ $template = new Twig_TemplateTest($twig, $useExt);
+
+ try {
+ $template->getAttribute($object, $item, array(), 'any');
+
+ if (!$allowed) {
+ $this->fail();
+ }
+ } catch (Twig_Sandbox_SecurityError $e) {
+ if ($allowed) {
+ $this->fail();
+ }
+
+ $this->assertContains('is not allowed', $e->getMessage());
+ }
+ }
+
+ public function getGetAttributeWithSandbox()
+ {
+ $tests = array(
+ array(new Twig_TemplatePropertyObject(), 'defined', false, false),
+ array(new Twig_TemplatePropertyObject(), 'defined', true, false),
+ array(new Twig_TemplateMethodObject(), 'defined', false, false),
+ array(new Twig_TemplateMethodObject(), 'defined', true, false),
+ );
+
+ if (function_exists('twig_template_get_attributes')) {
+ foreach (array_slice($tests, 0) as $test) {
+ $test[3] = true;
+ $tests[] = $test;
+ }
+ }
+
+ return $tests;
+ }
+
+ /**
+ * @dataProvider getGetAttributeWithTemplateAsObject
+ */
+ public function testGetAttributeWithTemplateAsObject($useExt)
+ {
+ $template = new Twig_TemplateTest(new Twig_Environment(), $useExt);
+ $template1 = new Twig_TemplateTest(new Twig_Environment(), false);
+
+ $this->assertInstanceof('Twig_Markup', $template->getAttribute($template1, 'string'));
+ $this->assertEquals('some_string', $template->getAttribute($template1, 'string'));
+
+ $this->assertInstanceof('Twig_Markup', $template->getAttribute($template1, 'true'));
+ $this->assertEquals('1', $template->getAttribute($template1, 'true'));
+
+ $this->assertInstanceof('Twig_Markup', $template->getAttribute($template1, 'zero'));
+ $this->assertEquals('0', $template->getAttribute($template1, 'zero'));
+
+ $this->assertNotInstanceof('Twig_Markup', $template->getAttribute($template1, 'empty'));
+ $this->assertSame('', $template->getAttribute($template1, 'empty'));
+ }
+
+ public function getGetAttributeWithTemplateAsObject()
+ {
+ $bools = array(
+ array(false),
+ );
+
+ if (function_exists('twig_template_get_attributes')) {
+ $bools[] = array(true);
+ }
+
+ return $bools;
+ }
+
+ /**
+ * @dataProvider getTestsDependingOnExtensionAvailability
+ */
+ public function testGetAttributeOnArrayWithConfusableKey($useExt = false)
+ {
+ $template = new Twig_TemplateTest(
+ new Twig_Environment(),
+ $useExt
+ );
+
+ $array = array('Zero', 'One', -1 => 'MinusOne', '' => 'EmptyString', '1.5' => 'FloatButString', '01' => 'IntegerButStringWithLeadingZeros');
+
+ $this->assertSame('Zero', $array[false]);
+ $this->assertSame('One', $array[true]);
+ $this->assertSame('One', $array[1.5]);
+ $this->assertSame('One', $array['1']);
+ $this->assertSame('MinusOne', $array[-1.5]);
+ $this->assertSame('FloatButString', $array['1.5']);
+ $this->assertSame('IntegerButStringWithLeadingZeros', $array['01']);
+ $this->assertSame('EmptyString', $array[null]);
+
+ $this->assertSame('Zero', $template->getAttribute($array, false), 'false is treated as 0 when accessing an array (equals PHP behavior)');
+ $this->assertSame('One', $template->getAttribute($array, true), 'true is treated as 1 when accessing an array (equals PHP behavior)');
+ $this->assertSame('One', $template->getAttribute($array, 1.5), 'float is casted to int when accessing an array (equals PHP behavior)');
+ $this->assertSame('One', $template->getAttribute($array, '1'), '"1" is treated as integer 1 when accessing an array (equals PHP behavior)');
+ $this->assertSame('MinusOne', $template->getAttribute($array, -1.5), 'negative float is casted to int when accessing an array (equals PHP behavior)');
+ $this->assertSame('FloatButString', $template->getAttribute($array, '1.5'), '"1.5" is treated as-is when accessing an array (equals PHP behavior)');
+ $this->assertSame('IntegerButStringWithLeadingZeros', $template->getAttribute($array, '01'), '"01" is treated as-is when accessing an array (equals PHP behavior)');
+ $this->assertSame('EmptyString', $template->getAttribute($array, null), 'null is treated as "" when accessing an array (equals PHP behavior)');
+ }
+
+ public function getTestsDependingOnExtensionAvailability()
+ {
+ if (function_exists('twig_template_get_attributes')) {
+ return array(array(false), array(true));
+ }
+
+ return array(array(false));
+ }
+
+ /**
+ * @dataProvider getGetAttributeTests
+ */
+ public function testGetAttribute($defined, $value, $object, $item, $arguments, $type, $useExt = false)
+ {
+ $template = new Twig_TemplateTest(new Twig_Environment(), $useExt);
+
+ $this->assertEquals($value, $template->getAttribute($object, $item, $arguments, $type));
+ }
+
+ /**
+ * @dataProvider getGetAttributeTests
+ */
+ public function testGetAttributeStrict($defined, $value, $object, $item, $arguments, $type, $useExt = false, $exceptionMessage = null)
+ {
+ $template = new Twig_TemplateTest(new Twig_Environment(null, array('strict_variables' => true)), $useExt);
+
+ if ($defined) {
+ $this->assertEquals($value, $template->getAttribute($object, $item, $arguments, $type));
+ } else {
+ try {
+ $this->assertEquals($value, $template->getAttribute($object, $item, $arguments, $type));
+
+ throw new Exception('Expected Twig_Error_Runtime exception.');
+ } catch (Twig_Error_Runtime $e) {
+ if (null !== $exceptionMessage) {
+ $this->assertSame($exceptionMessage, $e->getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * @dataProvider getGetAttributeTests
+ */
+ public function testGetAttributeDefined($defined, $value, $object, $item, $arguments, $type, $useExt = false)
+ {
+ $template = new Twig_TemplateTest(new Twig_Environment(), $useExt);
+
+ $this->assertEquals($defined, $template->getAttribute($object, $item, $arguments, $type, true));
+ }
+
+ /**
+ * @dataProvider getGetAttributeTests
+ */
+ public function testGetAttributeDefinedStrict($defined, $value, $object, $item, $arguments, $type, $useExt = false)
+ {
+ $template = new Twig_TemplateTest(new Twig_Environment(null, array('strict_variables' => true)), $useExt);
+
+ $this->assertEquals($defined, $template->getAttribute($object, $item, $arguments, $type, true));
+ }
+
+ public function getGetAttributeTests()
+ {
+ $array = array(
+ 'defined' => 'defined',
+ 'zero' => 0,
+ 'null' => null,
+ '1' => 1,
+ 'bar' => true,
+ '09' => '09',
+ '+4' => '+4',
+ );
+
+ $objectArray = new Twig_TemplateArrayAccessObject();
+ $stdObject = (object) $array;
+ $magicPropertyObject = new Twig_TemplateMagicPropertyObject();
+ $propertyObject = new Twig_TemplatePropertyObject();
+ $propertyObject1 = new Twig_TemplatePropertyObjectAndIterator();
+ $propertyObject2 = new Twig_TemplatePropertyObjectAndArrayAccess();
+ $methodObject = new Twig_TemplateMethodObject();
+ $magicMethodObject = new Twig_TemplateMagicMethodObject();
+
+ $anyType = Twig_TemplateInterface::ANY_CALL;
+ $methodType = Twig_TemplateInterface::METHOD_CALL;
+ $arrayType = Twig_TemplateInterface::ARRAY_CALL;
+
+ $basicTests = array(
+ // array(defined, value, property to fetch)
+ array(true, 'defined', 'defined'),
+ array(false, null, 'undefined'),
+ array(false, null, 'protected'),
+ array(true, 0, 'zero'),
+ array(true, 1, 1),
+ array(true, 1, 1.0),
+ array(true, null, 'null'),
+ array(true, true, 'bar'),
+ array(true, '09', '09'),
+ array(true, '+4', '+4'),
+ );
+ $testObjects = array(
+ // array(object, type of fetch)
+ array($array, $arrayType),
+ array($objectArray, $arrayType),
+ array($stdObject, $anyType),
+ array($magicPropertyObject, $anyType),
+ array($methodObject, $methodType),
+ array($methodObject, $anyType),
+ array($propertyObject, $anyType),
+ array($propertyObject1, $anyType),
+ array($propertyObject2, $anyType),
+ );
+
+ $tests = array();
+ foreach ($testObjects as $testObject) {
+ foreach ($basicTests as $test) {
+ // properties cannot be numbers
+ if (($testObject[0] instanceof stdClass || $testObject[0] instanceof Twig_TemplatePropertyObject) && is_numeric($test[2])) {
+ continue;
+ }
+
+ if ('+4' === $test[2] && $methodObject === $testObject[0]) {
+ continue;
+ }
+
+ $tests[] = array($test[0], $test[1], $testObject[0], $test[2], array(), $testObject[1]);
+ }
+ }
+
+ // additional method tests
+ $tests = array_merge($tests, array(
+ array(true, 'defined', $methodObject, 'defined', array(), $methodType),
+ array(true, 'defined', $methodObject, 'DEFINED', array(), $methodType),
+ array(true, 'defined', $methodObject, 'getDefined', array(), $methodType),
+ array(true, 'defined', $methodObject, 'GETDEFINED', array(), $methodType),
+ array(true, 'static', $methodObject, 'static', array(), $methodType),
+ array(true, 'static', $methodObject, 'getStatic', array(), $methodType),
+
+ array(true, '__call_undefined', $magicMethodObject, 'undefined', array(), $methodType),
+ array(true, '__call_UNDEFINED', $magicMethodObject, 'UNDEFINED', array(), $methodType),
+ ));
+
+ // add the same tests for the any type
+ foreach ($tests as $test) {
+ if ($anyType !== $test[5]) {
+ $test[5] = $anyType;
+ $tests[] = $test;
+ }
+ }
+
+ $methodAndPropObject = new Twig_TemplateMethodAndPropObject;
+
+ // additional method tests
+ $tests = array_merge($tests, array(
+ array(true, 'a', $methodAndPropObject, 'a', array(), $anyType),
+ array(true, 'a', $methodAndPropObject, 'a', array(), $methodType),
+ array(false, null, $methodAndPropObject, 'a', array(), $arrayType),
+
+ array(true, 'b_prop', $methodAndPropObject, 'b', array(), $anyType),
+ array(true, 'b', $methodAndPropObject, 'B', array(), $anyType),
+ array(true, 'b', $methodAndPropObject, 'b', array(), $methodType),
+ array(true, 'b', $methodAndPropObject, 'B', array(), $methodType),
+ array(false, null, $methodAndPropObject, 'b', array(), $arrayType),
+
+ array(false, null, $methodAndPropObject, 'c', array(), $anyType),
+ array(false, null, $methodAndPropObject, 'c', array(), $methodType),
+ array(false, null, $methodAndPropObject, 'c', array(), $arrayType),
+
+ ));
+
+ // tests when input is not an array or object
+ $tests = array_merge($tests, array(
+ array(false, null, 42, 'a', array(), $anyType, false, 'Impossible to access an attribute ("a") on a integer variable ("42")'),
+ array(false, null, "string", 'a', array(), $anyType, false, 'Impossible to access an attribute ("a") on a string variable ("string")'),
+ array(false, null, array(), 'a', array(), $anyType, false, 'Key "a" for array with keys "" does not exist'),
+ ));
+
+ // add twig_template_get_attributes tests
+
+ if (function_exists('twig_template_get_attributes')) {
+ foreach (array_slice($tests, 0) as $test) {
+ $test = array_pad($test, 7, null);
+ $test[6] = true;
+ $tests[] = $test;
+ }
+ }
+
+ return $tests;
+ }
+}
+
+class Twig_TemplateTest extends Twig_Template
+{
+ protected $useExtGetAttribute = false;
+
+ public function __construct(Twig_Environment $env, $useExtGetAttribute = false)
+ {
+ parent::__construct($env);
+ $this->useExtGetAttribute = $useExtGetAttribute;
+ Twig_Template::clearCache();
+ }
+
+ public function getZero()
+ {
+ return 0;
+ }
+
+ public function getEmpty()
+ {
+ return '';
+ }
+
+ public function getString()
+ {
+ return 'some_string';
+ }
+
+ public function getTrue()
+ {
+ return true;
+ }
+
+ public function getTemplateName()
+ {
+ }
+
+ public function getDebugInfo()
+ {
+ return array();
+ }
+
+ protected function doGetParent(array $context)
+ {
+ }
+
+ protected function doDisplay(array $context, array $blocks = array())
+ {
+ }
+
+ public function getAttribute($object, $item, array $arguments = array(), $type = Twig_TemplateInterface::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false)
+ {
+ if ($this->useExtGetAttribute) {
+ return twig_template_get_attributes($this, $object, $item, $arguments, $type, $isDefinedTest, $ignoreStrictCheck);
+ } else {
+ return parent::getAttribute($object, $item, $arguments, $type, $isDefinedTest, $ignoreStrictCheck);
+ }
+ }
+}
+
+class Twig_TemplateArrayAccessObject implements ArrayAccess
+{
+ protected $protected = 'protected';
+
+ public $attributes = array(
+ 'defined' => 'defined',
+ 'zero' => 0,
+ 'null' => null,
+ '1' => 1,
+ 'bar' => true,
+ '09' => '09',
+ '+4' => '+4',
+ );
+
+ public function offsetExists($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ public function offsetGet($name)
+ {
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
+ }
+
+ public function offsetSet($name, $value)
+ {
+ }
+
+ public function offsetUnset($name)
+ {
+ }
+}
+
+class Twig_TemplateMagicPropertyObject
+{
+ public $defined = 'defined';
+
+ public $attributes = array(
+ 'zero' => 0,
+ 'null' => null,
+ '1' => 1,
+ 'bar' => true,
+ '09' => '09',
+ '+4' => '+4',
+ );
+
+ protected $protected = 'protected';
+
+ public function __isset($name)
+ {
+ return array_key_exists($name, $this->attributes);
+ }
+
+ public function __get($name)
+ {
+ return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
+ }
+}
+
+class Twig_TemplateMagicPropertyObjectWithException
+{
+ public function __isset($key)
+ {
+ throw new Exception("Hey! Don't try to isset me!");
+ }
+}
+
+class Twig_TemplatePropertyObject
+{
+ public $defined = 'defined';
+ public $zero = 0;
+ public $null = null;
+ public $bar = true;
+
+ protected $protected = 'protected';
+}
+
+class Twig_TemplatePropertyObjectAndIterator extends Twig_TemplatePropertyObject implements IteratorAggregate
+{
+ public function getIterator()
+ {
+ return new ArrayIterator(array('foo', 'bar'));
+ }
+}
+
+class Twig_TemplatePropertyObjectAndArrayAccess extends Twig_TemplatePropertyObject implements ArrayAccess
+{
+ private $data = array();
+
+ public function offsetExists($offset)
+ {
+ return array_key_exists($offset, $this->data);
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->offsetExists($offset) ? $this->data[$offset] : 'n/a';
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ }
+
+ public function offsetUnset($offset)
+ {
+ }
+}
+
+class Twig_TemplateMethodObject
+{
+ public function getDefined()
+ {
+ return 'defined';
+ }
+
+ public function get1()
+ {
+ return 1;
+ }
+
+ public function get09()
+ {
+ return '09';
+ }
+
+ public function getZero()
+ {
+ return 0;
+ }
+
+ public function getNull()
+ {
+ return null;
+ }
+
+ public function isBar()
+ {
+ return true;
+ }
+
+ protected function getProtected()
+ {
+ return 'protected';
+ }
+
+ public static function getStatic()
+ {
+ return 'static';
+ }
+}
+
+class Twig_TemplateMethodAndPropObject
+{
+ private $a = 'a_prop';
+ public function getA()
+ {
+ return 'a';
+ }
+
+ public $b = 'b_prop';
+ public function getB()
+ {
+ return 'b';
+ }
+
+ private $c = 'c_prop';
+ private function getC()
+ {
+ return 'c';
+ }
+}
+
+class Twig_TemplateMagicMethodObject
+{
+ public function __call($method, $arguments)
+ {
+ return '__call_'.$method;
+ }
+}
+
+class CExtDisablingNodeVisitor implements Twig_NodeVisitorInterface
+{
+ public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
+ {
+ if ($node instanceof Twig_Node_Expression_GetAttr) {
+ $node->setAttribute('disable_c_ext', true);
+ }
+
+ return $node;
+ }
+
+ public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
+ {
+ return $node;
+ }
+
+ public function getPriority()
+ {
+ return 0;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_TokenStreamTest extends PHPUnit_Framework_TestCase
+{
+ protected static $tokens;
+
+ public function setUp()
+ {
+ self::$tokens = array(
+ new Twig_Token(Twig_Token::TEXT_TYPE, 1, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 2, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 3, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 4, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 5, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 6, 1),
+ new Twig_Token(Twig_Token::TEXT_TYPE, 7, 1),
+ new Twig_Token(Twig_Token::EOF_TYPE, 0, 1),
+ );
+ }
+
+ public function testNext()
+ {
+ $stream = new Twig_TokenStream(self::$tokens);
+ $repr = array();
+ while (!$stream->isEOF()) {
+ $token = $stream->next();
+
+ $repr[] = $token->getValue();
+ }
+ $this->assertEquals('1, 2, 3, 4, 5, 6, 7', implode(', ', $repr), '->next() advances the pointer and returns the current token');
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedMessage Unexpected end of template
+ */
+ public function testEndOfTemplateNext()
+ {
+ $stream = new Twig_TokenStream(array(
+ new Twig_Token(Twig_Token::BLOCK_START_TYPE, 1, 1),
+ ));
+ while (!$stream->isEOF()) {
+ $stream->next();
+ }
+ }
+
+ /**
+ * @expectedException Twig_Error_Syntax
+ * @expectedMessage Unexpected end of template
+ */
+ public function testEndOfTemplateLook()
+ {
+ $stream = new Twig_TokenStream(array(
+ new Twig_Token(Twig_Token::BLOCK_START_TYPE, 1, 1),
+ ));
+ while (!$stream->isEOF()) {
+ $stream->look();
+ $stream->next();
+ }
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * This class is adapted from code coming from Zend Framework.
+ *
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license http://framework.zend.com/license/new-bsd New BSD License
+ */
+
+class Twig_Test_EscapingTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * All character encodings supported by htmlspecialchars()
+ */
+ protected $htmlSpecialChars = array(
+ '\'' => ''',
+ '"' => '"',
+ '<' => '<',
+ '>' => '>',
+ '&' => '&'
+ );
+
+ protected $htmlAttrSpecialChars = array(
+ '\'' => ''',
+ /* Characters beyond ASCII value 255 to unicode escape */
+ 'Ā' => 'Ā',
+ /* Immune chars excluded */
+ ',' => ',',
+ '.' => '.',
+ '-' => '-',
+ '_' => '_',
+ /* Basic alnums excluded */
+ 'a' => 'a',
+ 'A' => 'A',
+ 'z' => 'z',
+ 'Z' => 'Z',
+ '0' => '0',
+ '9' => '9',
+ /* Basic control characters and null */
+ "\r" => '
',
+ "\n" => '
',
+ "\t" => '	',
+ "\0" => '�', // should use Unicode replacement char
+ /* Encode chars as named entities where possible */
+ '<' => '<',
+ '>' => '>',
+ '&' => '&',
+ '"' => '"',
+ /* Encode spaces for quoteless attribute protection */
+ ' ' => ' ',
+ );
+
+ protected $jsSpecialChars = array(
+ /* HTML special chars - escape without exception to hex */
+ '<' => '\\x3C',
+ '>' => '\\x3E',
+ '\'' => '\\x27',
+ '"' => '\\x22',
+ '&' => '\\x26',
+ /* Characters beyond ASCII value 255 to unicode escape */
+ 'Ā' => '\\u0100',
+ /* Immune chars excluded */
+ ',' => ',',
+ '.' => '.',
+ '_' => '_',
+ /* Basic alnums excluded */
+ 'a' => 'a',
+ 'A' => 'A',
+ 'z' => 'z',
+ 'Z' => 'Z',
+ '0' => '0',
+ '9' => '9',
+ /* Basic control characters and null */
+ "\r" => '\\x0D',
+ "\n" => '\\x0A',
+ "\t" => '\\x09',
+ "\0" => '\\x00',
+ /* Encode spaces for quoteless attribute protection */
+ ' ' => '\\x20',
+ );
+
+ protected $urlSpecialChars = array(
+ /* HTML special chars - escape without exception to percent encoding */
+ '<' => '%3C',
+ '>' => '%3E',
+ '\'' => '%27',
+ '"' => '%22',
+ '&' => '%26',
+ /* Characters beyond ASCII value 255 to hex sequence */
+ 'Ā' => '%C4%80',
+ /* Punctuation and unreserved check */
+ ',' => '%2C',
+ '.' => '.',
+ '_' => '_',
+ '-' => '-',
+ ':' => '%3A',
+ ';' => '%3B',
+ '!' => '%21',
+ /* Basic alnums excluded */
+ 'a' => 'a',
+ 'A' => 'A',
+ 'z' => 'z',
+ 'Z' => 'Z',
+ '0' => '0',
+ '9' => '9',
+ /* Basic control characters and null */
+ "\r" => '%0D',
+ "\n" => '%0A',
+ "\t" => '%09',
+ "\0" => '%00',
+ /* PHP quirks from the past */
+ ' ' => '%20',
+ '~' => '~',
+ '+' => '%2B',
+ );
+
+ protected $cssSpecialChars = array(
+ /* HTML special chars - escape without exception to hex */
+ '<' => '\\3C ',
+ '>' => '\\3E ',
+ '\'' => '\\27 ',
+ '"' => '\\22 ',
+ '&' => '\\26 ',
+ /* Characters beyond ASCII value 255 to unicode escape */
+ 'Ā' => '\\100 ',
+ /* Immune chars excluded */
+ ',' => '\\2C ',
+ '.' => '\\2E ',
+ '_' => '\\5F ',
+ /* Basic alnums excluded */
+ 'a' => 'a',
+ 'A' => 'A',
+ 'z' => 'z',
+ 'Z' => 'Z',
+ '0' => '0',
+ '9' => '9',
+ /* Basic control characters and null */
+ "\r" => '\\D ',
+ "\n" => '\\A ',
+ "\t" => '\\9 ',
+ "\0" => '\\0 ',
+ /* Encode spaces for quoteless attribute protection */
+ ' ' => '\\20 ',
+ );
+
+ protected $env;
+
+ public function setUp()
+ {
+ $this->env = new Twig_Environment();
+ }
+
+ public function testHtmlEscapingConvertsSpecialChars()
+ {
+ foreach ($this->htmlSpecialChars as $key => $value) {
+ $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html'), 'Failed to escape: '.$key);
+ }
+ }
+
+ public function testHtmlAttributeEscapingConvertsSpecialChars()
+ {
+ foreach ($this->htmlAttrSpecialChars as $key => $value) {
+ $this->assertEquals($value, twig_escape_filter($this->env, $key, 'html_attr'), 'Failed to escape: '.$key);
+ }
+ }
+
+ public function testJavascriptEscapingConvertsSpecialChars()
+ {
+ foreach ($this->jsSpecialChars as $key => $value) {
+ $this->assertEquals($value, twig_escape_filter($this->env, $key, 'js'), 'Failed to escape: '.$key);
+ }
+ }
+
+ public function testJavascriptEscapingReturnsStringIfZeroLength()
+ {
+ $this->assertEquals('', twig_escape_filter($this->env, '', 'js'));
+ }
+
+ public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits()
+ {
+ $this->assertEquals('123', twig_escape_filter($this->env, '123', 'js'));
+ }
+
+ public function testCssEscapingConvertsSpecialChars()
+ {
+ foreach ($this->cssSpecialChars as $key => $value) {
+ $this->assertEquals($value, twig_escape_filter($this->env, $key, 'css'), 'Failed to escape: '.$key);
+ }
+ }
+
+ public function testCssEscapingReturnsStringIfZeroLength()
+ {
+ $this->assertEquals('', twig_escape_filter($this->env, '', 'css'));
+ }
+
+ public function testCssEscapingReturnsStringIfContainsOnlyDigits()
+ {
+ $this->assertEquals('123', twig_escape_filter($this->env, '123', 'css'));
+ }
+
+ public function testUrlEscapingConvertsSpecialChars()
+ {
+ foreach ($this->urlSpecialChars as $key => $value) {
+ $this->assertEquals($value, twig_escape_filter($this->env, $key, 'url'), 'Failed to escape: '.$key);
+ }
+ }
+
+ /**
+ * Range tests to confirm escaped range of characters is within OWASP recommendation
+ */
+
+ /**
+ * Only testing the first few 2 ranges on this prot. function as that's all these
+ * other range tests require
+ */
+ public function testUnicodeCodepointConversionToUtf8()
+ {
+ $expected = " ~ޙ";
+ $codepoints = array(0x20, 0x7e, 0x799);
+ $result = '';
+ foreach ($codepoints as $value) {
+ $result .= $this->codepointToUtf8($value);
+ }
+ $this->assertEquals($expected, $result);
+ }
+
+ /**
+ * Convert a Unicode Codepoint to a literal UTF-8 character.
+ *
+ * @param int Unicode codepoint in hex notation
+ * @return string UTF-8 literal string
+ */
+ protected function codepointToUtf8($codepoint)
+ {
+ if ($codepoint < 0x80) {
+ return chr($codepoint);
+ }
+ if ($codepoint < 0x800) {
+ return chr($codepoint >> 6 & 0x3f | 0xc0)
+ . chr($codepoint & 0x3f | 0x80);
+ }
+ if ($codepoint < 0x10000) {
+ return chr($codepoint >> 12 & 0x0f | 0xe0)
+ . chr($codepoint >> 6 & 0x3f | 0x80)
+ . chr($codepoint & 0x3f | 0x80);
+ }
+ if ($codepoint < 0x110000) {
+ return chr($codepoint >> 18 & 0x07 | 0xf0)
+ . chr($codepoint >> 12 & 0x3f | 0x80)
+ . chr($codepoint >> 6 & 0x3f | 0x80)
+ . chr($codepoint & 0x3f | 0x80);
+ }
+ throw new Exception('Codepoint requested outside of Unicode range');
+ }
+
+ public function testJavascriptEscapingEscapesOwaspRecommendedRanges()
+ {
+ $immune = array(',', '.', '_'); // Exceptions to escaping ranges
+ for ($chr=0; $chr < 0xFF; $chr++) {
+ if ($chr >= 0x30 && $chr <= 0x39
+ || $chr >= 0x41 && $chr <= 0x5A
+ || $chr >= 0x61 && $chr <= 0x7A) {
+ $literal = $this->codepointToUtf8($chr);
+ $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js'));
+ } else {
+ $literal = $this->codepointToUtf8($chr);
+ if (in_array($literal, $immune)) {
+ $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'js'));
+ } else {
+ $this->assertNotEquals(
+ $literal,
+ twig_escape_filter($this->env, $literal, 'js'),
+ "$literal should be escaped!");
+ }
+ }
+ }
+ }
+
+ public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges()
+ {
+ $immune = array(',', '.', '-', '_'); // Exceptions to escaping ranges
+ for ($chr=0; $chr < 0xFF; $chr++) {
+ if ($chr >= 0x30 && $chr <= 0x39
+ || $chr >= 0x41 && $chr <= 0x5A
+ || $chr >= 0x61 && $chr <= 0x7A) {
+ $literal = $this->codepointToUtf8($chr);
+ $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr'));
+ } else {
+ $literal = $this->codepointToUtf8($chr);
+ if (in_array($literal, $immune)) {
+ $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'html_attr'));
+ } else {
+ $this->assertNotEquals(
+ $literal,
+ twig_escape_filter($this->env, $literal, 'html_attr'),
+ "$literal should be escaped!");
+ }
+ }
+ }
+ }
+
+ public function testCssEscapingEscapesOwaspRecommendedRanges()
+ {
+ $immune = array(); // CSS has no exceptions to escaping ranges
+ for ($chr=0; $chr < 0xFF; $chr++) {
+ if ($chr >= 0x30 && $chr <= 0x39
+ || $chr >= 0x41 && $chr <= 0x5A
+ || $chr >= 0x61 && $chr <= 0x7A) {
+ $literal = $this->codepointToUtf8($chr);
+ $this->assertEquals($literal, twig_escape_filter($this->env, $literal, 'css'));
+ } else {
+ $literal = $this->codepointToUtf8($chr);
+ $this->assertNotEquals(
+ $literal,
+ twig_escape_filter($this->env, $literal, 'css'),
+ "$literal should be escaped!");
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
+Twig_Autoloader::register(true);
--- /dev/null
+vendor
+phpunit.xml
+composer.lock
--- /dev/null
+language: php
+
+before_script:
+ - curl -s http://getcomposer.org/installer | php
+ - php composer.phar install --dev
+
+php:
+ - 5.3
+ - 5.4
+
--- /dev/null
+Copyright (c) Саша Стаменковић <umpirsky@gmail.com>
+
+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.
\ No newline at end of file
--- /dev/null
+Twig Gettext Extractor [![Build Status](https://secure.travis-ci.org/umpirsky/Twig-Gettext-Extractor.png?branch=master)](http://travis-ci.org/umpirsky/Twig-Gettext-Extractor)
+======================
+
+The Twig Gettext Extractor is [Poedit](http://www.poedit.net/download.php)
+friendly tool which extracts translations from twig templates.
+
+## Installation
+
+The recommended way to install Twig Gettext Extractor is through
+[composer](http://getcomposer.org).
+
+```json
+{
+ "require": {
+ "umpirsky/twig-gettext-extractor": "1.1.*"
+ }
+}
+```
+
+## Setup
+
+By default, Poedit does not have the ability to parse Twig templates.
+This can be resolved by adding an additional parser (Edit > Preferences > Parsers)
+with the following options:
+
+- Language: `Twig`
+- List of extensions: `*.twig`
+- Invocation:
+ - Parser command: `<project>/vendor/bin/twig-gettext-extractor --sort-output --force-po -o %o %C %K -L PHP --files %F`
+ - An item in keyword list: `-k%k`
+ - An item in input file list: `%f`
+ - Source code charset: `--from-code=%c`
+
+<img src="http://i.imgur.com/f9px2.png" />
+
+Now you can update your catalog and Poedit will synchronize it with your twig
+templates.
+
+## Tests
+
+To run the test suite, you need [composer](http://getcomposer.org) and
+[PHPUnit](https://github.com/sebastianbergmann/phpunit).
+
+ $ composer install --dev
+ $ phpunit
+
+## License
+
+Twig Gettext Extractor is licensed under the MIT license.
--- /dev/null
+{
+ "name": "umpirsky/twig-gettext-extractor",
+ "type": "application",
+ "description": "The Twig Gettext Extractor is Poedit friendly tool which extracts translations from twig templates.",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Саша Стаменковић",
+ "email": "umpirsky@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.3",
+ "twig/twig": ">=1.2.0,<2.0-dev",
+ "twig/extensions": "1.0.*",
+ "symfony/twig-bridge": ">=2.0,<3.0",
+ "symfony/routing": ">=2.0,<3.0",
+ "symfony/filesystem": ">=2.0,<3.0",
+ "symfony/translation": ">=2.0,<3.0",
+ "symfony/form": ">=2.0,<3.0"
+ },
+ "require-dev": {
+ "symfony/config": "2.1.*"
+ },
+ "minimum-stability": "dev",
+ "autoload": {
+ "psr-0": { "Twig\\Gettext": "." }
+ },
+ "bin": ["twig-gettext-extractor"]
+}
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ bootstrap="./vendor/autoload.php"
+>
+ <testsuites>
+ <testsuite name="Twig Gettext Extractor Test Suite">
+ <directory>./Twig/Gettext/Test/</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>
--- /dev/null
+#!/usr/bin/env php
+<?php
+
+/**
+ * This file is part of the Twig Gettext utility.
+ *
+ * (c) Саша Стаменковић <umpirsky@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Extracts translations from twig templates.
+ *
+ * @author Саша Стаменковић <umpirsky@gmail.com>
+ */
+
+if (file_exists($a = __DIR__.'/../../autoload.php')) {
+ require_once $a;
+} else {
+ require_once __DIR__.'/vendor/autoload.php';
+}
+
+$twig = new Twig_Environment(new Twig\Gettext\Loader\Filesystem('/'), array(
+ 'cache' => '/tmp/cache/'.uniqid(),
+ 'auto_reload' => true
+));
+$twig->addExtension(new Symfony\Bridge\Twig\Extension\TranslationExtension(
+ new Symfony\Component\Translation\Translator(null)
+));
+$twig->addExtension(new Twig_Extensions_Extension_I18n());
+$twig->addExtension(new Symfony\Bridge\Twig\Extension\RoutingExtension(
+ new Twig\Gettext\Routing\Generator\UrlGenerator()
+));
+$twig->addExtension(new Symfony\Bridge\Twig\Extension\FormExtension(
+ new Symfony\Bridge\Twig\Form\TwigRenderer(
+ new Symfony\Bridge\Twig\Form\TwigRendererEngine()
+ )
+));
+// You can add more extensions here.
+
+array_shift($_SERVER['argv']);
+$addTemplate = false;
+
+$extractor = new Twig\Gettext\Extractor($twig);
+
+foreach ($_SERVER['argv'] as $arg) {
+ if ('--files' == $arg) {
+ $addTemplate = true;
+ } else if ($addTemplate) {
+ $extractor->addTemplate(getcwd().DIRECTORY_SEPARATOR.$arg);
+ } else {
+ $extractor->addGettextParameter($arg);
+ }
+}
+
+$extractor->extract();