vendor/best-it/routing-bundle/src/Router/ChainRouter.php line 196

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace BestIt\Routing\Router;
  4. use BestIt\Routing\Collection\ChainRouteCollection;
  5. use Psr\Log\LoggerAwareInterface;
  6. use Psr\Log\LoggerAwareTrait;
  7. use Psr\Log\NullLogger;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
  10. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  11. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  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\RouterInterface;
  17. /**
  18. * Chain router
  19. *
  20. * @author Michel Chowanski <michel.chowanski@bestit-online.de>
  21. * @package BestIt\Routing\Router
  22. */
  23. class ChainRouter implements ChainRouterInterface, LoggerAwareInterface
  24. {
  25. use LoggerAwareTrait;
  26. /**
  27. * The request context
  28. *
  29. * @var RequestContext|null
  30. */
  31. private ?RequestContext $context = null;
  32. /**
  33. * Array of arrays of routers grouped by priority.
  34. *
  35. * @var RouterInterface[][] Priority => RouterInterface[]
  36. */
  37. private array $routers = [];
  38. /**
  39. * List of routers, sorted by priority
  40. *
  41. * @var RouterInterface[]
  42. */
  43. private array $sortedRouters = [];
  44. /**
  45. * The chain route collection
  46. *
  47. * @var ChainRouteCollection|null
  48. */
  49. private ?ChainRouteCollection $routeCollection = null;
  50. /**
  51. * ChainRouter constructor.
  52. */
  53. public function __construct()
  54. {
  55. $this->setLogger(new NullLogger());
  56. }
  57. /**
  58. * Add a new router to chain
  59. *
  60. * @param RouterInterface $router
  61. * @param int $priority
  62. *
  63. * @return void
  64. */
  65. #[\Override]
  66. public function add(RouterInterface $router, int $priority = 0): void
  67. {
  68. $this->routers[$priority][] = $router;
  69. $this->sortedRouters = [];
  70. }
  71. /**
  72. * Get all routers sorted by priority
  73. *
  74. * @return RouterInterface[]|array
  75. */
  76. #[\Override]
  77. public function all(): array
  78. {
  79. if (count($this->sortedRouters) === 0 && count($this->routers) > 0) {
  80. krsort($this->routers);
  81. $this->sortedRouters = call_user_func_array('array_merge', $this->routers);
  82. // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
  83. // See https://github.com/symfony-cmf/Routing/pull/18
  84. if ($this->context !== null) {
  85. foreach ($this->sortedRouters as $router) {
  86. if ($router instanceof RequestContextAwareInterface) {
  87. $router->setContext($this->context);
  88. }
  89. }
  90. }
  91. }
  92. return $this->sortedRouters;
  93. }
  94. /**
  95. * Set context to all routers
  96. *
  97. * @param RequestContext $context
  98. *
  99. * @return void
  100. */
  101. #[\Override]
  102. public function setContext(RequestContext $context): void
  103. {
  104. foreach ($this->all() as $router) {
  105. if ($router instanceof RequestContextAwareInterface) {
  106. $router->setContext($context);
  107. }
  108. }
  109. $this->context = $context;
  110. }
  111. /**
  112. * Get request context
  113. *
  114. * @return RequestContext
  115. */
  116. #[\Override]
  117. public function getContext(): RequestContext
  118. {
  119. if (!$this->context) {
  120. $this->context = new RequestContext();
  121. }
  122. return $this->context;
  123. }
  124. /**
  125. * Match route by request
  126. *
  127. * @param Request $request
  128. *
  129. * @return array
  130. */
  131. #[\Override]
  132. public function matchRequest(Request $request): array
  133. {
  134. return $this->doMatch($request->getPathInfo(), $request);
  135. }
  136. /**
  137. * Match route by path info
  138. *
  139. * @param string $path
  140. *
  141. * @return array
  142. */
  143. #[\Override]
  144. public function match(string $path): array
  145. {
  146. return $this->doMatch($path);
  147. }
  148. /**
  149. * Get chain route collection with all routes
  150. *
  151. * @return ChainRouteCollection|null
  152. */
  153. #[\Override]
  154. public function getRouteCollection(): ?ChainRouteCollection
  155. {
  156. if (!$this->routeCollection instanceof ChainRouteCollection) {
  157. $this->routeCollection = new ChainRouteCollection();
  158. foreach ($this->all() as $router) {
  159. $this->routeCollection->addCollection($router->getRouteCollection());
  160. }
  161. }
  162. return $this->routeCollection;
  163. }
  164. /**
  165. * Generate route by all routers
  166. *
  167. * @param string $name
  168. * @param array $parameters
  169. * @param int $referenceType
  170. *
  171. * @return string
  172. */
  173. #[\Override]
  174. public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
  175. {
  176. foreach ($this->all() as $router) {
  177. if (!$router instanceof UrlGeneratorInterface) {
  178. continue;
  179. }
  180. try {
  181. return $router->generate($name, $parameters, $referenceType);
  182. } catch (RouteNotFoundException) {
  183. $this->logger->debug('Router ' . $router::class . ' was unable to generate route.');
  184. }
  185. }
  186. throw new RouteNotFoundException('None of the chained routers were able to generate a route.');
  187. }
  188. /**
  189. * Warmup all warmable routers
  190. *
  191. * @param string $cacheDirectory
  192. *
  193. * @return void
  194. */
  195. #[\Override]
  196. public function warmUp(string $cacheDirectory): void
  197. {
  198. foreach ($this->all() as $router) {
  199. if ($router instanceof WarmableInterface) {
  200. $router->warmUp($cacheDirectory);
  201. }
  202. }
  203. }
  204. /**
  205. * Loops through all routers and tries to match the passed request or url.
  206. *
  207. * At least the url must be provided, if a request is additionally provided
  208. * the request takes precedence.
  209. *
  210. * @throws ResourceNotFoundException If no router matched
  211. *
  212. * @param string $path
  213. * @param Request $request
  214. *
  215. * @return array An array of parameters
  216. */
  217. private function doMatch(string $path, Request $request = null): array
  218. {
  219. foreach ($this->all() as $router) {
  220. try {
  221. // the request/url match logic is the same as in Symfony/.../RouterListener.php
  222. // matching requests is more powerful than matching URLs only, so try that first
  223. if ($router instanceof RequestMatcherInterface && $request !== null) {
  224. return $router->matchRequest($request);
  225. }
  226. return $router->match($path);
  227. } catch (ResourceNotFoundException) {
  228. $this->logger->debug('Router ' . $router::class . ' was not able to match.');
  229. }
  230. }
  231. throw new ResourceNotFoundException('None of the routers in the chain matched.');
  232. }
  233. }