]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
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 | ||
17 | /** | |
18 | * PhpMatcherDumper creates a PHP class able to match URLs for a given set of routes. | |
19 | * | |
20 | * @author Fabien Potencier <fabien@symfony.com> | |
21 | * @author Tobias Schultze <http://tobion.de> | |
22 | * @author Arnaud Le Blanc <arnaud.lb@gmail.com> | |
23 | */ | |
24 | class PhpMatcherDumper extends MatcherDumper | |
25 | { | |
26 | /** | |
27 | * Dumps a set of routes to a PHP class. | |
28 | * | |
29 | * Available options: | |
30 | * | |
31 | * * class: The class name | |
32 | * * base_class: The base class name | |
33 | * | |
34 | * @param array $options An array of options | |
35 | * | |
36 | * @return string A PHP class representing the matcher class | |
37 | */ | |
38 | public function dump(array $options = array()) | |
39 | { | |
40 | $options = array_replace(array( | |
41 | 'class' => 'ProjectUrlMatcher', | |
42 | 'base_class' => 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher', | |
43 | ), $options); | |
44 | ||
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']); | |
48 | ||
49 | return <<<EOF | |
50 | <?php | |
51 | ||
52 | use Symfony\Component\Routing\Exception\MethodNotAllowedException; | |
53 | use Symfony\Component\Routing\Exception\ResourceNotFoundException; | |
54 | use Symfony\Component\Routing\RequestContext; | |
55 | ||
56 | /** | |
57 | * {$options['class']} | |
58 | * | |
59 | * This class has been auto-generated | |
60 | * by the Symfony Routing Component. | |
61 | */ | |
62 | class {$options['class']} extends {$options['base_class']} | |
63 | { | |
64 | /** | |
65 | * Constructor. | |
66 | */ | |
67 | public function __construct(RequestContext \$context) | |
68 | { | |
69 | \$this->context = \$context; | |
70 | } | |
71 | ||
72 | {$this->generateMatchMethod($supportsRedirections)} | |
73 | } | |
74 | ||
75 | EOF; | |
76 | } | |
77 | ||
78 | /** | |
79 | * Generates the code for the match method implementing UrlMatcherInterface. | |
80 | * | |
81 | * @param Boolean $supportsRedirections Whether redirections are supported by the base class | |
82 | * | |
83 | * @return string Match method as PHP code | |
84 | */ | |
85 | private function generateMatchMethod($supportsRedirections) | |
86 | { | |
87 | $code = rtrim($this->compileRoutes($this->getRoutes(), $supportsRedirections), "\n"); | |
88 | ||
89 | return <<<EOF | |
90 | public function match(\$pathinfo) | |
91 | { | |
92 | \$allow = array(); | |
93 | \$pathinfo = rawurldecode(\$pathinfo); | |
94 | ||
95 | $code | |
96 | ||
97 | throw 0 < count(\$allow) ? new MethodNotAllowedException(array_unique(\$allow)) : new ResourceNotFoundException(); | |
98 | } | |
99 | EOF; | |
100 | } | |
101 | ||
102 | /** | |
103 | * Generates PHP code to match a RouteCollection with all its routes. | |
104 | * | |
105 | * @param RouteCollection $routes A RouteCollection instance | |
106 | * @param Boolean $supportsRedirections Whether redirections are supported by the base class | |
107 | * | |
108 | * @return string PHP code | |
109 | */ | |
110 | private function compileRoutes(RouteCollection $routes, $supportsRedirections) | |
111 | { | |
112 | $fetchedHost = false; | |
113 | ||
114 | $groups = $this->groupRoutesByHostRegex($routes); | |
115 | $code = ''; | |
116 | ||
117 | foreach ($groups as $collection) { | |
118 | if (null !== $regex = $collection->getAttribute('host_regex')) { | |
119 | if (!$fetchedHost) { | |
120 | $code .= " \$host = \$this->context->getHost();\n\n"; | |
121 | $fetchedHost = true; | |
122 | } | |
123 | ||
124 | $code .= sprintf(" if (preg_match(%s, \$host, \$hostMatches)) {\n", var_export($regex, true)); | |
125 | } | |
126 | ||
127 | $tree = $this->buildPrefixTree($collection); | |
128 | $groupCode = $this->compilePrefixRoutes($tree, $supportsRedirections); | |
129 | ||
130 | if (null !== $regex) { | |
131 | // apply extra indention at each line (except empty ones) | |
132 | $groupCode = preg_replace('/^.{2,}$/m', ' $0', $groupCode); | |
133 | $code .= $groupCode; | |
134 | $code .= " }\n\n"; | |
135 | } else { | |
136 | $code .= $groupCode; | |
137 | } | |
138 | } | |
139 | ||
140 | return $code; | |
141 | } | |
142 | ||
143 | /** | |
144 | * Generates PHP code recursively to match a tree of routes | |
145 | * | |
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 | |
149 | * | |
150 | * @return string PHP code | |
151 | */ | |
152 | private function compilePrefixRoutes(DumperPrefixCollection $collection, $supportsRedirections, $parentPrefix = '') | |
153 | { | |
154 | $code = ''; | |
155 | $prefix = $collection->getPrefix(); | |
156 | $optimizable = 1 < strlen($prefix) && 1 < count($collection->all()); | |
157 | $optimizedPrefix = $parentPrefix; | |
158 | ||
159 | if ($optimizable) { | |
160 | $optimizedPrefix = $prefix; | |
161 | ||
162 | $code .= sprintf(" if (0 === strpos(\$pathinfo, %s)) {\n", var_export($prefix, true)); | |
163 | } | |
164 | ||
165 | foreach ($collection as $route) { | |
166 | if ($route instanceof DumperCollection) { | |
167 | $code .= $this->compilePrefixRoutes($route, $supportsRedirections, $optimizedPrefix); | |
168 | } else { | |
169 | $code .= $this->compileRoute($route->getRoute(), $route->getName(), $supportsRedirections, $optimizedPrefix)."\n"; | |
170 | } | |
171 | } | |
172 | ||
173 | if ($optimizable) { | |
174 | $code .= " }\n\n"; | |
175 | // apply extra indention at each line (except empty ones) | |
176 | $code = preg_replace('/^.{2,}$/m', ' $0', $code); | |
177 | } | |
178 | ||
179 | return $code; | |
180 | } | |
181 | ||
182 | /** | |
183 | * Compiles a single Route to PHP code used to match it against the path info. | |
184 | * | |
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 | |
189 | * | |
190 | * @return string PHP code | |
191 | * | |
192 | * @throws \LogicException | |
193 | */ | |
194 | private function compileRoute(Route $route, $name, $supportsRedirections, $parentPrefix = null) | |
195 | { | |
196 | $code = ''; | |
197 | $compiledRoute = $route->compile(); | |
198 | $conditions = array(); | |
199 | $hasTrailingSlash = false; | |
200 | $matches = false; | |
201 | $hostMatches = false; | |
202 | $methods = array(); | |
203 | ||
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)) { | |
208 | $methods[] = 'HEAD'; | |
209 | } | |
210 | } | |
211 | ||
212 | $supportsTrailingSlash = $supportsRedirections && (!$methods || in_array('HEAD', $methods)); | |
213 | ||
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; | |
218 | } else { | |
219 | $conditions[] = sprintf("\$pathinfo === %s", var_export(str_replace('\\', '', $m['url']), true)); | |
220 | } | |
221 | } else { | |
222 | if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) { | |
223 | $conditions[] = sprintf("0 === strpos(\$pathinfo, %s)", var_export($compiledRoute->getStaticPrefix(), true)); | |
224 | } | |
225 | ||
226 | $regex = $compiledRoute->getRegex(); | |
227 | if ($supportsTrailingSlash && $pos = strpos($regex, '/$')) { | |
228 | $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2); | |
229 | $hasTrailingSlash = true; | |
230 | } | |
231 | $conditions[] = sprintf("preg_match(%s, \$pathinfo, \$matches)", var_export($regex, true)); | |
232 | ||
233 | $matches = true; | |
234 | } | |
235 | ||
236 | if ($compiledRoute->getHostVariables()) { | |
237 | $hostMatches = true; | |
238 | } | |
239 | ||
240 | $conditions = implode(' && ', $conditions); | |
241 | ||
242 | $code .= <<<EOF | |
243 | // $name | |
244 | if ($conditions) { | |
245 | ||
246 | EOF; | |
247 | ||
248 | if ($methods) { | |
249 | $gotoname = 'not_'.preg_replace('/[^A-Za-z0-9_]/', '', $name); | |
250 | ||
251 | if (1 === count($methods)) { | |
252 | $code .= <<<EOF | |
253 | if (\$this->context->getMethod() != '$methods[0]') { | |
254 | \$allow[] = '$methods[0]'; | |
255 | goto $gotoname; | |
256 | } | |
257 | ||
258 | ||
259 | EOF; | |
260 | } else { | |
261 | $methods = implode("', '", $methods); | |
262 | $code .= <<<EOF | |
263 | if (!in_array(\$this->context->getMethod(), array('$methods'))) { | |
264 | \$allow = array_merge(\$allow, array('$methods')); | |
265 | goto $gotoname; | |
266 | } | |
267 | ||
268 | ||
269 | EOF; | |
270 | } | |
271 | } | |
272 | ||
273 | if ($hasTrailingSlash) { | |
274 | $code .= <<<EOF | |
275 | if (substr(\$pathinfo, -1) !== '/') { | |
276 | return \$this->redirect(\$pathinfo.'/', '$name'); | |
277 | } | |
278 | ||
279 | ||
280 | EOF; | |
281 | } | |
282 | ||
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.'); | |
286 | } | |
287 | ||
288 | $code .= <<<EOF | |
289 | if (\$this->context->getScheme() !== '$scheme') { | |
290 | return \$this->redirect(\$pathinfo, '$name', '$scheme'); | |
291 | } | |
292 | ||
293 | ||
294 | EOF; | |
295 | } | |
296 | ||
297 | // optimize parameters array | |
298 | if ($matches || $hostMatches) { | |
299 | $vars = array(); | |
300 | if ($hostMatches) { | |
301 | $vars[] = '$hostMatches'; | |
302 | } | |
303 | if ($matches) { | |
304 | $vars[] = '$matches'; | |
305 | } | |
306 | $vars[] = "array('_route' => '$name')"; | |
307 | ||
308 | $code .= sprintf(" return \$this->mergeDefaults(array_replace(%s), %s);\n" | |
309 | , implode(', ', $vars), str_replace("\n", '', var_export($route->getDefaults(), true))); | |
310 | ||
311 | } elseif ($route->getDefaults()) { | |
312 | $code .= sprintf(" return %s;\n", str_replace("\n", '', var_export(array_replace($route->getDefaults(), array('_route' => $name)), true))); | |
313 | } else { | |
314 | $code .= sprintf(" return array('_route' => '%s');\n", $name); | |
315 | } | |
316 | $code .= " }\n"; | |
317 | ||
318 | if ($methods) { | |
319 | $code .= " $gotoname:\n"; | |
320 | } | |
321 | ||
322 | return $code; | |
323 | } | |
324 | ||
325 | /** | |
326 | * Groups consecutive routes having the same host regex. | |
327 | * | |
328 | * The result is a collection of collections of routes having the same host regex. | |
329 | * | |
330 | * @param RouteCollection $routes A flat RouteCollection | |
331 | * | |
332 | * @return DumperCollection A collection with routes grouped by host regex in sub-collections | |
333 | */ | |
334 | private function groupRoutesByHostRegex(RouteCollection $routes) | |
335 | { | |
336 | $groups = new DumperCollection(); | |
337 | ||
338 | $currentGroup = new DumperCollection(); | |
339 | $currentGroup->setAttribute('host_regex', null); | |
340 | $groups->add($currentGroup); | |
341 | ||
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); | |
348 | } | |
349 | $currentGroup->add(new DumperRoute($name, $route)); | |
350 | } | |
351 | ||
352 | return $groups; | |
353 | } | |
354 | ||
355 | /** | |
356 | * Organizes the routes into a prefix tree. | |
357 | * | |
358 | * Routes order is preserved such that traversing the tree will traverse the | |
359 | * routes in the origin order. | |
360 | * | |
361 | * @param DumperCollection $collection A collection of routes | |
362 | * | |
363 | * @return DumperPrefixCollection | |
364 | */ | |
365 | private function buildPrefixTree(DumperCollection $collection) | |
366 | { | |
367 | $tree = new DumperPrefixCollection(); | |
368 | $current = $tree; | |
369 | ||
370 | foreach ($collection as $route) { | |
371 | $current = $current->addPrefixRoute($route); | |
372 | } | |
373 | ||
374 | $tree->mergeSlashNodes(); | |
375 | ||
376 | return $tree; | |
377 | } | |
378 | } |