123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@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;
- use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
- use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
- /**
- * 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
- {
- private $expressionLanguage;
- /**
- * @var ExpressionFunctionProviderInterface[]
- */
- private $expressionLanguageProviders = array();
- /**
- * 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;
- }
- public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
- {
- $this->expressionLanguageProviders[] = $provider;
- }
- /**
- * Generates the code for the match method implementing UrlMatcherInterface.
- *
- * @param bool $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);
- \$context = \$this->context;
- \$request = \$this->request;
- $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 bool $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 bool $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 bool $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 = $route->getMethods();
- // 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;
- }
- if ($route->getCondition()) {
- $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
- }
- $conditions = implode(' && ', $conditions);
- $code .= <<<EOF
- // $name
- if ($conditions) {
- EOF;
- $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
- if ($methods) {
- 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 ($schemes = $route->getSchemes()) {
- if (!$supportsRedirections) {
- throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
- }
- $schemes = str_replace("\n", '', var_export(array_flip($schemes), true));
- $code .= <<<EOF
- \$requiredSchemes = $schemes;
- if (!isset(\$requiredSchemes[\$this->context->getScheme()])) {
- return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes));
- }
- 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;
- }
- private function getExpressionLanguage()
- {
- if (null === $this->expressionLanguage) {
- if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
- throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
- }
- $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
- }
- return $this->expressionLanguage;
- }
- }
|