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"