annotate vendor/symfony/routing/Matcher/Dumper/PhpMatcherDumper.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
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@17 14 use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
Chris@17 15 use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
Chris@0 16 use Symfony\Component\Routing\Route;
Chris@0 17 use Symfony\Component\Routing\RouteCollection;
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@17 33 private $expressionLanguageProviders = [];
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@17 47 public function dump(array $options = [])
Chris@0 48 {
Chris@17 49 $options = array_replace([
Chris@0 50 'class' => 'ProjectUrlMatcher',
Chris@0 51 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher',
Chris@17 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 * This class has been auto-generated
Chris@0 67 * by the Symfony Routing Component.
Chris@0 68 */
Chris@0 69 class {$options['class']} extends {$options['base_class']}
Chris@0 70 {
Chris@0 71 public function __construct(RequestContext \$context)
Chris@0 72 {
Chris@0 73 \$this->context = \$context;
Chris@0 74 }
Chris@0 75
Chris@0 76 {$this->generateMatchMethod($supportsRedirections)}
Chris@0 77 }
Chris@0 78
Chris@0 79 EOF;
Chris@0 80 }
Chris@0 81
Chris@0 82 public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
Chris@0 83 {
Chris@0 84 $this->expressionLanguageProviders[] = $provider;
Chris@0 85 }
Chris@0 86
Chris@0 87 /**
Chris@0 88 * Generates the code for the match method implementing UrlMatcherInterface.
Chris@0 89 *
Chris@0 90 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 91 *
Chris@0 92 * @return string Match method as PHP code
Chris@0 93 */
Chris@0 94 private function generateMatchMethod($supportsRedirections)
Chris@0 95 {
Chris@0 96 $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n");
Chris@0 97
Chris@0 98 return <<<EOF
Chris@14 99 public function match(\$rawPathinfo)
Chris@0 100 {
Chris@17 101 \$allow = [];
Chris@14 102 \$pathinfo = rawurldecode(\$rawPathinfo);
Chris@14 103 \$trimmedPathinfo = rtrim(\$pathinfo, '/');
Chris@0 104 \$context = \$this->context;
Chris@14 105 \$request = \$this->request ?: \$this->createRequest(\$pathinfo);
Chris@14 106 \$requestMethod = \$canonicalMethod = \$context->getMethod();
Chris@14 107
Chris@14 108 if ('HEAD' === \$requestMethod) {
Chris@14 109 \$canonicalMethod = 'GET';
Chris@14 110 }
Chris@0 111
Chris@0 112 $code
Chris@0 113
Chris@0 114 throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException();
Chris@0 115 }
Chris@0 116 EOF;
Chris@0 117 }
Chris@0 118
Chris@0 119 /**
Chris@0 120 * Generates PHP code to match a RouteCollection with all its routes.
Chris@0 121 *
Chris@0 122 * @param RouteCollection $routes A RouteCollection instance
Chris@0 123 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 124 *
Chris@0 125 * @return string PHP code
Chris@0 126 */
Chris@0 127 private function compileRoutes(RouteCollection $routes, $supportsRedirections)
Chris@0 128 {
Chris@0 129 $fetchedHost = false;
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@14 136 $code .= " \$host = \$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@14 143 $tree = $this->buildStaticPrefixCollection($collection);
Chris@14 144 $groupCode = $this->compileStaticPrefixRoutes($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@14 156 // used to display the Welcome Page in apps that don't define a homepage
Chris@14 157 $code .= " if ('/' === \$pathinfo && !\$allow) {\n";
Chris@14 158 $code .= " throw new Symfony\Component\Routing\Exception\NoConfigurationException();\n";
Chris@14 159 $code .= " }\n";
Chris@14 160
Chris@0 161 return $code;
Chris@0 162 }
Chris@0 163
Chris@14 164 private function buildStaticPrefixCollection(DumperCollection $collection)
Chris@14 165 {
Chris@14 166 $prefixCollection = new StaticPrefixCollection();
Chris@14 167
Chris@14 168 foreach ($collection as $dumperRoute) {
Chris@14 169 $prefix = $dumperRoute->getRoute()->compile()->getStaticPrefix();
Chris@14 170 $prefixCollection->addRoute($prefix, $dumperRoute);
Chris@14 171 }
Chris@14 172
Chris@14 173 $prefixCollection->optimizeGroups();
Chris@14 174
Chris@14 175 return $prefixCollection;
Chris@14 176 }
Chris@14 177
Chris@0 178 /**
Chris@14 179 * Generates PHP code to match a tree of routes.
Chris@0 180 *
Chris@14 181 * @param StaticPrefixCollection $collection A StaticPrefixCollection instance
Chris@0 182 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@14 183 * @param string $ifOrElseIf either "if" or "elseif" to influence chaining
Chris@0 184 *
Chris@0 185 * @return string PHP code
Chris@0 186 */
Chris@14 187 private function compileStaticPrefixRoutes(StaticPrefixCollection $collection, $supportsRedirections, $ifOrElseIf = 'if')
Chris@0 188 {
Chris@0 189 $code = '';
Chris@0 190 $prefix = $collection->getPrefix();
Chris@0 191
Chris@14 192 if (!empty($prefix) && '/' !== $prefix) {
Chris@14 193 $code .= sprintf(" %s (0 === strpos(\$pathinfo, %s)) {\n", $ifOrElseIf, var_export($prefix, true));
Chris@0 194 }
Chris@0 195
Chris@14 196 $ifOrElseIf = 'if';
Chris@14 197
Chris@14 198 foreach ($collection->getItems() as $route) {
Chris@14 199 if ($route instanceof StaticPrefixCollection) {
Chris@14 200 $code .= $this->compileStaticPrefixRoutes($route, $supportsRedirections, $ifOrElseIf);
Chris@14 201 $ifOrElseIf = 'elseif';
Chris@0 202 } else {
Chris@14 203 $code .= $this->compileRoute($route[1]->getRoute(), $route[1]->getName(), $supportsRedirections, $prefix)."\n";
Chris@14 204 $ifOrElseIf = 'if';
Chris@0 205 }
Chris@0 206 }
Chris@0 207
Chris@14 208 if (!empty($prefix) && '/' !== $prefix) {
Chris@0 209 $code .= " }\n\n";
Chris@0 210 // apply extra indention at each line (except empty ones)
Chris@0 211 $code = preg_replace('/^.{2,}$/m', ' $0', $code);
Chris@0 212 }
Chris@0 213
Chris@0 214 return $code;
Chris@0 215 }
Chris@0 216
Chris@0 217 /**
Chris@0 218 * Compiles a single Route to PHP code used to match it against the path info.
Chris@0 219 *
Chris@0 220 * @param Route $route A Route instance
Chris@0 221 * @param string $name The name of the Route
Chris@0 222 * @param bool $supportsRedirections Whether redirections are supported by the base class
Chris@0 223 * @param string|null $parentPrefix The prefix of the parent collection used to optimize the code
Chris@0 224 *
Chris@0 225 * @return string PHP code
Chris@0 226 *
Chris@0 227 * @throws \LogicException
Chris@0 228 */
Chris@0 229 private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null)
Chris@0 230 {
Chris@0 231 $code = '';
Chris@0 232 $compiledRoute = $route->compile();
Chris@17 233 $conditions = [];
Chris@0 234 $hasTrailingSlash = false;
Chris@0 235 $matches = false;
Chris@0 236 $hostMatches = false;
Chris@0 237 $methods = $route->getMethods();
Chris@0 238
Chris@17 239 $supportsTrailingSlash = $supportsRedirections && (!$methods || \in_array('GET', $methods));
Chris@0 240 $regex = $compiledRoute->getRegex();
Chris@0 241
Chris@17 242 if (!\count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#'.('u' === substr($regex, -1) ? 'u' : ''), $regex, $m)) {
Chris@14 243 if ($supportsTrailingSlash && '/' === substr($m['url'], -1)) {
Chris@14 244 $conditions[] = sprintf('%s === $trimmedPathinfo', var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
Chris@0 245 $hasTrailingSlash = true;
Chris@0 246 } else {
Chris@14 247 $conditions[] = sprintf('%s === $pathinfo', var_export(str_replace('\\', '', $m['url']), true));
Chris@0 248 }
Chris@0 249 } else {
Chris@0 250 if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
Chris@0 251 $conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
Chris@0 252 }
Chris@0 253
Chris@0 254 if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) {
Chris@0 255 $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
Chris@0 256 $hasTrailingSlash = true;
Chris@0 257 }
Chris@0 258 $conditions[] = sprintf('preg_match(%s, $pathinfo, $matches)', var_export($regex, true));
Chris@0 259
Chris@0 260 $matches = true;
Chris@0 261 }
Chris@0 262
Chris@0 263 if ($compiledRoute->getHostVariables()) {
Chris@0 264 $hostMatches = true;
Chris@0 265 }
Chris@0 266
Chris@0 267 if ($route->getCondition()) {
Chris@17 268 $conditions[] = $this->getExpressionLanguage()->compile($route->getCondition(), ['context', 'request']);
Chris@0 269 }
Chris@0 270
Chris@0 271 $conditions = implode(' && ', $conditions);
Chris@0 272
Chris@0 273 $code .= <<<EOF
Chris@0 274 // $name
Chris@0 275 if ($conditions) {
Chris@0 276
Chris@0 277 EOF;
Chris@0 278
Chris@0 279 $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name);
Chris@0 280
Chris@14 281 // the offset where the return value is appended below, with indendation
Chris@17 282 $retOffset = 12 + \strlen($code);
Chris@0 283
Chris@0 284 // optimize parameters array
Chris@0 285 if ($matches || $hostMatches) {
Chris@17 286 $vars = [];
Chris@0 287 if ($hostMatches) {
Chris@0 288 $vars[] = '$hostMatches';
Chris@0 289 }
Chris@0 290 if ($matches) {
Chris@0 291 $vars[] = '$matches';
Chris@0 292 }
Chris@17 293 $vars[] = "['_route' => '$name']";
Chris@0 294
Chris@0 295 $code .= sprintf(
Chris@14 296 " \$ret = \$this->mergeDefaults(array_replace(%s), %s);\n",
Chris@0 297 implode(', ', $vars),
Chris@0 298 str_replace("\n", '', var_export($route->getDefaults(), true))
Chris@0 299 );
Chris@0 300 } elseif ($route->getDefaults()) {
Chris@17 301 $code .= sprintf(" \$ret = %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), ['_route' => $name]), true)));
Chris@0 302 } else {
Chris@17 303 $code .= sprintf(" \$ret = ['_route' => '%s'];\n", $name);
Chris@14 304 }
Chris@14 305
Chris@14 306 if ($hasTrailingSlash) {
Chris@14 307 $code .= <<<EOF
Chris@14 308 if ('/' === substr(\$pathinfo, -1)) {
Chris@14 309 // no-op
Chris@14 310 } elseif ('GET' !== \$canonicalMethod) {
Chris@14 311 goto $gotoname;
Chris@14 312 } else {
Chris@14 313 return array_replace(\$ret, \$this->redirect(\$rawPathinfo.'/', '$name'));
Chris@14 314 }
Chris@14 315
Chris@14 316
Chris@14 317 EOF;
Chris@14 318 }
Chris@14 319
Chris@14 320 if ($methods) {
Chris@17 321 $methodVariable = \in_array('GET', $methods) ? '$canonicalMethod' : '$requestMethod';
Chris@14 322 $methods = implode("', '", $methods);
Chris@14 323 }
Chris@14 324
Chris@14 325 if ($schemes = $route->getSchemes()) {
Chris@14 326 if (!$supportsRedirections) {
Chris@14 327 throw new \LogicException('The "schemes" requirement is only supported for URL matchers that implement RedirectableUrlMatcherInterface.');
Chris@14 328 }
Chris@14 329 $schemes = str_replace("\n", '', var_export(array_flip($schemes), true));
Chris@14 330 if ($methods) {
Chris@14 331 $code .= <<<EOF
Chris@14 332 \$requiredSchemes = $schemes;
Chris@14 333 \$hasRequiredScheme = isset(\$requiredSchemes[\$context->getScheme()]);
Chris@17 334 if (!in_array($methodVariable, ['$methods'])) {
Chris@14 335 if (\$hasRequiredScheme) {
Chris@17 336 \$allow = array_merge(\$allow, ['$methods']);
Chris@14 337 }
Chris@14 338 goto $gotoname;
Chris@14 339 }
Chris@14 340 if (!\$hasRequiredScheme) {
Chris@14 341 if ('GET' !== \$canonicalMethod) {
Chris@14 342 goto $gotoname;
Chris@14 343 }
Chris@14 344
Chris@14 345 return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)));
Chris@14 346 }
Chris@14 347
Chris@14 348
Chris@14 349 EOF;
Chris@14 350 } else {
Chris@14 351 $code .= <<<EOF
Chris@14 352 \$requiredSchemes = $schemes;
Chris@14 353 if (!isset(\$requiredSchemes[\$context->getScheme()])) {
Chris@14 354 if ('GET' !== \$canonicalMethod) {
Chris@14 355 goto $gotoname;
Chris@14 356 }
Chris@14 357
Chris@14 358 return array_replace(\$ret, \$this->redirect(\$rawPathinfo, '$name', key(\$requiredSchemes)));
Chris@14 359 }
Chris@14 360
Chris@14 361
Chris@14 362 EOF;
Chris@14 363 }
Chris@14 364 } elseif ($methods) {
Chris@14 365 $code .= <<<EOF
Chris@17 366 if (!in_array($methodVariable, ['$methods'])) {
Chris@17 367 \$allow = array_merge(\$allow, ['$methods']);
Chris@14 368 goto $gotoname;
Chris@14 369 }
Chris@14 370
Chris@14 371
Chris@14 372 EOF;
Chris@14 373 }
Chris@14 374
Chris@14 375 if ($hasTrailingSlash || $schemes || $methods) {
Chris@14 376 $code .= " return \$ret;\n";
Chris@14 377 } else {
Chris@14 378 $code = substr_replace($code, 'return', $retOffset, 6);
Chris@0 379 }
Chris@0 380 $code .= " }\n";
Chris@0 381
Chris@14 382 if ($hasTrailingSlash || $schemes || $methods) {
Chris@0 383 $code .= " $gotoname:\n";
Chris@0 384 }
Chris@0 385
Chris@0 386 return $code;
Chris@0 387 }
Chris@0 388
Chris@0 389 /**
Chris@0 390 * Groups consecutive routes having the same host regex.
Chris@0 391 *
Chris@0 392 * The result is a collection of collections of routes having the same host regex.
Chris@0 393 *
Chris@0 394 * @param RouteCollection $routes A flat RouteCollection
Chris@0 395 *
Chris@0 396 * @return DumperCollection A collection with routes grouped by host regex in sub-collections
Chris@0 397 */
Chris@0 398 private function groupRoutesByHostRegex(RouteCollection $routes)
Chris@0 399 {
Chris@0 400 $groups = new DumperCollection();
Chris@0 401 $currentGroup = new DumperCollection();
Chris@0 402 $currentGroup->setAttribute('host_regex', null);
Chris@0 403 $groups->add($currentGroup);
Chris@0 404
Chris@0 405 foreach ($routes as $name => $route) {
Chris@0 406 $hostRegex = $route->compile()->getHostRegex();
Chris@0 407 if ($currentGroup->getAttribute('host_regex') !== $hostRegex) {
Chris@0 408 $currentGroup = new DumperCollection();
Chris@0 409 $currentGroup->setAttribute('host_regex', $hostRegex);
Chris@0 410 $groups->add($currentGroup);
Chris@0 411 }
Chris@0 412 $currentGroup->add(new DumperRoute($name, $route));
Chris@0 413 }
Chris@0 414
Chris@0 415 return $groups;
Chris@0 416 }
Chris@0 417
Chris@0 418 private function getExpressionLanguage()
Chris@0 419 {
Chris@0 420 if (null === $this->expressionLanguage) {
Chris@0 421 if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
Chris@0 422 throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
Chris@0 423 }
Chris@0 424 $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders);
Chris@0 425 }
Chris@0 426
Chris@0 427 return $this->expressionLanguage;
Chris@0 428 }
Chris@0 429 }