Add RouteTable
This commit is contained in:
parent
e4ef1a8cb3
commit
918e33bd0a
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WellRESTed\Routing;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use WellRESTed\Routing\Route\PrefixRouteInterface;
|
||||||
|
use WellRESTed\Routing\Route\RouteInterface;
|
||||||
|
use WellRESTed\Routing\Route\StaticRouteInterface;
|
||||||
|
|
||||||
|
class RouteTable implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/** @var RouteInterface[] Array of Route objects */
|
||||||
|
private $routes;
|
||||||
|
/** @var array Hash array mapping exact paths to routes */
|
||||||
|
private $staticRoutes;
|
||||||
|
/** @var array Hash array mapping path prefixes to routes */
|
||||||
|
private $prefixRoutes;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->routes = [];
|
||||||
|
$this->staticRoutes = [];
|
||||||
|
$this->prefixRoutes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRoute(RouteInterface $route)
|
||||||
|
{
|
||||||
|
$this->routes[] = $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addStaticRoute(StaticRouteInterface $staticRoute)
|
||||||
|
{
|
||||||
|
$this->staticRoutes[$staticRoute->getPath()] = $staticRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPrefixRoute(PrefixRouteInterface $prefxRoute)
|
||||||
|
{
|
||||||
|
$this->prefixRoutes[$prefxRoute->getPrefix()] = $prefxRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(ServerRequestInterface $request, ResponseInterface &$response)
|
||||||
|
{
|
||||||
|
$requestTarget = $request->getRequestTarget();
|
||||||
|
|
||||||
|
$route = $this->getStaticRoute($requestTarget);
|
||||||
|
if ($route) {
|
||||||
|
$route->dispatch($request, $response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$route = $this->getPrefixRoute($requestTarget);
|
||||||
|
if ($route) {
|
||||||
|
$route->dispatch($request, $response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try each of the routes.
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
if ($route->matchesRequestTarget($requestTarget, $captures)) {
|
||||||
|
if (is_array($captures)) {
|
||||||
|
foreach ($captures as $key => $value) {
|
||||||
|
$request = $request->withAttribute($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$route->dispatch($request, $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WellRESTed\Test\Unit\Routing;
|
||||||
|
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use WellRESTed\Routing\Route\TemplateRoute;
|
||||||
|
use WellRESTed\Routing\RouteTable;
|
||||||
|
|
||||||
|
class RouteTableTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
private $request;
|
||||||
|
private $response;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->request = $this->prophesize("\\Psr\\Http\\Message\\ServerRequestInterface");
|
||||||
|
$this->response = $this->prophesize("\\Psr\\Http\\Message\\ResponseInterface");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesStaticRoute()
|
||||||
|
{
|
||||||
|
$route = $this->prophesize("\\WellRESTed\\Routing\\Route\\StaticRouteInterface");
|
||||||
|
$route->getPath()->willReturn("/cats/");
|
||||||
|
$route->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/cats/");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addStaticRoute($route->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route->dispatch($this->request->reveal(), $this->response->reveal())->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesPrefixRoute()
|
||||||
|
{
|
||||||
|
$route = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface");
|
||||||
|
$route->getPrefix()->willReturn("/cats/");
|
||||||
|
$route->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/cats/molly");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addPrefixRoute($route->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route->dispatch($this->request->reveal(), $this->response->reveal())->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesBestPrefixRoute()
|
||||||
|
{
|
||||||
|
$route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface");
|
||||||
|
$route1->getPrefix()->willReturn("/animals/");
|
||||||
|
$route1->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface");
|
||||||
|
$route2->getPrefix()->willReturn("/animals/cats/");
|
||||||
|
$route2->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addPrefixRoute($route1->reveal());
|
||||||
|
$table->addPrefixRoute($route2->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$route2->dispatch(Argument::cetera())->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesStaticRouteBeforePrefixRoute()
|
||||||
|
{
|
||||||
|
$route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface");
|
||||||
|
$route1->getPrefix()->willReturn("/animals/cats/");
|
||||||
|
$route1->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\StaticRouteInterface");
|
||||||
|
$route2->getPath()->willReturn("/animals/cats/molly");
|
||||||
|
$route2->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addPrefixRoute($route1->reveal());
|
||||||
|
$table->addStaticRoute($route2->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$route2->dispatch(Argument::cetera())->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMatchesPrefixRouteBeforeRoute()
|
||||||
|
{
|
||||||
|
$route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface");
|
||||||
|
$route1->getPrefix()->willReturn("/animals/cats/");
|
||||||
|
$route1->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface");
|
||||||
|
$route2->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addPrefixRoute($route1->reveal());
|
||||||
|
$table->addRoute($route2->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route1->dispatch(Argument::cetera())->shouldHaveBeenCalled();
|
||||||
|
$route2->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @uses WellRESTed\Routing\Route\TemplateRoute
|
||||||
|
* @uses WellRESTed\Routing\Route\RegexRoute
|
||||||
|
* @uses WellRESTed\Routing\Route\Route
|
||||||
|
*/
|
||||||
|
public function testAddsCapturesAsRequestAttributes()
|
||||||
|
{
|
||||||
|
// This test needs to read the result of the $captures parameter which
|
||||||
|
// is passed by reference. This is not so eary so mock, so the test
|
||||||
|
// will use an actual TemplateRoute.
|
||||||
|
|
||||||
|
$middleware = $this->prophesize("\\WellRESTed\\Routing\\MiddlewareInterface");
|
||||||
|
$route = new TemplateRoute("/cats/{id}", $middleware->reveal());
|
||||||
|
|
||||||
|
$this->request->withAttribute(Argument::cetera())->willReturn($this->request->reveal());
|
||||||
|
$this->request->getRequestTarget()->willReturn("/cats/molly");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addRoute($route);
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$this->request->withAttribute("id", "molly")->shouldHaveBeenCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDispatchedFirstMatchingRoute()
|
||||||
|
{
|
||||||
|
$route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface");
|
||||||
|
$route1->matchesRequestTarget(Argument::cetera())->willReturn(false);
|
||||||
|
|
||||||
|
$route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface");
|
||||||
|
$route2->matchesRequestTarget(Argument::cetera())->willReturn(true);
|
||||||
|
$route2->dispatch(Argument::cetera())->willReturn();
|
||||||
|
|
||||||
|
$route3 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface");
|
||||||
|
$route3->matchesRequestTarget(Argument::cetera())->willReturn(false);
|
||||||
|
|
||||||
|
$this->request->getRequestTarget()->willReturn("/");
|
||||||
|
|
||||||
|
$table = new RouteTable();
|
||||||
|
$table->addRoute($route1->reveal());
|
||||||
|
$table->addRoute($route2->reveal());
|
||||||
|
$table->addRoute($route3->reveal());
|
||||||
|
$table->dispatch($this->request->reveal(), $this->response->reveal());
|
||||||
|
|
||||||
|
$route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
$route2->dispatch(Argument::cetera())->shouldHaveBeenCalled();
|
||||||
|
$route3->dispatch(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue