184 lines
6.1 KiB
PHP
184 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace WellRESTed\Routing;
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use WellRESTed\Dispatching\Dispatcher;
|
|
use WellRESTed\Dispatching\DispatcherInterface;
|
|
use WellRESTed\Routing\Route\RouteFactory;
|
|
use WellRESTed\Routing\Route\RouteFactoryInterface;
|
|
use WellRESTed\Routing\Route\RouteInterface;
|
|
|
|
class Router implements RouterInterface
|
|
{
|
|
/** @var string Key to ServerRequestInterface attribute for matched path variables */
|
|
public $pathVariablesAttributeKey = "pathVariables";
|
|
/** @var DispatcherInterface */
|
|
private $dispatcher;
|
|
/** @var RouteFactoryInterface */
|
|
private $factory;
|
|
/** @var RouteInterface[] Array of Route objects */
|
|
private $routes;
|
|
/** @var RouteInterface[] Hash array mapping exact paths to routes */
|
|
private $staticRoutes;
|
|
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
|
private $prefixRoutes;
|
|
/** @var RouteInterface[] Hash array mapping path prefixes to routes */
|
|
private $patternRoutes;
|
|
|
|
public function __construct(DispatcherInterface $dispatcher = null)
|
|
{
|
|
if ($dispatcher === null) {
|
|
$dispatcher = new Dispatcher();
|
|
}
|
|
$this->dispatcher = $dispatcher;
|
|
$this->factory = $this->getRouteFactory($this->dispatcher);
|
|
$this->routes = [];
|
|
$this->staticRoutes = [];
|
|
$this->prefixRoutes = [];
|
|
$this->patternRoutes = [];
|
|
}
|
|
|
|
/**
|
|
* Register middleware with the router for a given path and method.
|
|
*
|
|
* $method may be:
|
|
* - A single verb ("GET"),
|
|
* - A comma-separated list of verbs ("GET,PUT,DELETE")
|
|
* - "*" to indicate any method.
|
|
* @see MethodMapInterface::register
|
|
*
|
|
* $target may be:
|
|
* - An exact path (e.g., "/path/")
|
|
* - An prefix path ending with "*"" ("/path/*"")
|
|
* - A URI template with variables enclosed in "{}" ("/path/{id}")
|
|
* - A regular expression ("~/cat/([0-9]+)~")
|
|
*
|
|
* $middleware may be:
|
|
* - An instance implementing MiddlewareInterface
|
|
* - A string containing the fully qualified class name of a class
|
|
* implementing MiddlewareInterface
|
|
* - A callable that returns an instance implementing MiddleInterface
|
|
* - A callable maching the signature of MiddlewareInteraface::dispatch
|
|
* @see DispatchedInterface::dispatch
|
|
*
|
|
* @param string $target Request target or pattern to match
|
|
* @param string $method HTTP method(s) to match
|
|
* @param mixed $middleware Middleware to dispatch
|
|
* @return self
|
|
*/
|
|
public function register($method, $target, $middleware)
|
|
{
|
|
$route = $this->getRouteForTarget($target);
|
|
$route->getMethodMap()->register($method, $middleware);
|
|
return $this;
|
|
}
|
|
|
|
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
|
|
{
|
|
// Use only the path for routing.
|
|
$requestTarget = parse_url($request->getRequestTarget(), PHP_URL_PATH);
|
|
|
|
$route = $this->getStaticRoute($requestTarget);
|
|
if ($route) {
|
|
return $route($request, $response, $next);
|
|
}
|
|
|
|
$route = $this->getPrefixRoute($requestTarget);
|
|
if ($route) {
|
|
return $route($request, $response, $next);
|
|
}
|
|
|
|
// Try each of the routes.
|
|
foreach ($this->patternRoutes as $route) {
|
|
if ($route->matchesRequestTarget($requestTarget)) {
|
|
$pathVariables = $route->getPathVariables();
|
|
$request = $request->withAttribute($this->pathVariablesAttributeKey, $pathVariables);
|
|
return $route($request, $response, $next);
|
|
}
|
|
}
|
|
|
|
// If no route exists, set the status code of the response to 404.
|
|
return $next($request, $response->withStatus(404));
|
|
}
|
|
|
|
/**
|
|
* @param DispatcherInterface
|
|
* @return RouteFactoryInterface
|
|
*/
|
|
protected function getRouteFactory($dispatcher)
|
|
{
|
|
return new RouteFactory($dispatcher);
|
|
}
|
|
|
|
/**
|
|
* Return the route for a given target.
|
|
*
|
|
* @param $target
|
|
* @return RouteInterface
|
|
*/
|
|
private function getRouteForTarget($target)
|
|
{
|
|
if (isset($this->routes[$target])) {
|
|
$route = $this->routes[$target];
|
|
} else {
|
|
$route = $this->factory->create($target);
|
|
$this->registerRouteForTarget($route, $target);
|
|
}
|
|
return $route;
|
|
}
|
|
|
|
private function registerRouteForTarget($route, $target)
|
|
{
|
|
// Store the route to the hash indexed by original target.
|
|
$this->routes[$target] = $route;
|
|
|
|
// Store the route to the array of routes for its type.
|
|
switch ($route->getType()) {
|
|
case RouteInterface::TYPE_STATIC:
|
|
$this->staticRoutes[$route->getTarget()] = $route;
|
|
break;
|
|
case RouteInterface::TYPE_PREFIX:
|
|
$this->prefixRoutes[rtrim($route->getTarget(), "*")] = $route;
|
|
break;
|
|
case RouteInterface::TYPE_PATTERN:
|
|
$this->patternRoutes[] = $route;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private function getStaticRoute($requestTarget)
|
|
{
|
|
if (isset($this->staticRoutes[$requestTarget])) {
|
|
return $this->staticRoutes[$requestTarget];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function getPrefixRoute($requestTarget)
|
|
{
|
|
// Find all prefixes that match the start of this path.
|
|
$prefixes = array_keys($this->prefixRoutes);
|
|
$matches = array_filter(
|
|
$prefixes,
|
|
function ($prefix) use ($requestTarget) {
|
|
return (strrpos($requestTarget, $prefix, -strlen($requestTarget)) !== false);
|
|
}
|
|
);
|
|
|
|
if ($matches) {
|
|
if (count($matches) > 0) {
|
|
// If there are multiple matches, sort them to find the one with the longest string length.
|
|
$compareByLength = function ($a, $b) {
|
|
return strlen($b) - strlen($a);
|
|
};
|
|
usort($matches, $compareByLength);
|
|
}
|
|
$route = $this->prefixRoutes[$matches[0]];
|
|
return $route;
|
|
}
|
|
return null;
|
|
}
|
|
}
|