ChainRouter.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. /*
  3. * This file is part of the Symfony CMF package.
  4. *
  5. * (c) 2011-2015 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 Symfony\Component\Routing\RouterInterface;
  12. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  13. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  14. use Symfony\Component\Routing\RequestContext;
  15. use Symfony\Component\Routing\RequestContextAwareInterface;
  16. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  17. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  18. use Symfony\Component\Routing\Exception\MethodNotAllowedException;
  19. use Symfony\Component\Routing\RouteCollection;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  22. use Psr\Log\LoggerInterface;
  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 ChainRouterInterface, WarmableInterface
  30. {
  31. /**
  32. * @var RequestContext
  33. */
  34. private $context;
  35. /**
  36. * Array of arrays of routers grouped by priority.
  37. *
  38. * @var array
  39. */
  40. private $routers = array();
  41. /**
  42. * @var RouterInterface[] Array 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. return $this->context;
  66. }
  67. /**
  68. * {@inheritdoc}
  69. */
  70. public function add($router, $priority = 0)
  71. {
  72. if (!$router instanceof RouterInterface
  73. && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface)
  74. ) {
  75. throw new \InvalidArgumentException(sprintf('%s is not a valid router.', get_class($router)));
  76. }
  77. if (empty($this->routers[$priority])) {
  78. $this->routers[$priority] = array();
  79. }
  80. $this->routers[$priority][] = $router;
  81. $this->sortedRouters = array();
  82. }
  83. /**
  84. * {@inheritdoc}
  85. */
  86. public function all()
  87. {
  88. if (empty($this->sortedRouters)) {
  89. $this->sortedRouters = $this->sortRouters();
  90. // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
  91. // See https://github.com/symfony-cmf/Routing/pull/18
  92. $context = $this->getContext();
  93. if (null !== $context) {
  94. foreach ($this->sortedRouters as $router) {
  95. if ($router instanceof RequestContextAwareInterface) {
  96. $router->setContext($context);
  97. }
  98. }
  99. }
  100. }
  101. return $this->sortedRouters;
  102. }
  103. /**
  104. * Sort routers by priority.
  105. * The highest priority number is the highest priority (reverse sorting).
  106. *
  107. * @return RouterInterface[]
  108. */
  109. protected function sortRouters()
  110. {
  111. $sortedRouters = array();
  112. krsort($this->routers);
  113. foreach ($this->routers as $routers) {
  114. $sortedRouters = array_merge($sortedRouters, $routers);
  115. }
  116. return $sortedRouters;
  117. }
  118. /**
  119. * {@inheritdoc}
  120. *
  121. * Loops through all routes and tries to match the passed url.
  122. *
  123. * Note: You should use matchRequest if you can.
  124. */
  125. public function match($pathinfo)
  126. {
  127. return $this->doMatch($pathinfo);
  128. }
  129. /**
  130. * {@inheritdoc}
  131. *
  132. * Loops through all routes and tries to match the passed request.
  133. */
  134. public function matchRequest(Request $request)
  135. {
  136. return $this->doMatch($request->getPathInfo(), $request);
  137. }
  138. /**
  139. * Loops through all routers and tries to match the passed request or url.
  140. *
  141. * At least the url must be provided, if a request is additionally provided
  142. * the request takes precedence.
  143. *
  144. * @param string $pathinfo
  145. * @param Request $request
  146. *
  147. * @return array An array of parameters
  148. *
  149. * @throws ResourceNotFoundException If no router matched.
  150. */
  151. private function doMatch($pathinfo, Request $request = null)
  152. {
  153. $methodNotAllowed = null;
  154. $requestForMatching = $request;
  155. foreach ($this->all() as $router) {
  156. try {
  157. // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php
  158. // matching requests is more powerful than matching URLs only, so try that first
  159. if ($router instanceof RequestMatcherInterface) {
  160. if (empty($requestForMatching)) {
  161. $requestForMatching = $this->rebuildRequest($pathinfo);
  162. }
  163. return $router->matchRequest($requestForMatching);
  164. }
  165. // every router implements the match method
  166. return $router->match($pathinfo);
  167. } catch (ResourceNotFoundException $e) {
  168. if ($this->logger) {
  169. $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"');
  170. }
  171. // Needs special care
  172. } catch (MethodNotAllowedException $e) {
  173. if ($this->logger) {
  174. $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"');
  175. }
  176. $methodNotAllowed = $e;
  177. }
  178. }
  179. $info = $request
  180. ? "this request\n$request"
  181. : "url '$pathinfo'";
  182. throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info");
  183. }
  184. /**
  185. * {@inheritdoc}
  186. *
  187. * Loops through all registered routers and returns a router if one is found.
  188. * It will always return the first route generated.
  189. */
  190. public function generate($name, $parameters = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
  191. {
  192. $debug = array();
  193. foreach ($this->all() as $router) {
  194. // if $router does not announce it is capable of handling
  195. // non-string routes and $name is not a string, continue
  196. if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
  197. continue;
  198. }
  199. // If $router is versatile and doesn't support this route name, continue
  200. if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
  201. continue;
  202. }
  203. try {
  204. return $router->generate($name, $parameters, $absolute);
  205. } catch (RouteNotFoundException $e) {
  206. $hint = $this->getErrorMessage($name, $router, $parameters);
  207. $debug[] = $hint;
  208. if ($this->logger) {
  209. $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
  210. }
  211. }
  212. }
  213. if ($debug) {
  214. $debug = array_unique($debug);
  215. $info = implode(', ', $debug);
  216. } else {
  217. $info = $this->getErrorMessage($name);
  218. }
  219. throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info));
  220. }
  221. /**
  222. * Rebuild the request object from a URL with the help of the RequestContext.
  223. *
  224. * If the request context is not set, this simply returns the request object built from $uri.
  225. *
  226. * @param string $pathinfo
  227. *
  228. * @return Request
  229. */
  230. private function rebuildRequest($pathinfo)
  231. {
  232. if (!$this->context) {
  233. return Request::create('http://localhost'.$pathinfo);
  234. }
  235. $uri = $pathinfo;
  236. $server = array();
  237. if ($this->context->getBaseUrl()) {
  238. $uri = $this->context->getBaseUrl().$pathinfo;
  239. $server['SCRIPT_FILENAME'] = $this->context->getBaseUrl();
  240. $server['PHP_SELF'] = $this->context->getBaseUrl();
  241. }
  242. $host = $this->context->getHost() ?: 'localhost';
  243. if ('https' === $this->context->getScheme() && 443 !== $this->context->getHttpsPort()) {
  244. $host .= ':'.$this->context->getHttpsPort();
  245. }
  246. if ('http' === $this->context->getScheme() && 80 !== $this->context->getHttpPort()) {
  247. $host .= ':'.$this->context->getHttpPort();
  248. }
  249. $uri = $this->context->getScheme().'://'.$host.$uri.'?'.$this->context->getQueryString();
  250. return Request::create($uri, $this->context->getMethod(), $this->context->getParameters(), array(), array(), $server);
  251. }
  252. private function getErrorMessage($name, $router = null, $parameters = null)
  253. {
  254. if ($router instanceof VersatileGeneratorInterface) {
  255. $displayName = $router->getRouteDebugMessage($name, $parameters);
  256. } elseif (is_object($name)) {
  257. $displayName = method_exists($name, '__toString')
  258. ? (string) $name
  259. : get_class($name)
  260. ;
  261. } else {
  262. $displayName = (string) $name;
  263. }
  264. return "Route '$displayName' not found";
  265. }
  266. /**
  267. * {@inheritdoc}
  268. */
  269. public function setContext(RequestContext $context)
  270. {
  271. foreach ($this->all() as $router) {
  272. if ($router instanceof RequestContextAwareInterface) {
  273. $router->setContext($context);
  274. }
  275. }
  276. $this->context = $context;
  277. }
  278. /**
  279. * {@inheritdoc}
  280. *
  281. * check for each contained router if it can warmup
  282. */
  283. public function warmUp($cacheDir)
  284. {
  285. foreach ($this->all() as $router) {
  286. if ($router instanceof WarmableInterface) {
  287. $router->warmUp($cacheDir);
  288. }
  289. }
  290. }
  291. /**
  292. * {@inheritdoc}
  293. */
  294. public function getRouteCollection()
  295. {
  296. if (!$this->routeCollection instanceof RouteCollection) {
  297. $this->routeCollection = new ChainRouteCollection();
  298. foreach ($this->all() as $router) {
  299. $this->routeCollection->addCollection($router->getRouteCollection());
  300. }
  301. }
  302. return $this->routeCollection;
  303. }
  304. /**
  305. * Identify if any routers have been added into the chain yet.
  306. *
  307. * @return bool
  308. */
  309. public function hasRouters()
  310. {
  311. return !empty($this->routers);
  312. }
  313. }