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@17: use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; Chris@17: use Symfony\Component\ExpressionLanguage\ExpressionLanguage; Chris@0: use Symfony\Component\Routing\Route; Chris@0: use Symfony\Component\Routing\RouteCollection; 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@17: private $expressionLanguageProviders = []; 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@17: public function dump(array $options = []) Chris@0: { Chris@17: $options = array_replace([ Chris@0: 'class' => 'ProjectUrlMatcher', Chris@0: 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', Chris@17: ], $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@14: \$request = \$this->request ?: \$this->createRequest(\$pathinfo); Chris@14: \$requestMethod = \$canonicalMethod = \$context->getMethod(); Chris@14: Chris@14: if ('HEAD' === \$requestMethod) { Chris@14: \$canonicalMethod = 'GET'; Chris@14: } 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: $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@14: $code .= " \$host = \$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@14: $tree = $this->buildStaticPrefixCollection($collection); Chris@14: $groupCode = $this->compileStaticPrefixRoutes($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@14: // used to display the Welcome Page in apps that don't define a homepage Chris@14: $code .= " if ('/' === \$pathinfo && !\$allow) {\n"; Chris@14: $code .= " throw new Symfony\Component\Routing\Exception\NoConfigurationException();\n"; Chris@14: $code .= " }\n"; Chris@14: Chris@0: return $code; Chris@0: } Chris@0: Chris@14: private function buildStaticPrefixCollection(DumperCollection $collection) Chris@14: { Chris@14: $prefixCollection = new StaticPrefixCollection(); Chris@14: Chris@14: foreach ($collection as $dumperRoute) { Chris@14: $prefix = $dumperRoute->getRoute()->compile()->getStaticPrefix(); Chris@14: $prefixCollection->addRoute($prefix, $dumperRoute); Chris@14: } Chris@14: Chris@14: $prefixCollection->optimizeGroups(); Chris@14: Chris@14: return $prefixCollection; Chris@14: } Chris@14: Chris@0: /** Chris@14: * Generates PHP code to match a tree of routes. Chris@0: * Chris@14: * @param StaticPrefixCollection $collection A StaticPrefixCollection instance Chris@0: * @param bool $supportsRedirections Whether redirections are supported by the base class Chris@14: * @param string $ifOrElseIf either "if" or "elseif" to influence chaining Chris@0: * Chris@0: * @return string PHP code Chris@0: */ Chris@14: private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if') Chris@0: { Chris@0: $code = ''; Chris@0: $prefix = $collection->getPrefix(); Chris@0: Chris@14: if (!empty($prefix) && '/' !== $prefix) { Chris@14: $code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true)); Chris@0: } Chris@0: Chris@14: $ifOrElseIf = 'if'; Chris@14: Chris@14: foreach ($collection->getItems() as $route) { Chris@14: if ($route instanceof StaticPrefixCollection) { Chris@14: $code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf); Chris@14: $ifOrElseIf = 'elseif'; Chris@0: } else { Chris@14: $code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n"; Chris@14: $ifOrElseIf = 'if'; Chris@0: } Chris@0: } Chris@0: Chris@14: if (!empty($prefix) && '/' !== $prefix) { 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@17: $conditions = []; Chris@0: $hasTrailingSlash = false; Chris@0: $matches = false; Chris@0: $hostMatches = false; Chris@0: $methods = $route->getMethods(); Chris@0: Chris@17: $supportsTrailingSlash = $supportsRedirections && (!$methods || \in_array('GET', $methods)); Chris@0: $regex = $compiledRoute->getRegex(); Chris@0: Chris@17: if (!\count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P.*?)\$\1#'.('u' === substr($regex, -1) ? 'u' : ''), $regex, $m)) { Chris@14: if ($supportsTrailingSlash && '/' === substr($m['url'], -1)) { Chris@14: $conditions[] = sprintf('%s === $trimmedPathinfo', var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true)); Chris@0: $hasTrailingSlash = true; Chris@0: } else { Chris@14: $conditions[] = sprintf('%s === $pathinfo', 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@17: $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), ['context', 'request']); Chris@0: } Chris@0: Chris@0: $conditions = implode(' && ', $conditions); Chris@0: Chris@0: $code .= <<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@17: $code .= sprintf(" \$ret = %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), ['_route' => $name]), true))); Chris@0: } else { Chris@17: $code .= sprintf(" \$ret = ['_route' => '%s'];\n", $name); Chris@14: } Chris@14: Chris@14: if ($hasTrailingSlash) { Chris@14: $code .= <<redirect(\$rawPathinfo.'/', '$name')); Chris@14: } Chris@14: Chris@14: Chris@14: EOF; Chris@14: } Chris@14: Chris@14: if ($methods) { Chris@17: $methodVariable = \in_array('GET', $methods) ? '$canonicalMethod' : '$requestMethod'; Chris@14: $methods = implode("', '", $methods); Chris@14: } Chris@14: Chris@14: if ($schemes = $route->getSchemes()) { Chris@14: if (!$supportsRedirections) { Chris@14: throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.'); Chris@14: } Chris@14: $schemes = str_replace("\n", '', var_export(array_flip($schemes), true)); Chris@14: if ($methods) { Chris@14: $code .= <<getScheme()]); Chris@17: if (!in_array($methodVariable, ['$methods'])) { Chris@14: if (\$hasRequiredScheme) { Chris@17: \$allow = array_merge(\$allow, ['$methods']); Chris@14: } Chris@14: goto $gotoname; Chris@14: } Chris@14: if (!\$hasRequiredScheme) { Chris@14: if ('GET' !== \$canonicalMethod) { Chris@14: goto $gotoname; Chris@14: } Chris@14: Chris@14: return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes))); Chris@14: } Chris@14: Chris@14: Chris@14: EOF; Chris@14: } else { Chris@14: $code .= <<getScheme()])) { Chris@14: if ('GET' !== \$canonicalMethod) { Chris@14: goto $gotoname; Chris@14: } Chris@14: Chris@14: return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes))); Chris@14: } Chris@14: Chris@14: Chris@14: EOF; Chris@14: } Chris@14: } elseif ($methods) { Chris@14: $code .= <<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: 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: }