class PhpMatcherDumper

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>

Hierarchy

Expanded class hierarchy of PhpMatcherDumper

1 file declares its use of PhpMatcherDumper
PhpMatcherDumperTest.php in drupal/core/vendor/symfony/routing/Symfony/Component/Routing/Tests/Matcher/Dumper/PhpMatcherDumperTest.php

File

drupal/core/vendor/symfony/routing/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherDumper.php, line 23

Namespace

Symfony\Component\Routing\Matcher\Dumper
View source
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_merge(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;

/**
 * {<span class="php-variable">$options</span>[<span class="php-string">'class'</span>]}
 *
 * This class has been auto-generated
 * by the Symfony Routing Component.
 */
class {<span class="php-variable">$options</span>[<span class="php-string">'class'</span>]} extends {<span class="php-variable">$options</span>[<span class="php-string">'base_class'</span>]}
{
    /**
     * Constructor.
     */
    public function __construct(RequestContext \$context)
    {
        \$this->context = \$context;
    }

{<span class="php-variable">$this</span>
  -&gt;<span class="php-function-or-constant function member-of-self">generateMatchMethod</span>(<span class="php-variable">$supportsRedirections</span>)}
}

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);

{<span class="php-variable">$code</span>}

        throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
    }
EOF;
  }

  /**
   * Counts the number of routes as direct child of the RouteCollection.
   *
   * @param RouteCollection $routes A RouteCollection instance
   *
   * @return integer Number of Routes
   */
  private function countDirectChildRoutes(RouteCollection $routes) {
    $count = 0;
    foreach ($routes as $route) {
      if ($route instanceof Route) {
        $count++;
      }
    }
    return $count;
  }

  /**
   * Generates PHP code recursively to match a RouteCollection with all child routes and child collections.
   *
   * @param RouteCollection $routes               A RouteCollection instance
   * @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
   */
  private function compileRoutes(RouteCollection $routes, $supportsRedirections, $parentPrefix = null) {
    $code = '';
    $prefix = $routes
      ->getPrefix();
    $countDirectChildRoutes = $this
      ->countDirectChildRoutes($routes);
    $countAllChildRoutes = count($routes
      ->all());

    // Can the matching be optimized by wrapping it with the prefix condition
    // - no need to optimize if current prefix is the same as the parent prefix
    // - if $countDirectChildRoutes === 0, the sub-collections can do their own optimizations (in case there are any)
    // - it's not worth wrapping a single child route
    // - prefixes with variables cannot be optimized because routes within the collection might have different requirements for the same variable
    $optimizable = '' !== $prefix && $prefix !== $parentPrefix && $countDirectChildRoutes > 0 && $countAllChildRoutes > 1 && false === strpos($prefix, '{');
    if ($optimizable) {
      $code .= sprintf("    if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true));
    }
    foreach ($routes as $name => $route) {
      if ($route instanceof Route) {

        // a single route in a sub-collection is not wrapped so it should do its own optimization in ->compileRoute with $parentPrefix = null
        $code .= $this
          ->compileRoute($route, $name, $supportsRedirections, 1 === $countAllChildRoutes ? null : $prefix) . "\n";
      }
      elseif ($countAllChildRoutes - $countDirectChildRoutes > 0) {

        // we can stop iterating recursively if we already know there are no more routes
        $code .= $this
          ->compileRoutes($route, $supportsRedirections, $prefix);
      }
    }
    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
   */
  private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null) {
    $code = '';
    $compiledRoute = $route
      ->compile();
    $conditions = array();
    $hasTrailingSlash = false;
    $matches = 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
      ->getVariables()) && false !== preg_match('#^(.)\\^(?<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;
    }
    $conditions = implode(' && ', $conditions);
    $code .= <<<EOF
        // {<span class="php-variable">$name</span>}
        if ({<span class="php-variable">$conditions</span>}) {

EOF;
    if ($methods) {
      $gotoname = 'not_' . preg_replace('/[^A-Za-z0-9_]/', '', $name);
      if (1 === count($methods)) {
        $code .= <<<EOF
            if (\$this->context->getMethod() != '{<span class="php-variable">$methods</span>[<span class="php-constant">0</span>]}') {
                \$allow[] = '{<span class="php-variable">$methods</span>[<span class="php-constant">0</span>]}';
                goto {<span class="php-variable">$gotoname</span>};
            }


EOF;
      }
      else {
        $methods = implode("', '", $methods);
        $code .= <<<EOF
            if (!in_array(\$this->context->getMethod(), array('{<span class="php-variable">$methods</span>}'))) {
                \$allow = array_merge(\$allow, array('{<span class="php-variable">$methods</span>}'));
                goto {<span class="php-variable">$gotoname</span>};
            }


EOF;
      }
    }
    if ($hasTrailingSlash) {
      $code .= <<<EOF
            if (substr(\$pathinfo, -1) !== '/') {
                return \$this->redirect(\$pathinfo.'/', '{<span class="php-variable">$name</span>}');
            }


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() !== '{<span class="php-variable">$scheme</span>}') {
                return \$this->redirect(\$pathinfo, '{<span class="php-variable">$name</span>}', '{<span class="php-variable">$scheme</span>}');
            }


EOF;
    }

    // optimize parameters array
    if (true === $matches && $route
      ->getDefaults()) {
      $code .= sprintf("            return array_merge(\$this->mergeDefaults(\$matches, %s), array('_route' => '%s'));\n", str_replace("\n", '', var_export($route
        ->getDefaults(), true)), $name);
    }
    elseif (true === $matches) {
      $code .= sprintf("            \$matches['_route'] = '%s';\n\n", $name);
      $code .= "            return \$matches;\n";
    }
    elseif ($route
      ->getDefaults()) {
      $code .= sprintf("            return %s;\n", str_replace("\n", '', var_export(array_merge($route
        ->getDefaults(), array(
        '_route' => $name,
      )), true)));
    }
    else {
      $code .= sprintf("            return array('_route' => '%s');\n", $name);
    }
    $code .= "        }\n";
    if ($methods) {
      $code .= "        {$gotoname}:\n";
    }
    return $code;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MatcherDumper::$routes private property
MatcherDumper::getRoutes public function Gets the routes to dump. Overrides MatcherDumperInterface::getRoutes
MatcherDumper::__construct public function Constructor.
PhpMatcherDumper::compileRoute private function Compiles a single Route to PHP code used to match it against the path info.
PhpMatcherDumper::compileRoutes private function Generates PHP code recursively to match a RouteCollection with all child routes and child collections.
PhpMatcherDumper::countDirectChildRoutes private function Counts the number of routes as direct child of the RouteCollection.
PhpMatcherDumper::dump public function Dumps a set of routes to a PHP class. Overrides MatcherDumperInterface::dump
PhpMatcherDumper::generateMatchMethod private function Generates the code for the match method implementing UrlMatcherInterface.