4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Routing\Matcher\Dumper
;
14 use Symfony\Component\Routing\Route
;
15 use Symfony\Component\Routing\RouteCollection
;
18 * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
20 * @author Fabien Potencier <fabien@symfony.com>
21 * @author Tobias Schultze <http://tobion.de>
22 * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
24 class PhpMatcherDumper
extends MatcherDumper
27 * Dumps a set of routes to a PHP class.
31 * * class: The class name
32 * * base_class: The base class name
34 * @param array $options An array of options
36 * @return string A PHP class representing the matcher class
38 public function dump(array $options = array())
40 $options = array_replace(array(
41 'class' => 'ProjectUrlMatcher',
42 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
45 // trailing slash support is only enabled if we know how to redirect the user
46 $interfaces = class_implements($options['base_class']);
47 $supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']);
52 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
53 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
54 use Symfony\Component\Routing\RequestContext;
59 * This class has been auto-generated
60 * by the Symfony Routing Component.
62 class {$options['class']} extends {$options['base_class']}
67 public function __construct(RequestContext \$context)
69 \$this->context = \$context;
72 {$this->generateMatchMethod($supportsRedirections)}
79 * Generates the code for the match method implementing UrlMatcherInterface.
81 * @param Boolean $supportsRedirections Whether redirections are supported by the base class
83 * @return string Match method as PHP code
85 private function generateMatchMethod($supportsRedirections)
87 $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
90 public function match(\$pathinfo)
93 \$pathinfo = rawurldecode(\$pathinfo);
97 throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
103 * Generates PHP code to match a RouteCollection with all its routes.
105 * @param RouteCollection $routes A RouteCollection instance
106 * @param Boolean $supportsRedirections Whether redirections are supported by the base class
108 * @return string PHP code
110 private function compileRoutes(RouteCollection
$routes, $supportsRedirections)
112 $fetchedHost = false;
114 $groups = $this->groupRoutesByHostRegex($routes);
117 foreach ($groups as $collection) {
118 if (null !== $regex = $collection->getAttribute('host_regex')) {
120 $code .= " \$host = \$this->context->getHost();\n\n";
124 $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
127 $tree = $this->buildPrefixTree($collection);
128 $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections);
130 if (null !== $regex) {
131 // apply extra indention at each line (except empty ones)
132 $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode);
144 * Generates PHP code recursively to match a tree of routes
146 * @param DumperPrefixCollection $collection A DumperPrefixCollection instance
147 * @param Boolean $supportsRedirections Whether redirections are supported by the base class
148 * @param string $parentPrefix Prefix of the parent collection
150 * @return string PHP code
152 private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '')
155 $prefix = $collection->getPrefix();
156 $optimizable = 1 < strlen($prefix) && 1 < count($collection->all());
157 $optimizedPrefix = $parentPrefix;
160 $optimizedPrefix = $prefix;
162 $code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true));
165 foreach ($collection as $route) {
166 if ($route instanceof DumperCollection) {
167 $code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix);
169 $code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n";
175 // apply extra indention at each line (except empty ones)
176 $code = preg_replace('/^
.{2,}$
/m
', ' $0', $code);
183 * Compiles a single Route to PHP code used to match it against the path info.
185 * @param Route $route A Route instance
186 * @param string $name The name of the Route
187 * @param Boolean $supportsRedirections Whether redirections are supported by the base class
188 * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
190 * @return string PHP code
192 * @throws \LogicException
194 private function compileRoute(Route
$route, $name, $supportsRedirections, $parentPrefix = null)
197 $compiledRoute = $route->compile();
198 $conditions = array();
199 $hasTrailingSlash = false;
201 $hostMatches = false;
204 if ($req = $route->getRequirement('_method')) {
205 $methods = explode('|', strtoupper($req));
206 // GET and HEAD are equivalent
207 if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
212 $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));
214 if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
215 if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
216 $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
217 $hasTrailingSlash = true;
219 $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true));
222 if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
223 $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true));
226 $regex = $compiledRoute->getRegex();
227 if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
228 $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos +
2);
229 $hasTrailingSlash = true;
231 $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true));
236 if ($compiledRoute->getHostVariables()) {
240 $conditions = implode(' && ', $conditions);
249 $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
251 if (1 === count($methods)) {
253 if (\$this->context->getMethod() != '$methods[0]') {
254 \$allow[] = '$methods[0]';
261 $methods = implode("', '", $methods);
263 if (!in_array(\$this->context->getMethod(), array('$methods'))) {
264 \$allow = array_merge(\$allow, array('$methods'));
273 if ($hasTrailingSlash) {
275 if (substr(\$pathinfo, -1) !== '/') {
276 return \$this->redirect(\$pathinfo.'/', '$name');
283 if ($scheme = $route->getRequirement('_scheme')) {
284 if (!$supportsRedirections) {
285 throw new \
LogicException('The "_scheme" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
289 if (\$this->context->getScheme() !== '$scheme') {
290 return \$this->redirect(\$pathinfo, '$name', '$scheme');
297 // optimize parameters array
298 if ($matches || $hostMatches) {
301 $vars[] = '$hostMatches';
304 $vars[] = '$matches';
306 $vars[] = "array('_route' => '$name')";
308 $code .= sprintf(" return \$this->mergeDefaults(array_replace(%s), %s);\n"
309 , implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true)));
311 } elseif ($route->getDefaults()) {
312 $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
314 $code .= sprintf(" return array('_route' => '%s');\n", $name);
319 $code .= " $gotoname:\n";
326 * Groups consecutive routes having the same host regex.
328 * The result is a collection of collections of routes having the same host regex.
330 * @param RouteCollection $routes A flat RouteCollection
332 * @return DumperCollection A collection with routes grouped by host regex in sub-collections
334 private function groupRoutesByHostRegex(RouteCollection
$routes)
336 $groups = new DumperCollection();
338 $currentGroup = new DumperCollection();
339 $currentGroup->setAttribute('host_regex', null);
340 $groups->add($currentGroup);
342 foreach ($routes as $name => $route) {
343 $hostRegex = $route->compile()->getHostRegex();
344 if ($currentGroup->getAttribute('host_regex') !== $hostRegex) {
345 $currentGroup = new DumperCollection();
346 $currentGroup->setAttribute('host_regex', $hostRegex);
347 $groups->add($currentGroup);
349 $currentGroup->add(new DumperRoute($name, $route));
356 * Organizes the routes into a prefix tree.
358 * Routes order is preserved such that traversing the tree will traverse the
359 * routes in the origin order.
361 * @param DumperCollection $collection A collection of routes
363 * @return DumperPrefixCollection
365 private function buildPrefixTree(DumperCollection
$collection)
367 $tree = new DumperPrefixCollection();
370 foreach ($collection as $route) {
371 $current = $current->addPrefixRoute($route);
374 $tree->mergeSlashNodes();