vendor/friendsofsymfony/rest-bundle/Routing/Loader/Reader/RestActionReader.php line 644

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the FOSRestBundle package.
  4.  *
  5.  * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace FOS\RestBundle\Routing\Loader\Reader;
  11. use Doctrine\Common\Annotations\Reader;
  12. use FOS\RestBundle\Controller\Annotations\Route as RouteAnnotation;
  13. use FOS\RestBundle\Inflector\InflectorInterface;
  14. use FOS\RestBundle\Request\ParamFetcherInterface;
  15. use FOS\RestBundle\Request\ParamReaderInterface;
  16. use FOS\RestBundle\Routing\RestRouteCollection;
  17. use Psr\Http\Message\MessageInterface;
  18. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  21. use Symfony\Component\Routing\Route;
  22. use Symfony\Component\Security\Core\User\UserInterface;
  23. use Symfony\Component\Validator\ConstraintViolationListInterface;
  24. /**
  25.  * REST controller actions reader.
  26.  *
  27.  * @author Konstantin Kudryashov <ever.zet@gmail.com>
  28.  */
  29. class RestActionReader
  30. {
  31.     const COLLECTION_ROUTE_PREFIX 'c';
  32.     /**
  33.      * @var Reader
  34.      */
  35.     private $annotationReader;
  36.     /**
  37.      * @var ParamReaderInterface
  38.      */
  39.     private $paramReader;
  40.     /**
  41.      * @var InflectorInterface
  42.      */
  43.     private $inflector;
  44.     /**
  45.      * @var array
  46.      */
  47.     private $formats;
  48.     /**
  49.      * @var bool
  50.      */
  51.     private $includeFormat;
  52.     /**
  53.      * @var string|null
  54.      */
  55.     private $routePrefix;
  56.     /**
  57.      * @var string|null
  58.      */
  59.     private $namePrefix;
  60.     /**
  61.      * @var array|string|null
  62.      */
  63.     private $versions;
  64.     /**
  65.      * @var bool|null
  66.      */
  67.     private $pluralize;
  68.     /**
  69.      * @var array
  70.      */
  71.     private $parents = [];
  72.     /**
  73.      * @var array
  74.      */
  75.     private $availableHTTPMethods = [
  76.         'get',
  77.         'post',
  78.         'put',
  79.         'patch',
  80.         'delete',
  81.         'link',
  82.         'unlink',
  83.         'head',
  84.         'options',
  85.         'mkcol',
  86.         'propfind',
  87.         'proppatch',
  88.         'move',
  89.         'copy',
  90.         'lock',
  91.         'unlock',
  92.     ];
  93.     /**
  94.      * @var array
  95.      */
  96.     private $availableConventionalActions = ['new''edit''remove'];
  97.     /**
  98.      * @var bool
  99.      */
  100.     private $hasMethodPrefix;
  101.     /**
  102.      * Initializes controller reader.
  103.      *
  104.      * @param Reader               $annotationReader
  105.      * @param ParamReaderInterface $paramReader
  106.      * @param InflectorInterface   $inflector
  107.      * @param bool                 $includeFormat
  108.      * @param array                $formats
  109.      * @param bool                 $hasMethodPrefix
  110.      */
  111.     public function __construct(Reader $annotationReaderParamReaderInterface $paramReaderInflectorInterface $inflector$includeFormat, array $formats = [], $hasMethodPrefix true)
  112.     {
  113.         $this->annotationReader $annotationReader;
  114.         $this->paramReader $paramReader;
  115.         $this->inflector $inflector;
  116.         $this->includeFormat $includeFormat;
  117.         $this->formats $formats;
  118.         $this->hasMethodPrefix $hasMethodPrefix;
  119.     }
  120.     /**
  121.      * Sets routes prefix.
  122.      *
  123.      * @param string $prefix Routes prefix
  124.      */
  125.     public function setRoutePrefix($prefix null)
  126.     {
  127.         $this->routePrefix $prefix;
  128.     }
  129.     /**
  130.      * Returns route prefix.
  131.      *
  132.      * @return string
  133.      */
  134.     public function getRoutePrefix()
  135.     {
  136.         return $this->routePrefix;
  137.     }
  138.     /**
  139.      * Sets route names prefix.
  140.      *
  141.      * @param string $prefix Route names prefix
  142.      */
  143.     public function setNamePrefix($prefix null)
  144.     {
  145.         $this->namePrefix $prefix;
  146.     }
  147.     /**
  148.      * Returns name prefix.
  149.      *
  150.      * @return string
  151.      */
  152.     public function getNamePrefix()
  153.     {
  154.         return $this->namePrefix;
  155.     }
  156.     /**
  157.      * Sets route names versions.
  158.      *
  159.      * @param array|string|null $versions Route names versions
  160.      */
  161.     public function setVersions($versions null)
  162.     {
  163.         $this->versions = (array) $versions;
  164.     }
  165.     /**
  166.      * Returns versions.
  167.      *
  168.      * @return array|null
  169.      */
  170.     public function getVersions()
  171.     {
  172.         return $this->versions;
  173.     }
  174.     /**
  175.      * Sets pluralize.
  176.      *
  177.      * @param bool|null $pluralize Specify if resource name must be pluralized
  178.      */
  179.     public function setPluralize($pluralize)
  180.     {
  181.         $this->pluralize $pluralize;
  182.     }
  183.     /**
  184.      * Returns pluralize.
  185.      *
  186.      * @return bool|null
  187.      */
  188.     public function getPluralize()
  189.     {
  190.         return $this->pluralize;
  191.     }
  192.     /**
  193.      * Set parent routes.
  194.      *
  195.      * @param array $parents Array of parent resources names
  196.      */
  197.     public function setParents(array $parents)
  198.     {
  199.         $this->parents $parents;
  200.     }
  201.     /**
  202.      * Returns parents.
  203.      *
  204.      * @return array
  205.      */
  206.     public function getParents()
  207.     {
  208.         return $this->parents;
  209.     }
  210.     /**
  211.      * Reads action route.
  212.      *
  213.      * @param RestRouteCollection $collection
  214.      * @param \ReflectionMethod   $method
  215.      * @param string[]            $resource
  216.      *
  217.      * @throws \InvalidArgumentException
  218.      *
  219.      * @return Route
  220.      */
  221.     public function read(RestRouteCollection $collection, \ReflectionMethod $method$resource)
  222.     {
  223.         // check that every route parent has non-empty singular name
  224.         foreach ($this->parents as $parent) {
  225.             if (empty($parent) || '/' === substr($parent, -1)) {
  226.                 throw new \InvalidArgumentException('Every parent controller must have `get{SINGULAR}Action(\$id)` method where {SINGULAR} is a singular form of associated object');
  227.             }
  228.         }
  229.         // if method is not readable - skip
  230.         if (!$this->isMethodReadable($method)) {
  231.             return;
  232.         }
  233.         // if we can't get http-method and resources from method name - skip
  234.         $httpMethodAndResources $this->getHttpMethodAndResourcesFromMethod($method$resource);
  235.         if (!$httpMethodAndResources) {
  236.             return;
  237.         }
  238.         list($httpMethod$resources$isCollection$isInflectable) = $httpMethodAndResources;
  239.         $arguments $this->getMethodArguments($method);
  240.         // if we have only 1 resource & 1 argument passed, then it's object call, so
  241.         // we can set collection singular name
  242.         if (=== count($resources) && === count($arguments) - count($this->parents)) {
  243.             $collection->setSingularName($resources[0]);
  244.         }
  245.         // if we have parents passed - merge them with own resource names
  246.         if (count($this->parents)) {
  247.             $resources array_merge($this->parents$resources);
  248.         }
  249.         if (empty($resources)) {
  250.             $resources[] = null;
  251.         }
  252.         $routeName $httpMethod.$this->generateRouteName($resources);
  253.         $urlParts $this->generateUrlParts($resources$arguments$httpMethod);
  254.         // if passed method is not valid HTTP method then it's either
  255.         // a hypertext driver, a custom object (PUT) or collection (GET)
  256.         // method
  257.         if (!in_array($httpMethod$this->availableHTTPMethods)) {
  258.             $urlParts[] = $httpMethod;
  259.             $httpMethod $this->getCustomHttpMethod($httpMethod$resources$arguments);
  260.         }
  261.         // generated parameters
  262.         $routeName strtolower($routeName);
  263.         $path implode('/'$urlParts);
  264.         $defaults = ['_controller' => $method->getName()];
  265.         $requirements = [];
  266.         $options = [];
  267.         $host '';
  268.         $versionCondition $this->getVersionCondition();
  269.         $versionRequirement $this->getVersionRequirement();
  270.         $annotations $this->readRouteAnnotation($method);
  271.         if (!empty($annotations)) {
  272.             foreach ($annotations as $annotation) {
  273.                 $path implode('/'$urlParts);
  274.                 $defaults = ['_controller' => $method->getName()];
  275.                 $requirements = [];
  276.                 $options = [];
  277.                 $methods explode('|'$httpMethod);
  278.                 $annoRequirements $annotation->getRequirements();
  279.                 $annoMethods $annotation->getMethods();
  280.                 if (!empty($annoMethods)) {
  281.                     $methods $annoMethods;
  282.                 }
  283.                 $path null !== $annotation->getPath() ? $this->routePrefix.$annotation->getPath() : $path;
  284.                 $requirements array_merge($requirements$annoRequirements);
  285.                 $options array_merge($options$annotation->getOptions());
  286.                 $defaults array_merge($defaults$annotation->getDefaults());
  287.                 $host $annotation->getHost();
  288.                 $schemes $annotation->getSchemes();
  289.                 if ($this->hasVersionPlaceholder($path)) {
  290.                     $combinedCondition $annotation->getCondition();
  291.                     $requirements array_merge($versionRequirement$requirements);
  292.                 } else {
  293.                     $combinedCondition $this->combineConditions($versionCondition$annotation->getCondition());
  294.                 }
  295.                 $this->includeFormatIfNeeded($path$requirements);
  296.                 // add route to collection
  297.                 $route = new Route(
  298.                     $path,
  299.                     $defaults,
  300.                     $requirements,
  301.                     $options,
  302.                     $host,
  303.                     $schemes,
  304.                     $methods,
  305.                     $combinedCondition
  306.                 );
  307.                 $this->addRoute($collection$routeName$route$isCollection$isInflectable$annotation);
  308.             }
  309.         } else {
  310.             if ($this->hasVersionPlaceholder($path)) {
  311.                 $versionCondition null;
  312.                 $requirements $versionRequirement;
  313.             }
  314.             $this->includeFormatIfNeeded($path$requirements);
  315.             $methods explode('|'strtoupper($httpMethod));
  316.             // add route to collection
  317.             $route = new Route(
  318.                 $path,
  319.                 $defaults,
  320.                 $requirements,
  321.                 $options,
  322.                 $host,
  323.                 [],
  324.                 $methods,
  325.                 $versionCondition
  326.             );
  327.             $this->addRoute($collection$routeName$route$isCollection$isInflectable);
  328.         }
  329.     }
  330.     /**
  331.      * @return string|null
  332.      */
  333.     private function getVersionCondition()
  334.     {
  335.         if (empty($this->versions)) {
  336.             return;
  337.         }
  338.         return sprintf("request.attributes.get('version') in ['%s']"implode("', '"$this->versions));
  339.     }
  340.     /**
  341.      * @param string|null $conditionOne
  342.      * @param string|null $conditionTwo
  343.      *
  344.      * @return string|null
  345.      */
  346.     private function combineConditions($conditionOne$conditionTwo)
  347.     {
  348.         if (null === $conditionOne) {
  349.             return $conditionTwo;
  350.         }
  351.         if (null === $conditionTwo) {
  352.             return $conditionOne;
  353.         }
  354.         return sprintf('(%s) and (%s)'$conditionOne$conditionTwo);
  355.     }
  356.     /**
  357.      * @return array
  358.      */
  359.     private function getVersionRequirement()
  360.     {
  361.         if (empty($this->versions)) {
  362.             return [];
  363.         }
  364.         return ['version' => implode('|'$this->versions)];
  365.     }
  366.     /**
  367.      * Checks whether provided path contains {version} placeholder.
  368.      *
  369.      * @param string $path
  370.      *
  371.      * @return bool
  372.      */
  373.     private function hasVersionPlaceholder($path)
  374.     {
  375.         return false !== strpos($path'{version}');
  376.     }
  377.     /**
  378.      * Include the format in the path and requirements if its enabled.
  379.      *
  380.      * @param string $path
  381.      * @param array  $requirements
  382.      */
  383.     private function includeFormatIfNeeded(&$path, &$requirements)
  384.     {
  385.         if (true === $this->includeFormat) {
  386.             $path .= '.{_format}';
  387.             if (!isset($requirements['_format']) && !empty($this->formats)) {
  388.                 $requirements['_format'] = implode('|'array_keys($this->formats));
  389.             }
  390.         }
  391.     }
  392.     /**
  393.      * Checks whether provided method is readable.
  394.      *
  395.      * @param \ReflectionMethod $method
  396.      *
  397.      * @return bool
  398.      */
  399.     private function isMethodReadable(\ReflectionMethod $method)
  400.     {
  401.         // if method starts with _ - skip
  402.         if ('_' === substr($method->getName(), 01)) {
  403.             return false;
  404.         }
  405.         $hasNoRouteMethod = (bool) $this->readMethodAnnotation($method'NoRoute');
  406.         $hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
  407.         $hasNoRoute $hasNoRouteMethod || $hasNoRouteClass;
  408.         // since NoRoute extends Route we need to exclude all the method NoRoute annotations
  409.         $hasRoute = (bool) $this->readMethodAnnotation($method'Route') && !$hasNoRouteMethod;
  410.         // if method has NoRoute annotation and does not have Route annotation - skip
  411.         if ($hasNoRoute && !$hasRoute) {
  412.             return false;
  413.         }
  414.         return true;
  415.     }
  416.     /**
  417.      * Returns HTTP method and resources list from method signature.
  418.      *
  419.      * @param \ReflectionMethod $method
  420.      * @param string[]          $resource
  421.      *
  422.      * @return bool|array
  423.      */
  424.     private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method$resource)
  425.     {
  426.         // if method doesn't match regex - skip
  427.         if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/'$method->getName(), $matches)) {
  428.             return false;
  429.         }
  430.         $httpMethod strtolower($matches[1]);
  431.         $resources preg_split(
  432.             '/([A-Z][^A-Z]*)/',
  433.             $matches[2],
  434.             -1,
  435.             PREG_SPLIT_NO_EMPTY PREG_SPLIT_DELIM_CAPTURE
  436.         );
  437.         $isCollection false;
  438.         $isInflectable true;
  439.         if (=== strpos($httpMethodself::COLLECTION_ROUTE_PREFIX)
  440.             && in_array(substr($httpMethod1), $this->availableHTTPMethods)
  441.         ) {
  442.             $isCollection true;
  443.             $httpMethod substr($httpMethod1);
  444.         } elseif ('options' === $httpMethod) {
  445.             $isCollection true;
  446.         }
  447.         if ($isCollection && !empty($resource)) {
  448.             $resourcePluralized $this->generateResourceName(end($resource));
  449.             $isInflectable = ($resourcePluralized != $resource[count($resource) - 1]);
  450.             $resource[count($resource) - 1] = $resourcePluralized;
  451.         }
  452.         $resources array_merge($resource$resources);
  453.         return [$httpMethod$resources$isCollection$isInflectable];
  454.     }
  455.     /**
  456.      * Returns readable arguments from method.
  457.      *
  458.      * @param \ReflectionMethod $method
  459.      *
  460.      * @return \ReflectionParameter[]
  461.      */
  462.     private function getMethodArguments(\ReflectionMethod $method)
  463.     {
  464.         // ignore all query params
  465.         $params $this->paramReader->getParamsFromMethod($method);
  466.         // check if a parameter is coming from the request body
  467.         $ignoreParameters = [];
  468.         if (class_exists(ParamConverter::class)) {
  469.             $ignoreParameters array_map(function ($annotation) {
  470.                 return
  471.                     $annotation instanceof ParamConverter &&
  472.                     'fos_rest.request_body' === $annotation->getConverter()
  473.                         ? $annotation->getName() : null;
  474.             }, $this->annotationReader->getMethodAnnotations($method));
  475.         }
  476.         // ignore several type hinted arguments
  477.         $ignoreClasses = [
  478.             ConstraintViolationListInterface::class,
  479.             MessageInterface::class,
  480.             ParamConverter::class,
  481.             ParamFetcherInterface::class,
  482.             Request::class,
  483.             SessionInterface::class,
  484.             UserInterface::class,
  485.         ];
  486.         $arguments = [];
  487.         foreach ($method->getParameters() as $argument) {
  488.             if (isset($params[$argument->getName()])) {
  489.                 continue;
  490.             }
  491.             $argumentClass $argument->getClass();
  492.             if ($argumentClass) {
  493.                 $className $argumentClass->getName();
  494.                 foreach ($ignoreClasses as $class) {
  495.                     if ($className === $class || is_subclass_of($className$class)) {
  496.                         continue 2;
  497.                     }
  498.                 }
  499.             }
  500.             if (in_array($argument->getName(), $ignoreParameterstrue)) {
  501.                 continue;
  502.             }
  503.             $arguments[] = $argument;
  504.         }
  505.         return $arguments;
  506.     }
  507.     /**
  508.      * Generates final resource name.
  509.      *
  510.      * @param string|bool $resource
  511.      *
  512.      * @return string
  513.      */
  514.     private function generateResourceName($resource)
  515.     {
  516.         if (false === $this->pluralize) {
  517.             return $resource;
  518.         }
  519.         return $this->inflector->pluralize($resource);
  520.     }
  521.     /**
  522.      * Generates route name from resources list.
  523.      *
  524.      * @param string[] $resources
  525.      *
  526.      * @return string
  527.      */
  528.     private function generateRouteName(array $resources)
  529.     {
  530.         $routeName '';
  531.         foreach ($resources as $resource) {
  532.             if (null !== $resource) {
  533.                 $routeName .= '_'.basename($resource);
  534.             }
  535.         }
  536.         return $routeName;
  537.     }
  538.     /**
  539.      * Generates URL parts for route from resources list.
  540.      *
  541.      * @param string[]               $resources
  542.      * @param \ReflectionParameter[] $arguments
  543.      * @param string                 $httpMethod
  544.      *
  545.      * @return array
  546.      */
  547.     private function generateUrlParts(array $resources, array $arguments$httpMethod)
  548.     {
  549.         $urlParts = [];
  550.         foreach ($resources as $i => $resource) {
  551.             // if we already added all parent routes paths to URL & we have
  552.             // prefix - add it
  553.             if (!empty($this->routePrefix) && $i === count($this->parents)) {
  554.                 $urlParts[] = $this->routePrefix;
  555.             }
  556.             // if we have argument for current resource, then it's object.
  557.             // otherwise - it's collection
  558.             if (isset($arguments[$i])) {
  559.                 if (null !== $resource) {
  560.                     $urlParts[] =
  561.                         strtolower($this->generateResourceName($resource))
  562.                         .'/{'.$arguments[$i]->getName().'}';
  563.                 } else {
  564.                     $urlParts[] = '{'.$arguments[$i]->getName().'}';
  565.                 }
  566.             } elseif (null !== $resource) {
  567.                 if ((=== count($arguments) && !in_array($httpMethod$this->availableHTTPMethods))
  568.                     || 'new' === $httpMethod
  569.                     || 'post' === $httpMethod
  570.                 ) {
  571.                     $urlParts[] = $this->generateResourceName(strtolower($resource));
  572.                 } else {
  573.                     $urlParts[] = strtolower($resource);
  574.                 }
  575.             }
  576.         }
  577.         return $urlParts;
  578.     }
  579.     /**
  580.      * Returns custom HTTP method for provided list of resources, arguments, method.
  581.      *
  582.      * @param string                 $httpMethod current HTTP method
  583.      * @param string[]               $resources  resources list
  584.      * @param \ReflectionParameter[] $arguments  list of method arguments
  585.      *
  586.      * @return string
  587.      */
  588.     private function getCustomHttpMethod($httpMethod, array $resources, array $arguments)
  589.     {
  590.         if (in_array($httpMethod$this->availableConventionalActions)) {
  591.             // allow hypertext as the engine of application state
  592.             // through conventional GET actions
  593.             return 'get';
  594.         }
  595.         if (count($arguments) < count($resources)) {
  596.             // resource collection
  597.             return 'get';
  598.         }
  599.         // custom object
  600.         return 'patch';
  601.     }
  602.     /**
  603.      * Returns first route annotation for method.
  604.      *
  605.      * @param \ReflectionMethod $reflectionMethod
  606.      *
  607.      * @return RouteAnnotation[]
  608.      */
  609.     private function readRouteAnnotation(\ReflectionMethod $reflectionMethod)
  610.     {
  611.         $annotations = [];
  612.         if ($newAnnotations $this->readMethodAnnotations($reflectionMethod'Route')) {
  613.             $annotations array_merge($annotations$newAnnotations);
  614.         }
  615.         return $annotations;
  616.     }
  617.     /**
  618.      * Reads class annotations.
  619.      *
  620.      * @param \ReflectionClass $reflectionClass
  621.      * @param string           $annotationName
  622.      *
  623.      * @return RouteAnnotation|null
  624.      */
  625.     private function readClassAnnotation(\ReflectionClass $reflectionClass$annotationName)
  626.     {
  627.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  628.         if ($annotation $this->annotationReader->getClassAnnotation($reflectionClass$annotationClass)) {
  629.             return $annotation;
  630.         }
  631.     }
  632.     /**
  633.      * Reads method annotations.
  634.      *
  635.      * @param \ReflectionMethod $reflectionMethod
  636.      * @param string            $annotationName
  637.      *
  638.      * @return RouteAnnotation|null
  639.      */
  640.     private function readMethodAnnotation(\ReflectionMethod $reflectionMethod$annotationName)
  641.     {
  642.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  643.         if ($annotation $this->annotationReader->getMethodAnnotation($reflectionMethod$annotationClass)) {
  644.             return $annotation;
  645.         }
  646.     }
  647.     /**
  648.      * Reads method annotations.
  649.      *
  650.      * @param \ReflectionMethod $reflectionMethod
  651.      * @param string            $annotationName
  652.      *
  653.      * @return RouteAnnotation[]
  654.      */
  655.     private function readMethodAnnotations(\ReflectionMethod $reflectionMethod$annotationName)
  656.     {
  657.         $annotations = [];
  658.         $annotationClass "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
  659.         if ($annotations_new $this->annotationReader->getMethodAnnotations($reflectionMethod)) {
  660.             foreach ($annotations_new as $annotation) {
  661.                 if ($annotation instanceof $annotationClass) {
  662.                     $annotations[] = $annotation;
  663.                 }
  664.             }
  665.         }
  666.         return $annotations;
  667.     }
  668.     /**
  669.      * @param RestRouteCollection $collection
  670.      * @param string              $routeName
  671.      * @param Route               $route
  672.      * @param bool                $isCollection
  673.      * @param bool                $isInflectable
  674.      * @param RouteAnnotation     $annotation
  675.      */
  676.     private function addRoute(RestRouteCollection $collection$routeName$route$isCollection$isInflectableRouteAnnotation $annotation null)
  677.     {
  678.         if ($annotation && null !== $annotation->getName()) {
  679.             $options $annotation->getOptions();
  680.             if (false === $this->hasMethodPrefix || (isset($options['method_prefix']) && false === $options['method_prefix'])) {
  681.                 $routeName $annotation->getName();
  682.             } else {
  683.                 $routeName .= $annotation->getName();
  684.             }
  685.         }
  686.         $fullRouteName $this->namePrefix.$routeName;
  687.         if ($isCollection && !$isInflectable) {
  688.             $collection->add($this->namePrefix.self::COLLECTION_ROUTE_PREFIX.$routeName$route);
  689.             if (!$collection->get($fullRouteName)) {
  690.                 $collection->add($fullRouteName, clone $route);
  691.             }
  692.         } else {
  693.             $collection->add($fullRouteName$route);
  694.         }
  695.     }
  696. }