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

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