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