* @author Magnus Nordlander */ class ChainRouter implements ChainRouterInterface, WarmableInterface { /** * @var RequestContext */ private $context; /** * Array of arrays of routers grouped by priority. * * @var array */ private $routers = array(); /** * @var RouterInterface[] Array of routers, sorted by priority */ private $sortedRouters; /** * @var RouteCollection */ private $routeCollection; /** * @var null|LoggerInterface */ protected $logger; /** * @param LoggerInterface $logger */ public function __construct(LoggerInterface $logger = null) { $this->logger = $logger; } /** * @return RequestContext */ public function getContext() { return $this->context; } /** * {@inheritdoc} */ public function add($router, $priority = 0) { if (!$router instanceof RouterInterface && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface) ) { throw new \InvalidArgumentException(sprintf('%s is not a valid router.', get_class($router))); } if (empty($this->routers[$priority])) { $this->routers[$priority] = array(); } $this->routers[$priority][] = $router; $this->sortedRouters = array(); } /** * {@inheritdoc} */ public function all() { if (empty($this->sortedRouters)) { $this->sortedRouters = $this->sortRouters(); // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches // See https://github.com/symfony-cmf/Routing/pull/18 $context = $this->getContext(); if (null !== $context) { foreach ($this->sortedRouters as $router) { if ($router instanceof RequestContextAwareInterface) { $router->setContext($context); } } } } return $this->sortedRouters; } /** * Sort routers by priority. * The highest priority number is the highest priority (reverse sorting). * * @return RouterInterface[] */ protected function sortRouters() { $sortedRouters = array(); krsort($this->routers); foreach ($this->routers as $routers) { $sortedRouters = array_merge($sortedRouters, $routers); } return $sortedRouters; } /** * {@inheritdoc} * * Loops through all routes and tries to match the passed url. * * Note: You should use matchRequest if you can. */ public function match($pathinfo) { return $this->doMatch($pathinfo); } /** * {@inheritdoc} * * Loops through all routes and tries to match the passed request. */ public function matchRequest(Request $request) { return $this->doMatch($request->getPathInfo(), $request); } /** * Loops through all routers and tries to match the passed request or url. * * At least the url must be provided, if a request is additionally provided * the request takes precedence. * * @param string $pathinfo * @param Request $request * * @return array An array of parameters * * @throws ResourceNotFoundException If no router matched. */ private function doMatch($pathinfo, Request $request = null) { $methodNotAllowed = null; $requestForMatching = $request; foreach ($this->all() as $router) { try { // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php // matching requests is more powerful than matching URLs only, so try that first if ($router instanceof RequestMatcherInterface) { if (empty($requestForMatching)) { $requestForMatching = $this->rebuildRequest($pathinfo); } return $router->matchRequest($requestForMatching); } // every router implements the match method return $router->match($pathinfo); } catch (ResourceNotFoundException $e) { if ($this->logger) { $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"'); } // Needs special care } catch (MethodNotAllowedException $e) { if ($this->logger) { $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"'); } $methodNotAllowed = $e; } } $info = $request ? "this request\n$request" : "url '$pathinfo'"; throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info"); } /** * {@inheritdoc} * * Loops through all registered routers and returns a router if one is found. * It will always return the first route generated. */ public function generate($name, $parameters = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH) { $debug = array(); foreach ($this->all() as $router) { // if $router does not announce it is capable of handling // non-string routes and $name is not a string, continue if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) { continue; } // If $router is versatile and doesn't support this route name, continue if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) { continue; } try { return $router->generate($name, $parameters, $absolute); } catch (RouteNotFoundException $e) { $hint = $this->getErrorMessage($name, $router, $parameters); $debug[] = $hint; if ($this->logger) { $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage()); } } } if ($debug) { $debug = array_unique($debug); $info = implode(', ', $debug); } else { $info = $this->getErrorMessage($name); } throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info)); } /** * Rebuild the request object from a URL with the help of the RequestContext. * * If the request context is not set, this simply returns the request object built from $uri. * * @param string $pathinfo * * @return Request */ private function rebuildRequest($pathinfo) { if (!$this->context) { return Request::create('http://localhost'.$pathinfo); } $uri = $pathinfo; $server = array(); if ($this->context->getBaseUrl()) { $uri = $this->context->getBaseUrl().$pathinfo; $server['SCRIPT_FILENAME'] = $this->context->getBaseUrl(); $server['PHP_SELF'] = $this->context->getBaseUrl(); } $host = $this->context->getHost() ?: 'localhost'; if ('https' === $this->context->getScheme() && 443 !== $this->context->getHttpsPort()) { $host .= ':'.$this->context->getHttpsPort(); } if ('http' === $this->context->getScheme() && 80 !== $this->context->getHttpPort()) { $host .= ':'.$this->context->getHttpPort(); } $uri = $this->context->getScheme().'://'.$host.$uri.'?'.$this->context->getQueryString(); return Request::create($uri, $this->context->getMethod(), $this->context->getParameters(), array(), array(), $server); } private function getErrorMessage($name, $router = null, $parameters = null) { if ($router instanceof VersatileGeneratorInterface) { $displayName = $router->getRouteDebugMessage($name, $parameters); } elseif (is_object($name)) { $displayName = method_exists($name, '__toString') ? (string) $name : get_class($name) ; } else { $displayName = (string) $name; } return "Route '$displayName' not found"; } /** * {@inheritdoc} */ public function setContext(RequestContext $context) { foreach ($this->all() as $router) { if ($router instanceof RequestContextAwareInterface) { $router->setContext($context); } } $this->context = $context; } /** * {@inheritdoc} * * check for each contained router if it can warmup */ public function warmUp($cacheDir) { foreach ($this->all() as $router) { if ($router instanceof WarmableInterface) { $router->warmUp($cacheDir); } } } /** * {@inheritdoc} */ public function getRouteCollection() { if (!$this->routeCollection instanceof RouteCollection) { $this->routeCollection = new ChainRouteCollection(); foreach ($this->all() as $router) { $this->routeCollection->addCollection($router->getRouteCollection()); } } return $this->routeCollection; } /** * Identify if any routers have been added into the chain yet. * * @return bool */ public function hasRouters() { return !empty($this->routers); } }