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