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 }
|