Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Routing\Matcher\Dumper; Chris@0: Chris@0: use Symfony\Component\Routing\Route; Chris@0: use Symfony\Component\Routing\RouteCollection; Chris@0: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; Chris@0: use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; Chris@0: Chris@0: /** Chris@0: * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: * @author Tobias Schultze Chris@0: * @author Arnaud Le Blanc Chris@0: */ Chris@0: class PhpMatcherDumper extends MatcherDumper Chris@0: { Chris@0: private $expressionLanguage; Chris@0: Chris@0: /** Chris@0: * @var ExpressionFunctionProviderInterface[] Chris@0: */ Chris@0: private $expressionLanguageProviders = array(); Chris@0: Chris@0: /** Chris@0: * Dumps a set of routes to a PHP class. Chris@0: * Chris@0: * Available options: Chris@0: * Chris@0: * * class: The class name Chris@0: * * base_class: The base class name Chris@0: * Chris@0: * @param array $options An array of options Chris@0: * Chris@0: * @return string A PHP class representing the matcher class Chris@0: */ Chris@0: public function dump(array $options = array()) Chris@0: { Chris@0: $options = array_replace(array( Chris@0: 'class' => 'ProjectUrlMatcher', Chris@0: 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', Chris@0: ), $options); Chris@0: Chris@0: // trailing slash support is only enabled if we know how to redirect the user Chris@0: $interfaces = class_implements($options['base_class']); Chris@0: $supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']); Chris@0: Chris@0: return <<context = \$context; Chris@0: } Chris@0: Chris@0: {$this->generateMatchMethod($supportsRedirections)} Chris@0: } Chris@0: Chris@0: EOF; Chris@0: } Chris@0: Chris@0: public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) Chris@0: { Chris@0: $this->expressionLanguageProviders[] = $provider; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates the code for the match method implementing UrlMatcherInterface. Chris@0: * Chris@0: * @param bool $supportsRedirections Whether redirections are supported by the base class Chris@0: * Chris@0: * @return string Match method as PHP code Chris@0: */ Chris@0: private function generateMatchMethod($supportsRedirections) Chris@0: { Chris@0: $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n"); Chris@0: Chris@0: return <<context; Chris@0: \$request = \$this->request; Chris@0: Chris@0: $code Chris@0: Chris@0: throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException(); Chris@0: } Chris@0: EOF; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates PHP code to match a RouteCollection with all its routes. Chris@0: * Chris@0: * @param RouteCollection $routes A RouteCollection instance Chris@0: * @param bool $supportsRedirections Whether redirections are supported by the base class Chris@0: * Chris@0: * @return string PHP code Chris@0: */ Chris@0: private function compileRoutes(RouteCollection $routes, $supportsRedirections) Chris@0: { Chris@0: $fetchedHost = false; Chris@0: Chris@0: $groups = $this->groupRoutesByHostRegex($routes); Chris@0: $code = ''; Chris@0: Chris@0: foreach ($groups as $collection) { Chris@0: if (null !== $regex = $collection->getAttribute('host_regex')) { Chris@0: if (!$fetchedHost) { Chris@0: $code .= " \$host = \$this->context->getHost();\n\n"; Chris@0: $fetchedHost = true; Chris@0: } Chris@0: Chris@0: $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true)); Chris@0: } Chris@0: Chris@0: $tree = $this->buildPrefixTree($collection); Chris@0: $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections); Chris@0: Chris@0: if (null !== $regex) { Chris@0: // apply extra indention at each line (except empty ones) Chris@0: $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode); Chris@0: $code .= $groupCode; Chris@0: $code .= " }\n\n"; Chris@0: } else { Chris@0: $code .= $groupCode; Chris@0: } Chris@0: } Chris@0: Chris@0: return $code; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates PHP code recursively to match a tree of routes. Chris@0: * Chris@0: * @param DumperPrefixCollection $collection A DumperPrefixCollection instance Chris@0: * @param bool $supportsRedirections Whether redirections are supported by the base class Chris@0: * @param string $parentPrefix Prefix of the parent collection Chris@0: * Chris@0: * @return string PHP code Chris@0: */ Chris@0: private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '') Chris@0: { Chris@0: $code = ''; Chris@0: $prefix = $collection->getPrefix(); Chris@0: $optimizable = 1 < strlen($prefix) && 1 < count($collection->all()); Chris@0: $optimizedPrefix = $parentPrefix; Chris@0: Chris@0: if ($optimizable) { Chris@0: $optimizedPrefix = $prefix; Chris@0: Chris@0: $code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true)); Chris@0: } Chris@0: Chris@0: foreach ($collection as $route) { Chris@0: if ($route instanceof DumperCollection) { Chris@0: $code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix); Chris@0: } else { Chris@0: $code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n"; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($optimizable) { Chris@0: $code .= " }\n\n"; Chris@0: // apply extra indention at each line (except empty ones) Chris@0: $code = preg_replace('/^.{2,}$/m', ' $0', $code); Chris@0: } Chris@0: Chris@0: return $code; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Compiles a single Route to PHP code used to match it against the path info. Chris@0: * Chris@0: * @param Route $route A Route instance Chris@0: * @param string $name The name of the Route Chris@0: * @param bool $supportsRedirections Whether redirections are supported by the base class Chris@0: * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code Chris@0: * Chris@0: * @return string PHP code Chris@0: * Chris@0: * @throws \LogicException Chris@0: */ Chris@0: private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null) Chris@0: { Chris@0: $code = ''; Chris@0: $compiledRoute = $route->compile(); Chris@0: $conditions = array(); Chris@0: $hasTrailingSlash = false; Chris@0: $matches = false; Chris@0: $hostMatches = false; Chris@0: $methods = $route->getMethods(); Chris@0: Chris@0: // GET and HEAD are equivalent Chris@0: if (in_array('GET', $methods) && !in_array('HEAD', $methods)) { Chris@0: $methods[] = 'HEAD'; Chris@0: } Chris@0: Chris@0: $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods)); Chris@0: $regex = $compiledRoute->getRegex(); Chris@0: Chris@0: if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#'.(substr($regex, -1) === 'u' ? 'u' : ''), $regex, $m)) { Chris@0: if ($supportsTrailingSlash && substr($m['url'], -1) === '/') { Chris@0: $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); Chris@0: $hasTrailingSlash = true; Chris@0: } else { Chris@0: $conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true)); Chris@0: } Chris@0: } else { Chris@0: if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) { Chris@0: $conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true)); Chris@0: } Chris@0: Chris@0: if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { Chris@0: $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); Chris@0: $hasTrailingSlash = true; Chris@0: } Chris@0: $conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true)); Chris@0: Chris@0: $matches = true; Chris@0: } Chris@0: Chris@0: if ($compiledRoute->getHostVariables()) { Chris@0: $hostMatches = true; Chris@0: } Chris@0: Chris@0: if ($route->getCondition()) { Chris@0: $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request')); Chris@0: } Chris@0: Chris@0: $conditions = implode(' && ', $conditions); Chris@0: Chris@0: $code .= <<context->getMethod() != '$methods[0]') { Chris@0: \$allow[] = '$methods[0]'; Chris@0: goto $gotoname; Chris@0: } Chris@0: Chris@0: Chris@0: EOF; Chris@0: } else { Chris@0: $methods = implode("', '", $methods); Chris@0: $code .= <<context->getMethod(), array('$methods'))) { Chris@0: \$allow = array_merge(\$allow, array('$methods')); Chris@0: goto $gotoname; Chris@0: } Chris@0: Chris@0: Chris@0: EOF; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($hasTrailingSlash) { Chris@0: $code .= <<redirect(\$pathinfo.'/', '$name'); Chris@0: } Chris@0: Chris@0: Chris@0: EOF; Chris@0: } Chris@0: Chris@0: if ($schemes = $route->getSchemes()) { Chris@0: if (!$supportsRedirections) { Chris@0: throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); Chris@0: } Chris@0: $schemes = str_replace("\n", '', var_export(array_flip($schemes), true)); Chris@0: $code .= <<context->getScheme()])) { Chris@0: return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes)); Chris@0: } Chris@0: Chris@0: Chris@0: EOF; Chris@0: } Chris@0: Chris@0: // optimize parameters array Chris@0: if ($matches || $hostMatches) { Chris@0: $vars = array(); Chris@0: if ($hostMatches) { Chris@0: $vars[] = '$hostMatches'; Chris@0: } Chris@0: if ($matches) { Chris@0: $vars[] = '$matches'; Chris@0: } Chris@0: $vars[] = "array('_route' => '$name')"; Chris@0: Chris@0: $code .= sprintf( Chris@0: " return \$this->mergeDefaults(array_replace(%s), %s);\n", Chris@0: implode(', ', $vars), Chris@0: str_replace("\n", '', var_export($route->getDefaults(), true)) Chris@0: ); Chris@0: } elseif ($route->getDefaults()) { Chris@0: $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true))); Chris@0: } else { Chris@0: $code .= sprintf(" return array('_route' => '%s');\n", $name); Chris@0: } Chris@0: $code .= " }\n"; Chris@0: Chris@0: if ($methods) { Chris@0: $code .= " $gotoname:\n"; Chris@0: } Chris@0: Chris@0: return $code; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Groups consecutive routes having the same host regex. Chris@0: * Chris@0: * The result is a collection of collections of routes having the same host regex. Chris@0: * Chris@0: * @param RouteCollection $routes A flat RouteCollection Chris@0: * Chris@0: * @return DumperCollection A collection with routes grouped by host regex in sub-collections Chris@0: */ Chris@0: private function groupRoutesByHostRegex(RouteCollection $routes) Chris@0: { Chris@0: $groups = new DumperCollection(); Chris@0: Chris@0: $currentGroup = new DumperCollection(); Chris@0: $currentGroup->setAttribute('host_regex', null); Chris@0: $groups->add($currentGroup); Chris@0: Chris@0: foreach ($routes as $name => $route) { Chris@0: $hostRegex = $route->compile()->getHostRegex(); Chris@0: if ($currentGroup->getAttribute('host_regex') !== $hostRegex) { Chris@0: $currentGroup = new DumperCollection(); Chris@0: $currentGroup->setAttribute('host_regex', $hostRegex); Chris@0: $groups->add($currentGroup); Chris@0: } Chris@0: $currentGroup->add(new DumperRoute($name, $route)); Chris@0: } Chris@0: Chris@0: return $groups; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Organizes the routes into a prefix tree. Chris@0: * Chris@0: * Routes order is preserved such that traversing the tree will traverse the Chris@0: * routes in the origin order. Chris@0: * Chris@0: * @param DumperCollection $collection A collection of routes Chris@0: * Chris@0: * @return DumperPrefixCollection Chris@0: */ Chris@0: private function buildPrefixTree(DumperCollection $collection) Chris@0: { Chris@0: $tree = new DumperPrefixCollection(); Chris@0: $current = $tree; Chris@0: Chris@0: foreach ($collection as $route) { Chris@0: $current = $current->addPrefixRoute($route); Chris@0: } Chris@0: Chris@0: $tree->mergeSlashNodes(); Chris@0: Chris@0: return $tree; Chris@0: } Chris@0: Chris@0: private function getExpressionLanguage() Chris@0: { Chris@0: if (null === $this->expressionLanguage) { Chris@0: if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { Chris@0: throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); Chris@0: } Chris@0: $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); Chris@0: } Chris@0: Chris@0: return $this->expressionLanguage; Chris@0: } Chris@0: }