diff --git a/composer.json b/composer.json
index 1dececdeeaf37..c4a02d56cd14b 100644
--- a/composer.json
+++ b/composer.json
@@ -88,7 +88,8 @@
"egulias/email-validator": "~1.2,>=1.2.8|~2.0",
"symfony/polyfill-apcu": "~1.1",
"symfony/security-acl": "~2.8|~3.0",
- "phpdocumentor/reflection-docblock": "^3.0"
+ "phpdocumentor/reflection-docblock": "^3.0",
+ "nikic/PHP-Parser": "~2.0"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.0",
diff --git a/src/Symfony/Component/Ast/.gitignore b/src/Symfony/Component/Ast/.gitignore
new file mode 100644
index 0000000000000..c49a5d8df5c65
--- /dev/null
+++ b/src/Symfony/Component/Ast/.gitignore
@@ -0,0 +1,3 @@
+vendor/
+composer.lock
+phpunit.xml
diff --git a/src/Symfony/Component/Ast/AstDumper.php b/src/Symfony/Component/Ast/AstDumper.php
new file mode 100644
index 0000000000000..af5bd39d564a3
--- /dev/null
+++ b/src/Symfony/Component/Ast/AstDumper.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Ast;
+
+use PhpParser\PrettyPrinterAbstract;
+use PhpParser\PrettyPrinter\Standard;
+
+final class AstDumper
+{
+ private $printer;
+
+ public function __construct(PrettyPrinterAbstract $printer = null)
+ {
+ if (null === $printer) {
+ $printer = new Standard();
+ }
+
+ $this->printer = $printer;
+ }
+
+ public function dump(NodeList $nodeList)
+ {
+ return $this->printer->prettyPrintFile($nodeList->getNodes());
+ }
+}
diff --git a/src/Symfony/Component/Ast/LICENSE b/src/Symfony/Component/Ast/LICENSE
new file mode 100644
index 0000000000000..0564c5a9b7f1f
--- /dev/null
+++ b/src/Symfony/Component/Ast/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2016 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.
diff --git a/src/Symfony/Component/Ast/NodeList.php b/src/Symfony/Component/Ast/NodeList.php
new file mode 100644
index 0000000000000..9f8e08dc7f9a8
--- /dev/null
+++ b/src/Symfony/Component/Ast/NodeList.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Ast;
+
+use PhpParser\Node;
+
+final class NodeList
+{
+ private $nodes = array();
+
+ /**
+ * @param Node[] $nodes
+ */
+ public function __construct(array $nodes)
+ {
+ foreach ($nodes as $node) {
+ $this->addNode($node);
+ }
+ }
+
+ /**
+ * @return Node[]
+ */
+ public function getNodes()
+ {
+ return $this->nodes;
+ }
+
+ public function addNode(Node $node)
+ {
+ $this->nodes[] = $node;
+ }
+
+ public function append(NodeList $nodeList)
+ {
+ foreach ($nodeList->getNodes() as $node) {
+ $this->addNode($node);
+ }
+ }
+}
diff --git a/src/Symfony/Component/Ast/README.md b/src/Symfony/Component/Ast/README.md
new file mode 100644
index 0000000000000..d880147946395
--- /dev/null
+++ b/src/Symfony/Component/Ast/README.md
@@ -0,0 +1,8 @@
+AstGenerator Component
+======================
+
+AstGenerator allows to generate PHP AST for several Component:
+
+ * Transform class, properties and types extracted from the PropertyInfo Component into POPO objects ans Normalizers
+ compatible with Serializer Component
+
diff --git a/src/Symfony/Component/Ast/Util/AstHelper.php b/src/Symfony/Component/Ast/Util/AstHelper.php
new file mode 100644
index 0000000000000..32c0148f90efc
--- /dev/null
+++ b/src/Symfony/Component/Ast/Util/AstHelper.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Ast\Util;
+
+use PhpParser\Node;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Name;
+use PhpParser\Node\Scalar;
+use PhpParser\ParserFactory;
+use PhpParser\PrettyPrinter\Standard;
+
+/**
+ * @internal
+ */
+final class AstHelper
+{
+ private static $parser;
+
+ /**
+ * @param Node[] $stmts
+ *
+ * @return string the php code
+ */
+ public static function dump(array $stmts)
+ {
+ $printer = new Standard();
+
+ return $printer->prettyPrintFile($stmts);
+ }
+
+ /**
+ * Transforms php code into an ast node.
+ *
+ * @param string $code the php code
+ *
+ * @return Node[]
+ */
+ public static function raw($code)
+ {
+ $code = "create(ParserFactory::ONLY_PHP5);
+ }
+
+ return self::$parser->parse($code);
+ }
+
+ /**
+ * Transforms a php value into an AST node.
+ *
+ * @param null|bool|int|float|string|array $value
+ *
+ * @return Expr
+ */
+ public static function value($value)
+ {
+ if (is_null($value)) {
+ return new Expr\ConstFetch(
+ new Name('null')
+ );
+ } elseif (is_bool($value)) {
+ return new Expr\ConstFetch(
+ new Name($value ? 'true' : 'false')
+ );
+ } elseif (is_int($value)) {
+ return new Scalar\LNumber($value);
+ } elseif (is_float($value)) {
+ return new Scalar\DNumber($value);
+ } elseif (is_string($value)) {
+ return new Scalar\String_($value);
+ } elseif (is_array($value)) {
+ $items = array();
+ $lastKey = -1;
+ foreach ($value as $itemKey => $itemValue) {
+ // for consecutive, numeric keys don't generate keys
+ if (null !== $lastKey && ++$lastKey === $itemKey) {
+ $items[] = new Expr\ArrayItem(
+ self::value($itemValue)
+ );
+ } else {
+ $lastKey = null;
+ $items[] = new Expr\ArrayItem(
+ self::value($itemValue),
+ self::value($itemKey)
+ );
+ }
+ }
+
+ return new Expr\Array_($items);
+ } else {
+ throw new \LogicException('Invalid value');
+ }
+ }
+
+ private function __construct()
+ {
+ }
+}
diff --git a/src/Symfony/Component/Ast/composer.json b/src/Symfony/Component/Ast/composer.json
new file mode 100644
index 0000000000000..fc8dbc2b0d8d2
--- /dev/null
+++ b/src/Symfony/Component/Ast/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "symfony/ast",
+ "type": "library",
+ "description": "Symfony AST Component",
+ "keywords": ["symfony", "ast"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Joel Wurtz",
+ "email": "joel.wurtz@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=5.5.9",
+ "nikic/php-parser": "~2.0"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Ast\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ }
+}
diff --git a/src/Symfony/Component/Ast/phpunit.xml.dist b/src/Symfony/Component/Ast/phpunit.xml.dist
new file mode 100644
index 0000000000000..5484c4319c9e4
--- /dev/null
+++ b/src/Symfony/Component/Ast/phpunit.xml.dist
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+ ./Tests
+ ./vendor
+
+
+
+
diff --git a/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGenerator.php b/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGenerator.php
new file mode 100644
index 0000000000000..10fcc33186f8e
--- /dev/null
+++ b/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGenerator.php
@@ -0,0 +1,161 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\AstGenerator;
+
+use PhpParser\BuilderFactory;
+use PhpParser\Comment;
+use PhpParser\Node\Name;
+use PhpParser\Node\Param;
+use PhpParser\Node\Stmt;
+use PhpParser\Node\Expr;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Ast\NodeList;
+use Symfony\Component\Ast\Util\AstHelper;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * @author Guilhem N.
+ */
+final class GeneratorAstGenerator implements GeneratorAstGeneratorInterface
+{
+ public function __construct(RouteCollection $routes)
+ {
+ $this->routes = $routes;
+ $this->factory = new BuilderFactory();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generate(array $options = array())
+ {
+ $options = array_replace(array(
+ 'class' => 'ProjectUrlGenerator',
+ 'base_class' => UrlGenerator::class,
+ ), $options);
+
+ return new NodeList(array($this->generateClass($options)->getNode()));
+ }
+
+ private function generateClass(array $options)
+ {
+ $docComment = <<factory->class($options['class'])
+ ->setDocComment($docComment)
+ ->extend($options['base_class'])
+ ->addStmt($this->factory->property('declaredRoutes')->makePrivate()->makeStatic())
+ ->addStmt($this->generateConstructor())
+ ->addStmt($this->generateGenerateMethod());
+ }
+
+ private function generateConstructor()
+ {
+ $constructor = $this->factory->method('__construct')
+ ->makePublic()
+ ->addParam($this->factory->param('context')->setTypeHint(RequestContext::class))
+ ->addParam($this->factory->param('logger')->setTypeHint(LoggerInterface::class)->setDefault(null));
+
+ $code = <<<'EOF'
+$this->context = $context;
+$this->logger = $logger;
+EOF;
+ foreach (AstHelper::raw($code) as $stmt) {
+ $constructor->addStmt($stmt);
+ }
+
+ $constructor->addStmt(new Stmt\If_(
+ new Expr\BinaryOp\Equal(
+ AstHelper::value(null),
+ new Expr\StaticPropertyFetch(new Name('self'), 'declaredRoutes')
+ ),
+ array(
+ 'stmts' => array(
+ new Expr\Assign(
+ new Expr\StaticPropertyFetch(new Name('self'), 'declaredRoutes'),
+ $this->generateDeclaredRoutes()
+ ),
+ ),
+ )
+ ));
+
+ return $constructor;
+ }
+
+ /**
+ * Generates an AST node representing an array of defined routes
+ * together with the routes properties (e.g. requirements).
+ *
+ * @return Expr\Array_
+ */
+ private function generateDeclaredRoutes()
+ {
+ $routes = array();
+ foreach ($this->routes->all() as $name => $route) {
+ $compiledRoute = $route->compile();
+
+ $properties = array();
+ $properties[] = $compiledRoute->getVariables();
+ $properties[] = $route->getDefaults();
+ $properties[] = $route->getRequirements();
+ $properties[] = $compiledRoute->getTokens();
+ $properties[] = $compiledRoute->getHostTokens();
+ $properties[] = $route->getSchemes();
+
+ $routes[$name] = $properties;
+ }
+
+ return AstHelper::value($routes);
+ }
+
+ /**
+ * Generates an AST node representing the `generate` method that implements the UrlGeneratorInterface.
+ *
+ * @return string PHP code
+ */
+ private function generateGenerateMethod()
+ {
+ $generateMethod = $this->factory
+ ->method('generate')
+ ->makePublic()
+ ->addParam($this->factory->param('name'))
+ ->addParam($this->factory->param('parameters')->setDefault(array()))
+ ->addParam($this->factory->param('referenceType')->setDefault(UrlGenerator::ABSOLUTE_PATH));
+
+ $exception = RouteNotFoundException::class;
+ $code = <<doGenerate(\$variables, \$defaults, \$requirements, \$tokens, \$parameters, \$name, \$referenceType, \$hostTokens, \$requiredSchemes);
+EOF;
+
+ foreach (AstHelper::raw($code) as $stmt) {
+ $generateMethod->addStmt($stmt);
+ }
+
+ return $generateMethod;
+ }
+}
diff --git a/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGeneratorInterface.php b/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGeneratorInterface.php
new file mode 100644
index 0000000000000..725b5b4b23e4a
--- /dev/null
+++ b/src/Symfony/Component/Routing/Generator/AstGenerator/GeneratorAstGeneratorInterface.php
@@ -0,0 +1,37 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Routing\Generator\AstGenerator;
+
+use PhpParser\Node\Name;
+use PhpParser\Node\Param;
+use Symfony\Component\Ast\NodeList;
+
+/**
+ * @author Guilhem N.
+ */
+interface GeneratorAstGeneratorInterface
+{
+ /**
+ * Dumps a set of routes to an ast representation that
+ * can then be used to generate a URL of such a route.
+ *
+ * Available options:
+ *
+ * * class: The class name
+ * * base_class: The base class name
+ *
+ * @param array $options An array of options
+ *
+ * @return NodeList
+ */
+ public function generate(array $options = array());
+}
diff --git a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php
index 9fd35ea112559..f49f3bc6d6669 100644
--- a/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php
+++ b/src/Symfony/Component/Routing/Generator/Dumper/PhpGeneratorDumper.php
@@ -11,6 +11,9 @@
namespace Symfony\Component\Routing\Generator\Dumper;
+use Symfony\Component\Ast\AstDumper;
+use Symfony\Component\Routing\Generator\AstGenerator\GeneratorAstGenerator;
+
/**
* PhpGeneratorDumper creates a PHP class able to generate URLs for a given set of routes.
*
@@ -33,91 +36,9 @@ class PhpGeneratorDumper extends GeneratorDumper
*/
public function dump(array $options = array())
{
- $options = array_merge(array(
- 'class' => 'ProjectUrlGenerator',
- 'base_class' => 'Symfony\\Component\\Routing\\Generator\\UrlGenerator',
- ), $options);
-
- return <<context = \$context;
- \$this->logger = \$logger;
- if (null === self::\$declaredRoutes) {
- self::\$declaredRoutes = {$this->generateDeclaredRoutes()};
- }
- }
-
-{$this->generateGenerateMethod()}
-}
-
-EOF;
- }
+ $dumper = new AstDumper();
+ $generator = new GeneratorAstGenerator($this->getRoutes());
- /**
- * 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();
- $properties[] = $route->getSchemes();
-
- $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, $requiredSchemes) = self::$declaredRoutes[$name];
-
- return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
- }
-EOF;
+ return $dumper->dump($generator->generate($options));
}
}
diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json
index 156bbecb2a3bd..fb59944ae46b6 100644
--- a/src/Symfony/Component/Routing/composer.json
+++ b/src/Symfony/Component/Routing/composer.json
@@ -23,6 +23,7 @@
"symfony/http-foundation": "~2.8|~3.0",
"symfony/yaml": "~2.8|~3.0",
"symfony/expression-language": "~2.8|~3.0",
+ "symfony/ast": "~3.2",
"doctrine/annotations": "~1.0",
"doctrine/common": "~2.2",
"psr/log": "~1.0"