Completed
Push — master ( ce3f18...1319dc )
by Christian
05:54
created

RestActionReader::readRouteAnnotation()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3
Metric Value
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
crap 3
1
<?php
2
3
/*
4
 * This file is part of the FOSRestBundle package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\RestBundle\Routing\Loader\Reader;
13
14
use Doctrine\Common\Annotations\Reader;
15
use FOS\RestBundle\Controller\Annotations\Route as RouteAnnotation;
16
use FOS\RestBundle\Inflector\InflectorInterface;
17
use FOS\RestBundle\Request\ParamReader;
18
use FOS\RestBundle\Routing\RestRouteCollection;
19
use Symfony\Component\Routing\Route;
20
use FOS\RestBundle\Request\ParamReaderInterface;
21
22
/**
23
 * REST controller actions reader.
24
 *
25
 * @author Konstantin Kudryashov <[email protected]>
26
 */
27
class RestActionReader
28
{
29
    const COLLECTION_ROUTE_PREFIX = 'c';
30
31
    private $annotationReader;
32
    private $paramReader;
33
    private $inflector;
34
    private $formats;
35
    private $includeFormat;
36
    private $routePrefix;
37
    private $namePrefix;
38
    private $versions;
39
    private $pluralize;
40
    private $parents = [];
41
    private $availableHTTPMethods = ['get', 'post', 'put', 'patch', 'delete', 'link', 'unlink', 'head', 'options'];
42
    private $availableConventionalActions = ['new', 'edit', 'remove'];
43
44
    /**
45
     * Initializes controller reader.
46
     *
47
     * @param Reader               $annotationReader
48
     * @param ParamReaderInterface $paramReader
49
     * @param InflectorInterface   $inflector
50
     * @param bool                 $includeFormat
51
     * @param array                $formats
52
     */
53 40
    public function __construct(Reader $annotationReader, ParamReaderInterface $paramReader, InflectorInterface $inflector, $includeFormat, array $formats = [])
54
    {
55 40
        $this->annotationReader = $annotationReader;
56 40
        $this->paramReader = $paramReader;
57 40
        $this->inflector = $inflector;
58 40
        $this->includeFormat = $includeFormat;
59 40
        $this->formats = $formats;
60 40
    }
61
62
    /**
63
     * Sets routes prefix.
64
     *
65
     * @param string $prefix Routes prefix
66
     */
67 30
    public function setRoutePrefix($prefix = null)
68
    {
69 30
        $this->routePrefix = $prefix;
70 30
    }
71
72
    /**
73
     * Returns route prefix.
74
     *
75
     * @return string
76
     */
77 30
    public function getRoutePrefix()
78
    {
79 30
        return $this->routePrefix;
80
    }
81
82
    /**
83
     * Sets route names prefix.
84
     *
85
     * @param string $prefix Route names prefix
86
     */
87 30
    public function setNamePrefix($prefix = null)
88
    {
89 30
        $this->namePrefix = $prefix;
90 30
    }
91
92
    /**
93
     * Returns name prefix.
94
     *
95
     * @return string
96
     */
97
    public function getNamePrefix()
98
    {
99
        return $this->namePrefix;
100
    }
101
102
    /**
103
     * Sets route names versions.
104
     *
105
     * @param array|string|null $versions Route names versions
106
     */
107 30
    public function setVersions($versions = null)
108
    {
109 30
        $this->versions = (array) $versions;
110 30
    }
111
112
    /**
113
     * Returns versions.
114
     *
115
     * @return array|null
116
     */
117
    public function getVersions()
118
    {
119
        return $this->versions;
120
    }
121
122
    /**
123
     * Sets pluralize.
124
     *
125
     * @param bool|null $pluralize Specify if resource name must be pluralized
126
     */
127 30
    public function setPluralize($pluralize)
128
    {
129 30
        $this->pluralize = $pluralize;
130 30
    }
131
132
    /**
133
     * Returns pluralize.
134
     *
135
     * @return bool|null
136
     */
137
    public function getPluralize()
138
    {
139
        return $this->pluralize;
140
    }
141
142
    /**
143
     * Set parent routes.
144
     *
145
     * @param array $parents Array of parent resources names
146
     */
147 30
    public function setParents(array $parents)
148
    {
149 30
        $this->parents = $parents;
150 30
    }
151
152
    /**
153
     * Returns parents.
154
     *
155
     * @return array
156
     */
157
    public function getParents()
158
    {
159
        return $this->parents;
160
    }
161
162
    /**
163
     * Reads action route.
164
     *
165
     * @param RestRouteCollection $collection
166
     * @param \ReflectionMethod   $method
167
     * @param string[]            $resource
168
     *
169
     * @throws \InvalidArgumentException
170
     *
171
     * @return Route
172
     */
173 30
    public function read(RestRouteCollection $collection, \ReflectionMethod $method, $resource)
174
    {
175
        // check that every route parent has non-empty singular name
176 30
        foreach ($this->parents as $parent) {
177 5
            if (empty($parent) || '/' === substr($parent, -1)) {
178
                throw new \InvalidArgumentException(
179
                    "Every parent controller must have `get{SINGULAR}Action(\$id)` method\n".
180
                    'where {SINGULAR} is a singular form of associated object'
181
                );
182
            }
183 30
        }
184
185
        // if method is not readable - skip
186 30
        if (!$this->isMethodReadable($method)) {
187 14
            return;
188
        }
189
190
        // if we can't get http-method and resources from method name - skip
191 30
        $httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method, $resource);
192 30
        if (!$httpMethodAndResources) {
193 30
            return;
194
        }
195
196 30
        list($httpMethod, $resources, $isCollection, $isInflectable) = $httpMethodAndResources;
197 30
        $arguments = $this->getMethodArguments($method);
198
199
        // if we have only 1 resource & 1 argument passed, then it's object call, so
200
        // we can set collection singular name
201 30
        if (1 === count($resources) && 1 === count($arguments) - count($this->parents)) {
202 18
            $collection->setSingularName($resources[0]);
203 18
        }
204
205
        // if we have parents passed - merge them with own resource names
206 30
        if (count($this->parents)) {
207 5
            $resources = array_merge($this->parents, $resources);
208 5
        }
209
210 30
        if (empty($resources)) {
211
            $resources[] = null;
212
        }
213
214 30
        $routeName = $httpMethod.$this->generateRouteName($resources);
215 30
        $urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod);
216
217
        // if passed method is not valid HTTP method then it's either
218
        // a hypertext driver, a custom object (PUT) or collection (GET)
219
        // method
220 30
        if (!in_array($httpMethod, $this->availableHTTPMethods)) {
221 17
            $urlParts[] = $httpMethod;
222 17
            $httpMethod = $this->getCustomHttpMethod($httpMethod, $resources, $arguments);
223 17
        }
224
225
        // generated parameters
226 30
        $routeName = strtolower($routeName);
227 30
        $path = implode('/', $urlParts);
228 30
        $defaults = ['_controller' => $method->getName()];
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
229 30
        $requirements = [];
230 30
        $options = [];
231 30
        $host = '';
232 30
        $condition = null;
233
234 30
        $annotations = $this->readRouteAnnotation($method);
235 30
        if (!empty($annotations)) {
236 8
            foreach ($annotations as $annotation) {
237 8
                $path = implode('/', $urlParts);
238 8
                $defaults = ['_controller' => $method->getName()];
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
239 8
                $requirements = [];
240 8
                $options = [];
241 8
                $methods = explode('|', $httpMethod);
242
243 8
                $annoRequirements = $annotation->getRequirements();
244 8
                $annoMethods = $annotation->getMethods();
245
246 8
                if (!empty($annoMethods)) {
247 8
                    $methods = $annoMethods;
248 8
                }
249
250 8
                $path = $annotation->getPath() !== null ? $this->routePrefix.$annotation->getPath() : $path;
251 8
                $requirements = array_merge($requirements, $annoRequirements);
252 8
                $options = array_merge($options, $annotation->getOptions());
253 8
                $defaults = array_merge($defaults, $annotation->getDefaults());
254 8
                $host = $annotation->getHost();
255 8
                $schemes = $annotation->getSchemes();
256 8
                $condition = $this->getCondition($method, $annotation);
257
258 8
                $this->includeFormatIfNeeded($path, $requirements);
259
260
                // add route to collection
261 8
                $route = new Route(
262 8
                    $path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition
263 8
                );
264 8
                $this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable, $annotation);
265 8
            }
266 8
        } else {
267 29
            $this->includeFormatIfNeeded($path, $requirements);
268
269 29
            $methods = explode('|', strtoupper($httpMethod));
270
271
            // add route to collection
272 29
            $route = new Route(
273 29
                $path, $defaults, $requirements, $options, $host, [], $methods, $condition
274 29
            );
275 29
            $this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable);
276
        }
277 30
    }
278
279
    /**
280
     * Determine the Route condition by combining Route annotations with Version annotation.
281
     *
282
     * @param \ReflectionMethod $method
283
     * @param RouteAnnotation   $annotation
284
     *
285
     * @return string
286
     */
287 8
    private function getCondition(\ReflectionMethod $method, RouteAnnotation $annotation)
0 ignored issues
show
Unused Code introduced by
The parameter $method is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
288
    {
289 8
        $condition = $annotation->getCondition();
290
291 8
        if (!empty($this->versions)) {
292 1
            $versionCondition = "request.attributes.get('version') == (";
293 1
            $first = true;
294 1
            foreach ($this->versions as $version) {
295 1
                if (!$first) {
296 1
                    $versionCondition .= ' or ';
297 1
                }
298 1
                $versionCondition .= '\''.$version.'\'';
299 1
                $first = false;
300 1
            }
301 1
            $versionCondition .= ')';
302 1
            $condition = $condition ? '('.$condition.') and '.$versionCondition : $versionCondition;
303 1
        }
304
305 8
        return $condition;
306
    }
307
308
    /**
309
     * Include the format in the path and requirements if its enabled.
310
     *
311
     * @param string $path
312
     * @param array  $requirements
313
     */
314 30
    private function includeFormatIfNeeded(&$path, &$requirements)
315
    {
316 30 View Code Duplication
        if ($this->includeFormat === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317 30
            $path .= '.{_format}';
318
319 30
            if (!isset($requirements['_format']) && !empty($this->formats)) {
320 7
                $requirements['_format'] = implode('|', array_keys($this->formats));
321 7
            }
322 30
        }
323 30
    }
324
325
    /**
326
     * Checks whether provided method is readable.
327
     *
328
     * @param \ReflectionMethod $method
329
     *
330
     * @return bool
331
     */
332 30
    private function isMethodReadable(\ReflectionMethod $method)
333
    {
334
        // if method starts with _ - skip
335 30
        if ('_' === substr($method->getName(), 0, 1)) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
336 10
            return false;
337
        }
338
339 30
        $hasNoRouteMethod = (bool) $this->readMethodAnnotation($method, 'NoRoute');
340 30
        $hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
341
342 30
        $hasNoRoute = $hasNoRouteMethod || $hasNoRouteClass;
343
        // since NoRoute extends Route we need to exclude all the method NoRoute annotations
344 30
        $hasRoute = (bool) $this->readMethodAnnotation($method, 'Route') && !$hasNoRouteMethod;
345
346
        // if method has NoRoute annotation and does not have Route annotation - skip
347 30
        if ($hasNoRoute && !$hasRoute) {
348 4
            return false;
349
        }
350
351 30
        return true;
352
    }
353
354
    /**
355
     * Returns HTTP method and resources list from method signature.
356
     *
357
     * @param \ReflectionMethod $method
358
     * @param string[]          $resource
359
     *
360
     * @return bool|array
361
     */
362 30
    private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method, $resource)
363
    {
364
        // if method doesn't match regex - skip
365 30
        if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
366 30
            return false;
367
        }
368
369 30
        $httpMethod = strtolower($matches[1]);
370 30
        $resources = preg_split(
371 30
            '/([A-Z][^A-Z]*)/', $matches[2], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
372 30
        );
373 30
        $isCollection = false;
374 30
        $isInflectable = true;
375
376 30
        if (0 === strpos($httpMethod, self::COLLECTION_ROUTE_PREFIX)
377 30
            && in_array(substr($httpMethod, 1), $this->availableHTTPMethods)
378 30
        ) {
379 11
            $isCollection = true;
380 11
            $httpMethod = substr($httpMethod, 1);
381 30
        } elseif ('options' === $httpMethod) {
382 14
            $isCollection = true;
383 14
        }
384
385 30
        if ($isCollection && !empty($resource)) {
386 11
            $resourcePluralized = $this->generateResourceName(end($resource));
387 11
            $isInflectable = ($resourcePluralized != $resource[count($resource) - 1]);
388 11
            $resource[count($resource) - 1] = $resourcePluralized;
389 11
        }
390
391 30
        $resources = array_merge($resource, $resources);
392
393 30
        return [$httpMethod, $resources, $isCollection, $isInflectable];
394
    }
395
396
    /**
397
     * Returns readable arguments from method.
398
     *
399
     * @param \ReflectionMethod $method
400
     *
401
     * @return \ReflectionParameter[]
402
     */
403 30
    private function getMethodArguments(\ReflectionMethod $method)
404
    {
405
        // ignore all query params
406 30
        $params = $this->paramReader->getParamsFromMethod($method);
407
408
        // ignore several type hinted arguments
409
        $ignoreClasses = [
410 30
            \Symfony\Component\HttpFoundation\Request::class,
411 30
            \FOS\RestBundle\Request\ParamFetcherInterface::class,
412 30
            \Symfony\Component\Validator\ConstraintViolationListInterface::class,
413 30
            \Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter::class,
414 30
        ];
415
416 30
        $arguments = [];
417 30
        foreach ($method->getParameters() as $argument) {
418 27
            if (isset($params[$argument->getName()])) {
0 ignored issues
show
Bug introduced by
Consider using $argument->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
419
                continue;
420
            }
421
422 27
            $argumentClass = $argument->getClass();
423 27
            if ($argumentClass) {
424 16
                foreach ($ignoreClasses as $class) {
425 16
                    $className = $argumentClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $argumentClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
426 16
                    if ($className === $class || is_subclass_of($argumentClass, $className)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $className can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
427 16
                        continue 2;
428
                    }
429
                }
430
            }
431
432 21
            $arguments[] = $argument;
433 30
        }
434
435 30
        return $arguments;
436
    }
437
438
    /**
439
     * Generates final resource name.
440
     *
441
     * @param string|bool $resource
442
     *
443
     * @return string
444
     */
445 30
    private function generateResourceName($resource)
446
    {
447 30
        if (false === $this->pluralize) {
448 1
            return $resource;
449
        }
450
451 29
        return $this->inflector->pluralize($resource);
0 ignored issues
show
Bug introduced by
It seems like $resource defined by parameter $resource on line 445 can also be of type boolean; however, FOS\RestBundle\Inflector...rInterface::pluralize() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
452
    }
453
454
    /**
455
     * Generates route name from resources list.
456
     *
457
     * @param string[] $resources
458
     *
459
     * @return string
460
     */
461 30
    private function generateRouteName(array $resources)
462
    {
463 30
        $routeName = '';
464 30
        foreach ($resources as $resource) {
465 30
            if (null !== $resource) {
466 30
                $routeName .= '_'.basename($resource);
467 30
            }
468 30
        }
469
470 30
        return $routeName;
471
    }
472
473
    /**
474
     * Generates URL parts for route from resources list.
475
     *
476
     * @param string[]               $resources
477
     * @param \ReflectionParameter[] $arguments
478
     * @param string                 $httpMethod
479
     *
480
     * @return array
481
     */
482 30
    private function generateUrlParts(array $resources, array $arguments, $httpMethod)
483
    {
484 30
        $urlParts = [];
485 30
        foreach ($resources as $i => $resource) {
486
            // if we already added all parent routes paths to URL & we have
487
            // prefix - add it
488 30
            if (!empty($this->routePrefix) && $i === count($this->parents)) {
489 3
                $urlParts[] = $this->routePrefix;
490 3
            }
491
492
            // if we have argument for current resource, then it's object.
493
            // otherwise - it's collection
494 30
            if (isset($arguments[$i])) {
495 21
                if (null !== $resource) {
496 21
                    $urlParts[] =
497 21
                        strtolower($this->generateResourceName($resource))
498 21
                        .'/{'.$arguments[$i]->getName().'}';
0 ignored issues
show
Bug introduced by
Consider using $arguments[$i]->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
499 21
                } else {
500
                    $urlParts[] = '{'.$arguments[$i]->getName().'}';
0 ignored issues
show
Bug introduced by
Consider using $arguments[$i]->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
501
                }
502 30
            } elseif (null !== $resource) {
503 30
                if ((0 === count($arguments) && !in_array($httpMethod, $this->availableHTTPMethods))
504
                    || 'new' === $httpMethod
505 29
                    || 'post' === $httpMethod
506 30
                ) {
507 17
                    $urlParts[] = $this->generateResourceName(strtolower($resource));
508 17
                } else {
509 29
                    $urlParts[] = strtolower($resource);
510
                }
511 30
            }
512 30
        }
513
514 30
        return $urlParts;
515
    }
516
517
    /**
518
     * Returns custom HTTP method for provided list of resources, arguments, method.
519
     *
520
     * @param string                 $httpMethod current HTTP method
521
     * @param string[]               $resources  resources list
522
     * @param \ReflectionParameter[] $arguments  list of method arguments
523
     *
524
     * @return string
525
     */
526 17
    private function getCustomHttpMethod($httpMethod, array $resources, array $arguments)
527
    {
528 17
        if (in_array($httpMethod, $this->availableConventionalActions)) {
529
            // allow hypertext as the engine of application state
530
            // through conventional GET actions
531 12
            return 'get';
532
        }
533
534 15
        if (count($arguments) < count($resources)) {
535
            // resource collection
536 15
            return 'get';
537
        }
538
539
        //custom object
540 14
        return 'patch';
541
    }
542
543
    /**
544
     * Returns first route annotation for method.
545
     *
546
     * @param \ReflectionMethod $reflectionMethod
547
     *
548
     * @return RouteAnnotation[]
549
     */
550 30
    private function readRouteAnnotation(\ReflectionMethod $reflectionMethod)
551
    {
552 30
        $annotations = [];
553
554 30
        foreach (['Route', 'Get', 'Post', 'Put', 'Patch', 'Delete', 'Link', 'Unlink', 'Head', 'Options'] as $annotationName) {
555 30
            if ($annotations_new = $this->readMethodAnnotations($reflectionMethod, $annotationName)) {
556 8
                $annotations = array_merge($annotations, $annotations_new);
557 8
            }
558 30
        }
559
560 30
        return $annotations;
561
    }
562
563
    /**
564
     * Reads class annotations.
565
     *
566
     * @param \ReflectionClass $reflectionClass
567
     * @param string           $annotationName
568
     *
569
     * @return RouteAnnotation|null
570
     */
571 30
    private function readClassAnnotation(\ReflectionClass $reflectionClass, $annotationName)
572
    {
573 30
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
574
575 30
        if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, $annotationClass)) {
576
            return $annotation;
577
        }
578 30
    }
579
580
    /**
581
     * Reads method annotations.
582
     *
583
     * @param \ReflectionMethod $reflectionMethod
584
     * @param string            $annotationName
585
     *
586
     * @return RouteAnnotation|null
587
     */
588 30
    private function readMethodAnnotation(\ReflectionMethod $reflectionMethod, $annotationName)
589
    {
590 30
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
591
592 30
        if ($annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClass)) {
593 8
            return $annotation;
594
        }
595 30
    }
596
597
    /**
598
     * Reads method annotations.
599
     *
600
     * @param \ReflectionMethod $reflectionMethod
601
     * @param string            $annotationName
602
     *
603
     * @return RouteAnnotation[]
604
     */
605 30
    private function readMethodAnnotations(\ReflectionMethod $reflectionMethod, $annotationName)
606
    {
607 30
        $annotations = [];
608 30
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
609
610 30
        if ($annotations_new = $this->annotationReader->getMethodAnnotations($reflectionMethod)) {
611 14
            foreach ($annotations_new as $annotation) {
612 14
                if ($annotation instanceof $annotationClass) {
613 8
                    $annotations[] = $annotation;
614 8
                }
615 14
            }
616 14
        }
617
618 30
        return $annotations;
619
    }
620
621
    /**
622
     * @param RestRouteCollection $collection
623
     * @param string              $routeName
624
     * @param Route               $route
625
     * @param bool                $isCollection
626
     * @param bool                $isInflectable
627
     * @param RouteAnnotation     $annotation
628
     */
629 30
    private function addRoute(RestRouteCollection $collection, $routeName, $route, $isCollection, $isInflectable, RouteAnnotation $annotation = null)
630
    {
631 30
        if ($annotation && null !== $annotation->getName()) {
632 4
            $options = $annotation->getOptions();
633
634 4
            if (isset($options['method_prefix']) && false === $options['method_prefix']) {
635 3
                $routeName = $annotation->getName();
636 3
            } else {
637 4
                $routeName = $routeName.$annotation->getName();
638
            }
639 4
        }
640
641 30
        $fullRouteName = $this->namePrefix.$routeName;
642
643 30
        if ($isCollection && !$isInflectable) {
644 4
            $collection->add($this->namePrefix.self::COLLECTION_ROUTE_PREFIX.$routeName, $route);
645 4
            if (!$collection->get($fullRouteName)) {
646 4
                $collection->add($fullRouteName, clone $route);
647 4
            }
648 4
        } else {
649 28
            $collection->add($fullRouteName, $route);
650
        }
651 30
    }
652
}
653