vendor/symfony-cmf/routing/src/ChainRouter.php line 210

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony CMF package.
  4.  *
  5.  * (c) Symfony CMF
  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 Symfony\Cmf\Component\Routing;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\HttpFoundation\Request;
  13. use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  14. use Symfony\Component\Routing\Exception\MethodNotAllowedException;
  15. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  16. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  17. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  18. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  19. use Symfony\Component\Routing\RequestContext;
  20. use Symfony\Component\Routing\RequestContextAwareInterface;
  21. use Symfony\Component\Routing\RouteCollection;
  22. use Symfony\Component\Routing\RouterInterface;
  23. /**
  24.  * The ChainRouter allows to combine several routers to try in a defined order.
  25.  *
  26.  * @author Henrik Bjornskov <henrik@bjrnskov.dk>
  27.  * @author Magnus Nordlander <magnus@e-butik.se>
  28.  */
  29. class ChainRouter implements ChainRouterInterfaceWarmableInterface
  30. {
  31.     /**
  32.      * @var RequestContext|null
  33.      */
  34.     private $context;
  35.     /**
  36.      * Array of arrays of routers grouped by priority.
  37.      *
  38.      * @var RouterInterface[][] Priority => RouterInterface[]
  39.      */
  40.     private $routers = [];
  41.     /**
  42.      * @var RouterInterface[] List of routers, sorted by priority
  43.      */
  44.     private $sortedRouters = [];
  45.     /**
  46.      * @var RouteCollection
  47.      */
  48.     private $routeCollection;
  49.     /**
  50.      * @var null|LoggerInterface
  51.      */
  52.     protected $logger;
  53.     /**
  54.      * @param LoggerInterface $logger
  55.      */
  56.     public function __construct(LoggerInterface $logger null)
  57.     {
  58.         $this->logger $logger;
  59.     }
  60.     /**
  61.      * @return RequestContext
  62.      */
  63.     public function getContext()
  64.     {
  65.         if (!$this->context) {
  66.             $this->context = new RequestContext();
  67.         }
  68.         return $this->context;
  69.     }
  70.     /**
  71.      * {@inheritdoc}
  72.      */
  73.     public function add($router$priority 0)
  74.     {
  75.         if (!$router instanceof RouterInterface
  76.             && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface)
  77.         ) {
  78.             throw new \InvalidArgumentException(sprintf('%s is not a valid router.'get_class($router)));
  79.         }
  80.         if (empty($this->routers[$priority])) {
  81.             $this->routers[$priority] = [];
  82.         }
  83.         $this->routers[$priority][] = $router;
  84.         $this->sortedRouters = [];
  85.     }
  86.     /**
  87.      * {@inheritdoc}
  88.      */
  89.     public function all()
  90.     {
  91.         if (=== count($this->sortedRouters)) {
  92.             $this->sortedRouters $this->sortRouters();
  93.             // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
  94.             // See https://github.com/symfony-cmf/Routing/pull/18
  95.             if (null !== $this->context) {
  96.                 foreach ($this->sortedRouters as $router) {
  97.                     if ($router instanceof RequestContextAwareInterface) {
  98.                         $router->setContext($this->context);
  99.                     }
  100.                 }
  101.             }
  102.         }
  103.         return $this->sortedRouters;
  104.     }
  105.     /**
  106.      * Sort routers by priority.
  107.      * The highest priority number is the highest priority (reverse sorting).
  108.      *
  109.      * @return RouterInterface[]
  110.      */
  111.     protected function sortRouters()
  112.     {
  113.         if (=== count($this->routers)) {
  114.             return [];
  115.         }
  116.         krsort($this->routers);
  117.         return call_user_func_array('array_merge'$this->routers);
  118.     }
  119.     /**
  120.      * {@inheritdoc}
  121.      *
  122.      * Loops through all routes and tries to match the passed url.
  123.      *
  124.      * Note: You should use matchRequest if you can.
  125.      */
  126.     public function match($pathinfo)
  127.     {
  128.         return $this->doMatch($pathinfo);
  129.     }
  130.     /**
  131.      * {@inheritdoc}
  132.      *
  133.      * Loops through all routes and tries to match the passed request.
  134.      */
  135.     public function matchRequest(Request $request)
  136.     {
  137.         return $this->doMatch($request->getPathInfo(), $request);
  138.     }
  139.     /**
  140.      * Loops through all routers and tries to match the passed request or url.
  141.      *
  142.      * At least the  url must be provided, if a request is additionally provided
  143.      * the request takes precedence.
  144.      *
  145.      * @param string  $pathinfo
  146.      * @param Request $request
  147.      *
  148.      * @return array An array of parameters
  149.      *
  150.      * @throws ResourceNotFoundException If no router matched
  151.      */
  152.     private function doMatch($pathinfoRequest $request null)
  153.     {
  154.         $methodNotAllowed null;
  155.         $requestForMatching $request;
  156.         foreach ($this->all() as $router) {
  157.             try {
  158.                 // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php
  159.                 // matching requests is more powerful than matching URLs only, so try that first
  160.                 if ($router instanceof RequestMatcherInterface) {
  161.                     if (null === $requestForMatching) {
  162.                         $requestForMatching $this->rebuildRequest($pathinfo);
  163.                     }
  164.                     return $router->matchRequest($requestForMatching);
  165.                 }
  166.                 // every router implements the match method
  167.                 return $router->match($pathinfo);
  168.             } catch (ResourceNotFoundException $e) {
  169.                 if ($this->logger) {
  170.                     $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"');
  171.                 }
  172.                 // Needs special care
  173.             } catch (MethodNotAllowedException $e) {
  174.                 if ($this->logger) {
  175.                     $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"');
  176.                 }
  177.                 $methodNotAllowed $e;
  178.             }
  179.         }
  180.         $info $request
  181.             "this request\n$request"
  182.             "url '$pathinfo'";
  183.         throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info");
  184.     }
  185.     /**
  186.      * {@inheritdoc}
  187.      *
  188.      * Loops through all registered routers and returns a router if one is found.
  189.      * It will always return the first route generated.
  190.      */
  191.     public function generate($name$parameters = [], $absolute UrlGeneratorInterface::ABSOLUTE_PATH)
  192.     {
  193.         if (is_object($name)) {
  194.             @trigger_error('Passing an object as route name is deprecated since version 2.3 and will not work in Symfony 5.0. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`.'E_USER_DEPRECATED);
  195.         }
  196.         $debug = [];
  197.         foreach ($this->all() as $router) {
  198.             // if $router does not announce it is capable of handling
  199.             // non-string routes and $name is not a string, continue
  200.             if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
  201.                 continue;
  202.             }
  203.             // If $router is versatile and doesn't support this route name, continue
  204.             if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
  205.                 continue;
  206.             }
  207.             try {
  208.                 return $router->generate($name$parameters$absolute);
  209.             } catch (RouteNotFoundException $e) {
  210.                 $hint $this->getErrorMessage($name$router$parameters);
  211.                 $debug[] = $hint;
  212.                 if ($this->logger) {
  213.                     $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
  214.                 }
  215.             }
  216.         }
  217.         if ($debug) {
  218.             $debug array_unique($debug);
  219.             $info implode(', '$debug);
  220.         } else {
  221.             $info $this->getErrorMessage($name);
  222.         }
  223.         throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s'$info));
  224.     }
  225.     /**
  226.      * Rebuild the request object from a URL with the help of the RequestContext.
  227.      *
  228.      * If the request context is not set, this returns the request object built from $pathinfo.
  229.      *
  230.      * @param string $pathinfo
  231.      *
  232.      * @return Request
  233.      */
  234.     private function rebuildRequest($pathinfo)
  235.     {
  236.         $context $this->getContext();
  237.         $uri $pathinfo;
  238.         $server = [];
  239.         if ($context->getBaseUrl()) {
  240.             $uri $context->getBaseUrl().$pathinfo;
  241.             $server['SCRIPT_FILENAME'] = $context->getBaseUrl();
  242.             $server['PHP_SELF'] = $context->getBaseUrl();
  243.         }
  244.         $host $context->getHost() ?: 'localhost';
  245.         if ('https' === $context->getScheme() && 443 !== $context->getHttpsPort()) {
  246.             $host .= ':'.$context->getHttpsPort();
  247.         }
  248.         if ('http' === $context->getScheme() && 80 !== $context->getHttpPort()) {
  249.             $host .= ':'.$context->getHttpPort();
  250.         }
  251.         $uri $context->getScheme().'://'.$host.$uri.'?'.$context->getQueryString();
  252.         return Request::create($uri$context->getMethod(), $context->getParameters(), [], [], $server);
  253.     }
  254.     private function getErrorMessage($name$router null$parameters null)
  255.     {
  256.         if ($router instanceof VersatileGeneratorInterface) {
  257.             // the $parameters are not forced to be array, but versatile generator does typehint it
  258.             if (!is_array($parameters)) {
  259.                 $parameters = [];
  260.             }
  261.             $displayName $router->getRouteDebugMessage($name$parameters);
  262.         } elseif (is_object($name)) {
  263.             $displayName method_exists($name'__toString')
  264.                 ? (string) $name
  265.                 get_class($name)
  266.             ;
  267.         } else {
  268.             $displayName = (string) $name;
  269.         }
  270.         return "Route '$displayName' not found";
  271.     }
  272.     /**
  273.      * {@inheritdoc}
  274.      */
  275.     public function setContext(RequestContext $context)
  276.     {
  277.         foreach ($this->all() as $router) {
  278.             if ($router instanceof RequestContextAwareInterface) {
  279.                 $router->setContext($context);
  280.             }
  281.         }
  282.         $this->context $context;
  283.     }
  284.     /**
  285.      * {@inheritdoc}
  286.      *
  287.      * check for each contained router if it can warmup
  288.      */
  289.     public function warmUp($cacheDir)
  290.     {
  291.         foreach ($this->all() as $router) {
  292.             if ($router instanceof WarmableInterface) {
  293.                 $router->warmUp($cacheDir);
  294.             }
  295.         }
  296.     }
  297.     /**
  298.      * {@inheritdoc}
  299.      */
  300.     public function getRouteCollection()
  301.     {
  302.         if (!$this->routeCollection instanceof RouteCollection) {
  303.             $this->routeCollection = new ChainRouteCollection();
  304.             foreach ($this->all() as $router) {
  305.                 $this->routeCollection->addCollection($router->getRouteCollection());
  306.             }
  307.         }
  308.         return $this->routeCollection;
  309.     }
  310.     /**
  311.      * Identify if any routers have been added into the chain yet.
  312.      *
  313.      * @return bool
  314.      */
  315.     public function hasRouters()
  316.     {
  317.         return count($this->routers);
  318.     }
  319. }