Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 liveuser 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
 
227
        if (!count($compiledRoute->getPathVariables()) && false !== preg_match('#^(.)\^(?P<url>.*?)\$\1#', $compiledRoute->getRegex(), $m)) {
228
            if ($supportsTrailingSlash && substr($m['url'], -1) === '/') {
229
                $conditions[] = sprintf("rtrim(\$pathinfo, '/') === %s", var_export(rtrim(str_replace('\\', '', $m['url']), '/'), true));
230
                $hasTrailingSlash = true;
231
            } else {
232
                $conditions[] = sprintf('$pathinfo === %s', var_export(str_replace('\\', '', $m['url']), true));
233
            }
234
        } else {
235
            if ($compiledRoute->getStaticPrefix() && $compiledRoute->getStaticPrefix() !== $parentPrefix) {
236
                $conditions[] = sprintf('0 === strpos($pathinfo, %s)', var_export($compiledRoute->getStaticPrefix(), true));
237
            }
238
 
239
            $regex = $compiledRoute->getRegex();
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
}