vendor/symfony-cmf/routing/src/ChainRouter.php line 174

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of the Symfony CMF package.
  4. *
  5. * (c) 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 ChainRouterInterface, WarmableInterface
  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 LoggerInterface|null
  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 (0 === 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 (0 === 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($pathinfo, Request $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. * @param mixed $name
  189. *
  190. * The CMF routing system used to allow to pass route objects as $name to generate the route.
  191. * Since Symfony 5.0, the UrlGeneratorInterface declares $name as string. We widen the contract
  192. * for BC but deprecate passing non-strings.
  193. * Instead, Pass the RouteObjectInterface::OBJECT_BASED_ROUTE_NAME as route name and the object
  194. * in the parameters with key RouteObjectInterface::ROUTE_OBJECT.
  195. *
  196. * Loops through all registered routers and returns a router if one is found.
  197. * It will always return the first route generated.
  198. */
  199. public function generate($name, $parameters = [], $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
  200. {
  201. if (is_object($name)) {
  202. @trigger_error('Passing an object as route name is deprecated since version 2.3. Pass the `RouteObjectInterface::OBJECT_BASED_ROUTE_NAME` as route name and the object in the parameters with key `RouteObjectInterface::ROUTE_OBJECT`.', E_USER_DEPRECATED);
  203. }
  204. $debug = [];
  205. foreach ($this->all() as $router) {
  206. // if $router does not announce it is capable of handling
  207. // non-string routes and $name is not a string, continue
  208. if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
  209. continue;
  210. }
  211. // If $router is versatile and doesn't support this route name, continue
  212. if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
  213. continue;
  214. }
  215. try {
  216. return $router->generate($name, $parameters, $absolute);
  217. } catch (RouteNotFoundException $e) {
  218. $hint = $this->getErrorMessage($name, $router, $parameters);
  219. $debug[] = $hint;
  220. if ($this->logger) {
  221. $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
  222. }
  223. }
  224. }
  225. if ($debug) {
  226. $debug = array_unique($debug);
  227. $info = implode(', ', $debug);
  228. } else {
  229. $info = $this->getErrorMessage($name);
  230. }
  231. throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info));
  232. }
  233. /**
  234. * Rebuild the request object from a URL with the help of the RequestContext.
  235. *
  236. * If the request context is not set, this returns the request object built from $pathinfo.
  237. *
  238. * @param string $pathinfo
  239. *
  240. * @return Request
  241. */
  242. private function rebuildRequest($pathinfo)
  243. {
  244. $context = $this->getContext();
  245. $uri = $pathinfo;
  246. $server = [];
  247. if ($context->getBaseUrl()) {
  248. $uri = $context->getBaseUrl().$pathinfo;
  249. $server['SCRIPT_FILENAME'] = $context->getBaseUrl();
  250. $server['PHP_SELF'] = $context->getBaseUrl();
  251. }
  252. $host = $context->getHost() ?: 'localhost';
  253. if ('https' === $context->getScheme() && 443 !== $context->getHttpsPort()) {
  254. $host .= ':'.$context->getHttpsPort();
  255. }
  256. if ('http' === $context->getScheme() && 80 !== $context->getHttpPort()) {
  257. $host .= ':'.$context->getHttpPort();
  258. }
  259. $uri = $context->getScheme().'://'.$host.$uri.'?'.$context->getQueryString();
  260. return Request::create($uri, $context->getMethod(), $context->getParameters(), [], [], $server);
  261. }
  262. private function getErrorMessage($name, $router = null, $parameters = null)
  263. {
  264. if ($router instanceof VersatileGeneratorInterface) {
  265. // the $parameters are not forced to be array, but versatile generator does typehint it
  266. if (!is_array($parameters)) {
  267. $parameters = [];
  268. }
  269. $displayName = $router->getRouteDebugMessage($name, $parameters);
  270. } elseif (is_object($name)) {
  271. $displayName = method_exists($name, '__toString')
  272. ? (string) $name
  273. : get_class($name)
  274. ;
  275. } else {
  276. $displayName = (string) $name;
  277. }
  278. return "Route '$displayName' not found";
  279. }
  280. /**
  281. * {@inheritdoc}
  282. */
  283. public function setContext(RequestContext $context)
  284. {
  285. foreach ($this->all() as $router) {
  286. if ($router instanceof RequestContextAwareInterface) {
  287. $router->setContext($context);
  288. }
  289. }
  290. $this->context = $context;
  291. }
  292. /**
  293. * {@inheritdoc}
  294. *
  295. * check for each contained router if it can warmup
  296. */
  297. public function warmUp($cacheDir)
  298. {
  299. foreach ($this->all() as $router) {
  300. if ($router instanceof WarmableInterface) {
  301. $router->warmUp($cacheDir);
  302. }
  303. }
  304. }
  305. /**
  306. * {@inheritdoc}
  307. */
  308. public function getRouteCollection()
  309. {
  310. if (!$this->routeCollection instanceof RouteCollection) {
  311. $this->routeCollection = new ChainRouteCollection();
  312. foreach ($this->all() as $router) {
  313. $this->routeCollection->addCollection($router->getRouteCollection());
  314. }
  315. }
  316. return $this->routeCollection;
  317. }
  318. /**
  319. * Identify if any routers have been added into the chain yet.
  320. *
  321. * @return bool
  322. */
  323. public function hasRouters()
  324. {
  325. return 0 < count($this->routers);
  326. }
  327. }