4 * This file is part of the Symfony CMF package.
6 * (c) 2011-2015 Symfony CMF
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Cmf\Component\Routing;
14 use Symfony\Component\Routing\RouterInterface;
15 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
17 use Symfony\Component\Routing\RequestContext;
18 use Symfony\Component\Routing\RequestContextAwareInterface;
19 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
20 use Symfony\Component\Routing\Exception\RouteNotFoundException;
21 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
22 use Symfony\Component\Routing\RouteCollection;
23 use Symfony\Component\HttpFoundation\Request;
24 use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
25 use Psr\Log\LoggerInterface;
28 * The ChainRouter allows to combine several routers to try in a defined order.
30 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
31 * @author Magnus Nordlander <magnus@e-butik.se>
33 class ChainRouter implements ChainRouterInterface, WarmableInterface
41 * Array of arrays of routers grouped by priority.
45 private $routers = array();
48 * @var RouterInterface[] Array of routers, sorted by priority
50 private $sortedRouters;
53 * @var RouteCollection
55 private $routeCollection;
58 * @var null|LoggerInterface
63 * @param LoggerInterface $logger
65 public function __construct(LoggerInterface $logger = null)
67 $this->logger = $logger;
71 * @return RequestContext
73 public function getContext()
75 return $this->context;
81 public function add($router, $priority = 0)
83 if (!$router instanceof RouterInterface
84 && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface)
86 throw new \InvalidArgumentException(sprintf('%s is not a valid router.', get_class($router)));
88 if (empty($this->routers[$priority])) {
89 $this->routers[$priority] = array();
92 $this->routers[$priority][] = $router;
93 $this->sortedRouters = array();
101 if (empty($this->sortedRouters)) {
102 $this->sortedRouters = $this->sortRouters();
104 // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
105 // See https://github.com/symfony-cmf/Routing/pull/18
106 $context = $this->getContext();
107 if (null !== $context) {
108 foreach ($this->sortedRouters as $router) {
109 if ($router instanceof RequestContextAwareInterface) {
110 $router->setContext($context);
116 return $this->sortedRouters;
120 * Sort routers by priority.
121 * The highest priority number is the highest priority (reverse sorting).
123 * @return RouterInterface[]
125 protected function sortRouters()
127 $sortedRouters = array();
128 krsort($this->routers);
130 foreach ($this->routers as $routers) {
131 $sortedRouters = array_merge($sortedRouters, $routers);
134 return $sortedRouters;
140 * Loops through all routes and tries to match the passed url.
142 * Note: You should use matchRequest if you can.
144 public function match($pathinfo)
146 return $this->doMatch($pathinfo);
152 * Loops through all routes and tries to match the passed request.
154 public function matchRequest(Request $request)
156 return $this->doMatch($request->getPathInfo(), $request);
160 * Loops through all routers and tries to match the passed request or url.
162 * At least the url must be provided, if a request is additionally provided
163 * the request takes precedence.
165 * @param string $pathinfo
166 * @param Request $request
168 * @return array An array of parameters
170 * @throws ResourceNotFoundException If no router matched.
172 private function doMatch($pathinfo, Request $request = null)
174 $methodNotAllowed = null;
176 $requestForMatching = $request;
177 foreach ($this->all() as $router) {
179 // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php
180 // matching requests is more powerful than matching URLs only, so try that first
181 if ($router instanceof RequestMatcherInterface) {
182 if (empty($requestForMatching)) {
183 $requestForMatching = $this->rebuildRequest($pathinfo);
186 return $router->matchRequest($requestForMatching);
189 // every router implements the match method
190 return $router->match($pathinfo);
191 } catch (ResourceNotFoundException $e) {
193 $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"');
195 // Needs special care
196 } catch (MethodNotAllowedException $e) {
198 $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"');
200 $methodNotAllowed = $e;
205 ? "this request\n$request"
207 throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info");
213 * Loops through all registered routers and returns a router if one is found.
214 * It will always return the first route generated.
216 public function generate($name, $parameters = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
220 foreach ($this->all() as $router) {
221 // if $router does not announce it is capable of handling
222 // non-string routes and $name is not a string, continue
223 if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
227 // If $router is versatile and doesn't support this route name, continue
228 if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
233 return $router->generate($name, $parameters, $absolute);
234 } catch (RouteNotFoundException $e) {
235 $hint = $this->getErrorMessage($name, $router, $parameters);
238 $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
244 $debug = array_unique($debug);
245 $info = implode(', ', $debug);
247 $info = $this->getErrorMessage($name);
250 throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info));
254 * Rebuild the request object from a URL with the help of the RequestContext.
256 * If the request context is not set, this simply returns the request object built from $uri.
258 * @param string $pathinfo
262 private function rebuildRequest($pathinfo)
264 if (!$this->context) {
265 return Request::create('http://localhost'.$pathinfo);
271 if ($this->context->getBaseUrl()) {
272 $uri = $this->context->getBaseUrl().$pathinfo;
273 $server['SCRIPT_FILENAME'] = $this->context->getBaseUrl();
274 $server['PHP_SELF'] = $this->context->getBaseUrl();
276 $host = $this->context->getHost() ?: 'localhost';
277 if ('https' === $this->context->getScheme() && 443 !== $this->context->getHttpsPort()) {
278 $host .= ':'.$this->context->getHttpsPort();
280 if ('http' === $this->context->getScheme() && 80 !== $this->context->getHttpPort()) {
281 $host .= ':'.$this->context->getHttpPort();
283 $uri = $this->context->getScheme().'://'.$host.$uri.'?'.$this->context->getQueryString();
285 return Request::create($uri, $this->context->getMethod(), $this->context->getParameters(), array(), array(), $server);
288 private function getErrorMessage($name, $router = null, $parameters = null)
290 if ($router instanceof VersatileGeneratorInterface) {
291 $displayName = $router->getRouteDebugMessage($name, $parameters);
292 } elseif (is_object($name)) {
293 $displayName = method_exists($name, '__toString')
298 $displayName = (string) $name;
301 return "Route '$displayName' not found";
307 public function setContext(RequestContext $context)
309 foreach ($this->all() as $router) {
310 if ($router instanceof RequestContextAwareInterface) {
311 $router->setContext($context);
315 $this->context = $context;
321 * check for each contained router if it can warmup
323 public function warmUp($cacheDir)
325 foreach ($this->all() as $router) {
326 if ($router instanceof WarmableInterface) {
327 $router->warmUp($cacheDir);
335 public function getRouteCollection()
337 if (!$this->routeCollection instanceof RouteCollection) {
338 $this->routeCollection = new ChainRouteCollection();
339 foreach ($this->all() as $router) {
340 $this->routeCollection->addCollection($router->getRouteCollection());
344 return $this->routeCollection;
348 * Identify if any routers have been added into the chain yet.
352 public function hasRouters()
354 return !empty($this->routers);