annotate vendor/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 1fec387a4317
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of the Symfony package.
Chris@0 5 *
Chris@0 6 * (c) Fabien Potencier <fabien@symfony.com>
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Symfony\Component\Routing\Matcher\Dumper;
Chris@0 13
Chris@0 14 use Symfony\Component\Routing\Route;
Chris@0 15 use Symfony\Component\Routing\RouteCollection;
Chris@0 16 use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
Chris@0 17 use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
Chris@0 18
Chris@0 19 /**
Chris@0 20 * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes.
Chris@0 21 *
Chris@0 22 * @author Fabien Potencier <fabien@symfony.com>
Chris@0 23 * @author Tobias Schultze <http://tobion.de>
Chris@0 24 * @author Arnaud Le Blanc <arnaud.lb@gmail.com>
Chris@0 25 */
Chris@0 26 class PhpMatcherDumper extends MatcherDumper
Chris@0 27 {
Chris@0 28 private $expressionLanguage;
Chris@0 29
Chris@0 30 /**
Chris@0 31 * @var ExpressionFunctionProviderInterface[]
Chris@0 32 */
Chris@0 33 private $expressionLanguageProviders = array();
Chris@0 34
Chris@0 35 /**
Chris@0 36 * Dumps a set of routes to a PHP class.
Chris@0 37 *
Chris@0 38 * Available options:
Chris@0 39 *
Chris@0 40 * * class: The class name
Chris@0 41 * * base_class: The base class name
Chris@0 42 *
Chris@0 43 * @param array $options An array of options
Chris@0 44 *
Chris@0 45 * @return string A PHP class representing the matcher class
Chris@0 46 */
Chris@0 47 public function dump(array $options = array())
Chris@0 48 {
Chris@0 49 $options = array_replace(array(
Chris@0 50 'class' => 'ProjectUrlMatcher',
Chris@0 51 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
Chris@0 52 ), $options);
Chris@0 53
Chris@0 54 // trailing slash support is only enabled if we know how to redirect the user
Chris@0 55 $interfaces = class_implements($options['base_class']);
Chris@0 56 $supportsRedirections = isset($interfaces['Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface']);
Chris@0 57
Chris@0 58 return <<<EOF
Chris@0 59 <?php
Chris@0 60
Chris@0 61 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
Chris@0 62 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
Chris@0 63 use Symfony\Component\Routing\RequestContext;
Chris@0 64
Chris@0 65 /**
Chris@0 66 * {$options['class']}.
Chris@0 67 *
Chris@0 68 * This class has been auto-generated
Chris@0 69 * by the Symfony Routing Component.
Chris@0 70 */
Chris@0 71 class {$options['class']} extends {$options['base_class']}
Chris@0 72 {
Chris@0 73 /**
Chris@0 74 * Constructor.
Chris@0 75 */
Chris@0 76 public function __construct(RequestContext \$context)
Chris@0 77 {
Chris@0 78 \$this->context = \$context;
Chris@0 79 }
Chris@0 80
Chris@0 81 {$this->generateMatchMethod($supportsRedirections)}
Chris@0 82 }
Chris@0 83
Chris@0 84 EOF;
Chris@0 85 }
Chris@0 86
Chris@0 87 public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
Chris@0 88 {
Chris@0 89 $this->expressionLanguageProviders[] = $provider;
Chris@0 90 }
Chris@0 91
Chris@0 92 /**
Chris@0 93 * Generates the code for the match method implementing UrlMatcherInterface.
Chris@0 94 *
Chris@0 95 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 96 *
Chris@0 97 * @return string Match method as PHP code
Chris@0 98 */
Chris@0 99 private function generateMatchMethod($supportsRedirections)
Chris@0 100 {
Chris@0 101 $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
Chris@0 102
Chris@0 103 return <<<EOF
Chris@0 104 public function match(\$pathinfo)
Chris@0 105 {
Chris@0 106 \$allow = array();
Chris@0 107 \$pathinfo = rawurldecode(\$pathinfo);
Chris@0 108 \$context = \$this->context;
Chris@0 109 \$request = \$this->request;
Chris@0 110
Chris@0 111 $code
Chris@0 112
Chris@0 113 throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
Chris@0 114 }
Chris@0 115 EOF;
Chris@0 116 }
Chris@0 117
Chris@0 118 /**
Chris@0 119 * Generates PHP code to match a RouteCollection with all its routes.
Chris@0 120 *
Chris@0 121 * @param RouteCollection $routes A RouteCollection instance
Chris@0 122 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 123 *
Chris@0 124 * @return string PHP code
Chris@0 125 */
Chris@0 126 private function compileRoutes(RouteCollection $routes, $supportsRedirections)
Chris@0 127 {
Chris@0 128 $fetchedHost = false;
Chris@0 129
Chris@0 130 $groups = $this->groupRoutesByHostRegex($routes);
Chris@0 131 $code = '';
Chris@0 132
Chris@0 133 foreach ($groups as $collection) {
Chris@0 134 if (null !== $regex = $collection->getAttribute('host_regex')) {
Chris@0 135 if (!$fetchedHost) {
Chris@0 136 $code .= " \$host = \$this->context->getHost();\n\n";
Chris@0 137 $fetchedHost = true;
Chris@0 138 }
Chris@0 139
Chris@0 140 $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true));
Chris@0 141 }
Chris@0 142
Chris@0 143 $tree = $this->buildPrefixTree($collection);
Chris@0 144 $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections);
Chris@0 145
Chris@0 146 if (null !== $regex) {
Chris@0 147 // apply extra indention at each line (except empty ones)
Chris@0 148 $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode);
Chris@0 149 $code .= $groupCode;
Chris@0 150 $code .= " }\n\n";
Chris@0 151 } else {
Chris@0 152 $code .= $groupCode;
Chris@0 153 }
Chris@0 154 }
Chris@0 155
Chris@0 156 return $code;
Chris@0 157 }
Chris@0 158
Chris@0 159 /**
Chris@0 160 * Generates PHP code recursively to match a tree of routes.
Chris@0 161 *
Chris@0 162 * @param DumperPrefixCollection $collection A DumperPrefixCollection instance
Chris@0 163 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 164 * @param string $parentPrefix Prefix of the parent collection
Chris@0 165 *
Chris@0 166 * @return string PHP code
Chris@0 167 */
Chris@0 168 private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '')
Chris@0 169 {
Chris@0 170 $code = '';
Chris@0 171 $prefix = $collection->getPrefix();
Chris@0 172 $optimizable = 1 < strlen($prefix) && 1 < count($collection->all());
Chris@0 173 $optimizedPrefix = $parentPrefix;
Chris@0 174
Chris@0 175 if ($optimizable) {
Chris@0 176 $optimizedPrefix = $prefix;
Chris@0 177
Chris@0 178 $code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true));
Chris@0 179 }
Chris@0 180
Chris@0 181 foreach ($collection as $route) {
Chris@0 182 if ($route instanceof DumperCollection) {
Chris@0 183 $code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix);
Chris@0 184 } else {
Chris@0 185 $code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n";
Chris@0 186 }
Chris@0 187 }
Chris@0 188
Chris@0 189 if ($optimizable) {
Chris@0 190 $code .= " }\n\n";
Chris@0 191 // apply extra indention at each line (except empty ones)
Chris@0 192 $code = preg_replace('/^.{2,}$/m', ' $0', $code);
Chris@0 193 }
Chris@0 194
Chris@0 195 return $code;
Chris@0 196 }
Chris@0 197
Chris@0 198 /**
Chris@0 199 * Compiles a single Route to PHP code used to match it against the path info.
Chris@0 200 *
Chris@0 201 * @param Route $route A Route instance
Chris@0 202 * @param string $name The name of the Route
Chris@0 203 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 204 * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
Chris@0 205 *
Chris@0 206 * @return string PHP code
Chris@0 207 *
Chris@0 208 * @throws \LogicException
Chris@0 209 */
Chris@0 210 private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
Chris@0 211 {
Chris@0 212 $code = '';
Chris@0 213 $compiledRoute = $route->compile();
Chris@0 214 $conditions = array();
Chris@0 215 $hasTrailingSlash = false;
Chris@0 216 $matches = false;
Chris@0 217 $hostMatches = false;
Chris@0 218 $methods = $route->getMethods();
Chris@0 219
Chris@0 220 // GET and HEAD are equivalent
Chris@0 221 if (in_array('GET', $methods) && !in_array('HEAD', $methods)) {
Chris@0 222 $methods[] = 'HEAD';
Chris@0 223 }
Chris@0 224
Chris@0 225 $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods));
Chris@0 226 $regex = $compiledRoute->getRegex();
Chris@0 227
Chris@0 228 if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#'.(substr($regex, -1) === 'u' ? 'u' : ''), $regex, $m)) {
Chris@0 229 if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
Chris@0 230 $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
Chris@0 231 $hasTrailingSlash = true;
Chris@0 232 } else {
Chris@0 233 $conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true));
Chris@0 234 }
Chris@0 235 } else {
Chris@0 236 if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
Chris@0 237 $conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
Chris@0 238 }
Chris@0 239
Chris@0 240 if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
Chris@0 241 $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
Chris@0 242 $hasTrailingSlash = true;
Chris@0 243 }
Chris@0 244 $conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true));
Chris@0 245
Chris@0 246 $matches = true;
Chris@0 247 }
Chris@0 248
Chris@0 249 if ($compiledRoute->getHostVariables()) {
Chris@0 250 $hostMatches = true;
Chris@0 251 }
Chris@0 252
Chris@0 253 if ($route->getCondition()) {
Chris@0 254 $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), array('context', 'request'));
Chris@0 255 }
Chris@0 256
Chris@0 257 $conditions = implode(' && ', $conditions);
Chris@0 258
Chris@0 259 $code .= <<<EOF
Chris@0 260 // $name
Chris@0 261 if ($conditions) {
Chris@0 262
Chris@0 263 EOF;
Chris@0 264
Chris@0 265 $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
Chris@0 266 if ($methods) {
Chris@0 267 if (1 === count($methods)) {
Chris@0 268 $code .= <<<EOF
Chris@0 269 if (\$this->context->getMethod() != '$methods[0]') {
Chris@0 270 \$allow[] = '$methods[0]';
Chris@0 271 goto $gotoname;
Chris@0 272 }
Chris@0 273
Chris@0 274
Chris@0 275 EOF;
Chris@0 276 } else {
Chris@0 277 $methods = implode("', '", $methods);
Chris@0 278 $code .= <<<EOF
Chris@0 279 if (!in_array(\$this->context->getMethod(), array('$methods'))) {
Chris@0 280 \$allow = array_merge(\$allow, array('$methods'));
Chris@0 281 goto $gotoname;
Chris@0 282 }
Chris@0 283
Chris@0 284
Chris@0 285 EOF;
Chris@0 286 }
Chris@0 287 }
Chris@0 288
Chris@0 289 if ($hasTrailingSlash) {
Chris@0 290 $code .= <<<EOF
Chris@0 291 if (substr(\$pathinfo, -1) !== '/') {
Chris@0 292 return \$this->redirect(\$pathinfo.'/', '$name');
Chris@0 293 }
Chris@0 294
Chris@0 295
Chris@0 296 EOF;
Chris@0 297 }
Chris@0 298
Chris@0 299 if ($schemes = $route->getSchemes()) {
Chris@0 300 if (!$supportsRedirections) {
Chris@0 301 throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
Chris@0 302 }
Chris@0 303 $schemes = str_replace("\n", '', var_export(array_flip($schemes), true));
Chris@0 304 $code .= <<<EOF
Chris@0 305 \$requiredSchemes = $schemes;
Chris@0 306 if (!isset(\$requiredSchemes[\$this->context->getScheme()])) {
Chris@0 307 return \$this->redirect(\$pathinfo, '$name', key(\$requiredSchemes));
Chris@0 308 }
Chris@0 309
Chris@0 310
Chris@0 311 EOF;
Chris@0 312 }
Chris@0 313
Chris@0 314 // optimize parameters array
Chris@0 315 if ($matches || $hostMatches) {
Chris@0 316 $vars = array();
Chris@0 317 if ($hostMatches) {
Chris@0 318 $vars[] = '$hostMatches';
Chris@0 319 }
Chris@0 320 if ($matches) {
Chris@0 321 $vars[] = '$matches';
Chris@0 322 }
Chris@0 323 $vars[] = "array('_route' => '$name')";
Chris@0 324
Chris@0 325 $code .= sprintf(
Chris@0 326 " return \$this->mergeDefaults(array_replace(%s), %s);\n",
Chris@0 327 implode(', ', $vars),
Chris@0 328 str_replace("\n", '', var_export($route->getDefaults(), true))
Chris@0 329 );
Chris@0 330 } elseif ($route->getDefaults()) {
Chris@0 331 $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true)));
Chris@0 332 } else {
Chris@0 333 $code .= sprintf(" return array('_route' => '%s');\n", $name);
Chris@0 334 }
Chris@0 335 $code .= " }\n";
Chris@0 336
Chris@0 337 if ($methods) {
Chris@0 338 $code .= " $gotoname:\n";
Chris@0 339 }
Chris@0 340
Chris@0 341 return $code;
Chris@0 342 }
Chris@0 343
Chris@0 344 /**
Chris@0 345 * Groups consecutive routes having the same host regex.
Chris@0 346 *
Chris@0 347 * The result is a collection of collections of routes having the same host regex.
Chris@0 348 *
Chris@0 349 * @param RouteCollection $routes A flat RouteCollection
Chris@0 350 *
Chris@0 351 * @return DumperCollection A collection with routes grouped by host regex in sub-collections
Chris@0 352 */
Chris@0 353 private function groupRoutesByHostRegex(RouteCollection $routes)
Chris@0 354 {
Chris@0 355 $groups = new DumperCollection();
Chris@0 356
Chris@0 357 $currentGroup = new DumperCollection();
Chris@0 358 $currentGroup->setAttribute('host_regex', null);
Chris@0 359 $groups->add($currentGroup);
Chris@0 360
Chris@0 361 foreach ($routes as $name => $route) {
Chris@0 362 $hostRegex = $route->compile()->getHostRegex();
Chris@0 363 if ($currentGroup->getAttribute('host_regex') !== $hostRegex) {
Chris@0 364 $currentGroup = new DumperCollection();
Chris@0 365 $currentGroup->setAttribute('host_regex', $hostRegex);
Chris@0 366 $groups->add($currentGroup);
Chris@0 367 }
Chris@0 368 $currentGroup->add(new DumperRoute($name, $route));
Chris@0 369 }
Chris@0 370
Chris@0 371 return $groups;
Chris@0 372 }
Chris@0 373
Chris@0 374 /**
Chris@0 375 * Organizes the routes into a prefix tree.
Chris@0 376 *
Chris@0 377 * Routes order is preserved such that traversing the tree will traverse the
Chris@0 378 * routes in the origin order.
Chris@0 379 *
Chris@0 380 * @param DumperCollection $collection A collection of routes
Chris@0 381 *
Chris@0 382 * @return DumperPrefixCollection
Chris@0 383 */
Chris@0 384 private function buildPrefixTree(DumperCollection $collection)
Chris@0 385 {
Chris@0 386 $tree = new DumperPrefixCollection();
Chris@0 387 $current = $tree;
Chris@0 388
Chris@0 389 foreach ($collection as $route) {
Chris@0 390 $current = $current->addPrefixRoute($route);
Chris@0 391 }
Chris@0 392
Chris@0 393 $tree->mergeSlashNodes();
Chris@0 394
Chris@0 395 return $tree;
Chris@0 396 }
Chris@0 397
Chris@0 398 private function getExpressionLanguage()
Chris@0 399 {
Chris@0 400 if (null === $this->expressionLanguage) {
Chris@0 401 if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
Chris@0 402 throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
Chris@0 403 }
Chris@0 404 $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
Chris@0 405 }
Chris@0 406
Chris@0 407 return $this->expressionLanguage;
Chris@0 408 }
Chris@0 409 }