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 LoggerInterface|null
  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.      * @param mixed $name
  189.      *
  190.      * The CMF routing system used to allow to pass route objects as $name to generate the route.
  191.      * Since Symfony 5.0, the UrlGeneratorInterface declares $name as string. We widen the contract
  192.      * for BC but deprecate passing non-strings.
  193.      * Instead, Pass the RouteObjectInterface::OBJECT_BASED_ROUTE_NAME as route name and the object
  194.      * in the parameters with key RouteObjectInterface::ROUTE_OBJECT.
  195.      *
  196.      * Loops through all registered routers and returns a router if one is found.
  197.      * It will always return the first route generated.
  198.      */
  199.     public function generate($name$parameters = [], $absolute UrlGeneratorInterface::ABSOLUTE_PATH)
  200.     {
  201.         if (is_object($name)) {
  202.             @trigger_error('Passing an object as route name is deprecated since version 2.3. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`.'E_USER_DEPRECATED);
  203.         }
  204.         $debug = [];
  205.         foreach ($this->all() as $router) {
  206.             // if $router does not announce it is capable of handling
  207.             // non-string routes and $name is not a string, continue
  208.             if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
  209.                 continue;
  210.             }
  211.             // If $router is versatile and doesn't support this route name, continue
  212.             if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
  213.                 continue;
  214.             }
  215.             try {
  216.                 return $router->generate($name$parameters$absolute);
  217.             } catch (RouteNotFoundException $e) {
  218.                 $hint $this->getErrorMessage($name$router$parameters);
  219.                 $debug[] = $hint;
  220.                 if ($this->logger) {
  221.                     $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
  222.                 }
  223.             }
  224.         }
  225.         if ($debug) {
  226.             $debug array_unique($debug);
  227.             $info implode(', '$debug);
  228.         } else {
  229.             $info $this->getErrorMessage($name);
  230.         }
  231.         throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s'$info));
  232.     }
  233.     /**
  234.      * Rebuild the request object from a URL with the help of the RequestContext.
  235.      *
  236.      * If the request context is not set, this returns the request object built from $pathinfo.
  237.      *
  238.      * @param string $pathinfo
  239.      *
  240.      * @return Request
  241.      */
  242.     private function rebuildRequest($pathinfo)
  243.     {
  244.         $context $this->getContext();
  245.         $uri $pathinfo;
  246.         $server = [];
  247.         if ($context->getBaseUrl()) {
  248.             $uri $context->getBaseUrl().$pathinfo;
  249.             $server['SCRIPT_FILENAME'] = $context->getBaseUrl();
  250.             $server['PHP_SELF'] = $context->getBaseUrl();
  251.         }
  252.         $host $context->getHost() ?: 'localhost';
  253.         if ('https' === $context->getScheme() && 443 !== $context->getHttpsPort()) {
  254.             $host .= ':'.$context->getHttpsPort();
  255.         }
  256.         if ('http' === $context->getScheme() && 80 !== $context->getHttpPort()) {
  257.             $host .= ':'.$context->getHttpPort();
  258.         }
  259.         $uri $context->getScheme().'://'.$host.$uri.'?'.$context->getQueryString();
  260.         return Request::create($uri$context->getMethod(), $context->getParameters(), [], [], $server);
  261.     }
  262.     private function getErrorMessage($name$router null$parameters null)
  263.     {
  264.         if ($router instanceof VersatileGeneratorInterface) {
  265.             // the $parameters are not forced to be array, but versatile generator does typehint it
  266.             if (!is_array($parameters)) {
  267.                 $parameters = [];
  268.             }
  269.             $displayName $router->getRouteDebugMessage($name$parameters);
  270.         } elseif (is_object($name)) {
  271.             $displayName method_exists($name'__toString')
  272.                 ? (string) $name
  273.                 get_class($name)
  274.             ;
  275.         } else {
  276.             $displayName = (string) $name;
  277.         }
  278.         return "Route '$displayName' not found";
  279.     }
  280.     /**
  281.      * {@inheritdoc}
  282.      */
  283.     public function setContext(RequestContext $context)
  284.     {
  285.         foreach ($this->all() as $router) {
  286.             if ($router instanceof RequestContextAwareInterface) {
  287.                 $router->setContext($context);
  288.             }
  289.         }
  290.         $this->context $context;
  291.     }
  292.     /**
  293.      * {@inheritdoc}
  294.      *
  295.      * check for each contained router if it can warmup
  296.      */
  297.     public function warmUp($cacheDir)
  298.     {
  299.         foreach ($this->all() as $router) {
  300.             if ($router instanceof WarmableInterface) {
  301.                 $router->warmUp($cacheDir);
  302.             }
  303.         }
  304.     }
  305.     /**
  306.      * {@inheritdoc}
  307.      */
  308.     public function getRouteCollection()
  309.     {
  310.         if (!$this->routeCollection instanceof RouteCollection) {
  311.             $this->routeCollection = new ChainRouteCollection();
  312.             foreach ($this->all() as $router) {
  313.                 $this->routeCollection->addCollection($router->getRouteCollection());
  314.             }
  315.         }
  316.         return $this->routeCollection;
  317.     }
  318.     /**
  319.      * Identify if any routers have been added into the chain yet.
  320.      *
  321.      * @return bool
  322.      */
  323.     public function hasRouters()
  324.     {
  325.         return count($this->routers);
  326.     }
  327. }