Completed
Branch development (b1b115)
by Johannes
10:28
created

PhpMatcherDumper::compileRoute()   F

Complexity

Conditions 32
Paths > 20000

Size

Total Lines 158

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 158
rs 0
c 0
b 0
f 0
cc 32
nc 44064
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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