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