<?php
declare(strict_types=1);
namespace BestIt\Routing\Router;
use BestIt\Routing\Collection\ChainRouteCollection;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RequestContextAwareInterface;
use Symfony\Component\Routing\RouterInterface;
/**
* Chain router
*
* @author Michel Chowanski <michel.chowanski@bestit-online.de>
* @package BestIt\Routing\Router
*/
class ChainRouter implements ChainRouterInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* The request context
*
* @var RequestContext|null
*/
private ?RequestContext $context = null;
/**
* Array of arrays of routers grouped by priority.
*
* @var RouterInterface[][] Priority => RouterInterface[]
*/
private array $routers = [];
/**
* List of routers, sorted by priority
*
* @var RouterInterface[]
*/
private array $sortedRouters = [];
/**
* The chain route collection
*
* @var ChainRouteCollection|null
*/
private ?ChainRouteCollection $routeCollection = null;
/**
* ChainRouter constructor.
*/
public function __construct()
{
$this->setLogger(new NullLogger());
}
/**
* Add a new router to chain
*
* @param RouterInterface $router
* @param int $priority
*
* @return void
*/
#[\Override]
public function add(RouterInterface $router, int $priority = 0): void
{
$this->routers[$priority][] = $router;
$this->sortedRouters = [];
}
/**
* Get all routers sorted by priority
*
* @return RouterInterface[]|array
*/
#[\Override]
public function all(): array
{
if (count($this->sortedRouters) === 0 && count($this->routers) > 0) {
krsort($this->routers);
$this->sortedRouters = call_user_func_array('array_merge', $this->routers);
// setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
// See https://github.com/symfony-cmf/Routing/pull/18
if ($this->context !== null) {
foreach ($this->sortedRouters as $router) {
if ($router instanceof RequestContextAwareInterface) {
$router->setContext($this->context);
}
}
}
}
return $this->sortedRouters;
}
/**
* Set context to all routers
*
* @param RequestContext $context
*
* @return void
*/
#[\Override]
public function setContext(RequestContext $context): void
{
foreach ($this->all() as $router) {
if ($router instanceof RequestContextAwareInterface) {
$router->setContext($context);
}
}
$this->context = $context;
}
/**
* Get request context
*
* @return RequestContext
*/
#[\Override]
public function getContext(): RequestContext
{
if (!$this->context) {
$this->context = new RequestContext();
}
return $this->context;
}
/**
* Match route by request
*
* @param Request $request
*
* @return array
*/
#[\Override]
public function matchRequest(Request $request): array
{
return $this->doMatch($request->getPathInfo(), $request);
}
/**
* Match route by path info
*
* @param string $path
*
* @return array
*/
#[\Override]
public function match(string $path): array
{
return $this->doMatch($path);
}
/**
* Get chain route collection with all routes
*
* @return ChainRouteCollection|null
*/
#[\Override]
public function getRouteCollection(): ?ChainRouteCollection
{
if (!$this->routeCollection instanceof ChainRouteCollection) {
$this->routeCollection = new ChainRouteCollection();
foreach ($this->all() as $router) {
$this->routeCollection->addCollection($router->getRouteCollection());
}
}
return $this->routeCollection;
}
/**
* Generate route by all routers
*
* @param string $name
* @param array $parameters
* @param int $referenceType
*
* @return string
*/
#[\Override]
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
foreach ($this->all() as $router) {
if (!$router instanceof UrlGeneratorInterface) {
continue;
}
try {
return $router->generate($name, $parameters, $referenceType);
} catch (RouteNotFoundException) {
$this->logger->debug('Router ' . $router::class . ' was unable to generate route.');
}
}
throw new RouteNotFoundException('None of the chained routers were able to generate a route.');
}
/**
* Warmup all warmable routers
*
* @param string $cacheDirectory
*
* @return void
*/
#[\Override]
public function warmUp(string $cacheDirectory): void
{
foreach ($this->all() as $router) {
if ($router instanceof WarmableInterface) {
$router->warmUp($cacheDirectory);
}
}
}
/**
* Loops through all routers and tries to match the passed request or url.
*
* At least the url must be provided, if a request is additionally provided
* the request takes precedence.
*
* @throws ResourceNotFoundException If no router matched
*
* @param string $path
* @param Request $request
*
* @return array An array of parameters
*/
private function doMatch(string $path, Request $request = null): array
{
foreach ($this->all() as $router) {
try {
// the request/url match logic is the same as in Symfony/.../RouterListener.php
// matching requests is more powerful than matching URLs only, so try that first
if ($router instanceof RequestMatcherInterface && $request !== null) {
return $router->matchRequest($request);
}
return $router->match($path);
} catch (ResourceNotFoundException) {
$this->logger->debug('Router ' . $router::class . ' was not able to match.');
}
}
throw new ResourceNotFoundException('None of the routers in the chain matched.');
}
}