Refactor Router to use RouteTables

This commit is contained in:
PJ Dietz 2015-02-21 08:10:58 -05:00
parent 14195355e3
commit 1c82908eeb
2 changed files with 134 additions and 312 deletions

View File

@ -14,8 +14,6 @@ use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface;
use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface;
use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface;
use pjdietz\WellRESTed\Routes\PrefixRoute;
use pjdietz\WellRESTed\Routes\StaticRoute;
@ -26,14 +24,8 @@ use pjdietz\WellRESTed\Routes\StaticRoute;
*/
class Router implements HandlerInterface
{
/** @var array Array of Route objects */
private $routes;
/** @var array Hash array mapping path prefixes to routes */
private $prefixRoutes;
/** @var array Hash array mapping exact paths to routes */
private $staticRoutes;
/** @var array Hash array HTTP verb => RouteTable */
private $routeTables;
/** @var array Hash array of status code => error handler */
private $errorHandlers;
@ -41,74 +33,10 @@ class Router implements HandlerInterface
/** Create a new Router. */
public function __construct()
{
$this->routes = array();
$this->prefixRoutes = array();
$this->staticRoutes = array();
$this->routeTables = array();
$this->errorHandlers = array();
}
/**
* Return the response built by the handler based on the request
*
* @param RequestInterface $request
* @param array|null $args
* @return ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$response = $this->getResponseFromRoutes($request, $args);
if ($response) {
// Check if the router has an error handler for this status code.
$status = $response->getStatusCode();
$errorResponse = $this->getErrorResponse($status, $request, $args, $response);
if ($errorResponse) {
return $errorResponse;
}
}
return $response;
}
/**
* Append a new route to the route table.
*
* @param HandlerInterface $route
*/
public function addRoute(HandlerInterface $route)
{
if ($route instanceof StaticRouteInterface) {
$this->addStaticRoute($route);
} elseif ($route instanceof PrefixRouteInterface) {
$this->addPrefixRoute($route);
} else {
$this->routes[] = $route;
}
}
/**
* Append a series of routes.
*
* @param array $routes List array of HandlerInterface instances
*/
public function addRoutes(array $routes)
{
foreach ($routes as $route) {
if ($route instanceof HandlerInterface) {
$this->addRoute($route);
}
}
}
/**
* Add a custom error handler.
*
* @param integer $statusCode The error status code
* @param callable|string|HandlerInterface $errorHandler
*/
public function setErrorHandler($statusCode, $errorHandler)
{
$this->errorHandlers[$statusCode] = $errorHandler;
}
/**
* Add custom error handlers.
*
@ -121,6 +49,17 @@ class Router implements HandlerInterface
}
}
/**
* Add a custom error handler.
*
* @param integer $statusCode The error status code
* @param callable|string|HandlerInterface $errorHandler
*/
public function setErrorHandler($statusCode, $errorHandler)
{
$this->errorHandlers[$statusCode] = $errorHandler;
}
/**
* Dispatch the singleton Request through the router and output the response.
*
@ -137,6 +76,27 @@ class Router implements HandlerInterface
$response->respond();
}
/**
* Return the response built by the handler based on the request
*
* @param RequestInterface $request
* @param array|null $args
* @return ResponseInterface
*/
public function getResponse(RequestInterface $request, array $args = null)
{
$response = $this->getResponseFromRouteTables($request, $args);
if ($response) {
// Check if the router has an error handler for this status code.
$status = $response->getStatusCode();
$errorResponse = $this->getErrorResponse($status, $request, $args, $response);
if ($errorResponse) {
return $errorResponse;
}
}
return $response;
}
/**
* Prepare a response indicating a 404 Not Found error
*
@ -155,18 +115,20 @@ class Router implements HandlerInterface
return $response;
}
private function addStaticRoute(StaticRouteInterface $staticRoute)
private function getResponseFromRouteTables(RequestInterface $request, array $args = null)
{
foreach ($staticRoute->getPaths() as $path) {
$this->staticRoutes[$path] = $staticRoute;
}
$method = $request->getMethod();
if (isset($this->routeTables[$method])) {
$table = $this->routeTables[$method];
return $this->tryResponse($table, $request, $args);
}
private function addPrefixRoute(PrefixRouteInterface $prefixRoute)
{
foreach ($prefixRoute->getPrefixes() as $prefix) {
$this->prefixRoutes[$prefix] = $prefixRoute;
if (isset($this->routeTables["*"])) {
$table = $this->routeTables["*"];
return $this->tryResponse($table, $request, $args);
}
return null;
}
private function getErrorResponse($status, $request, $args = null, $response = null)
@ -184,79 +146,6 @@ class Router implements HandlerInterface
return null;
}
/**
* Returning the handler associated with the matching static route, or null if none match.
*
* @param $path string The request's path
* @return HandlerInterface|null
*/
private function getStaticHandler($path)
{
if (isset($this->staticRoutes[$path])) {
$route = $this->staticRoutes[$path];
return $route->getHandler();
}
return null;
}
/**
* Returning the best-matching prefix handler, or null if none match.
*
* @param $path string The request's path
* @return HandlerInterface|null
*/
private function getPrefixHandler($path)
{
// Find all prefixes that match the start of this path.
$prefixes = array_keys($this->prefixRoutes);
$matches = array_filter($prefixes, function ($prefix) use ($path) {
return (strrpos($path, $prefix, -strlen($path)) !== false);
});
if ($matches) {
// If there are multiple matches, sort them to find the one with the longest string length.
if (count($matches) > 0) {
usort($matches, function ($a, $b) {
return strlen($b) - strlen($a);
});
}
// Instantiate and return the handler identified as the best match.
$route = $this->prefixRoutes[$matches[0]];
return $route->getHandler();
}
return null;
}
private function getResponseFromRoutes(RequestInterface $request, array $args = null)
{
$response = null;
$path = $request->getPath();
// First check if there is a handler for this exact path.
$handler = $this->getStaticHandler($path);
if ($handler) {
return $this->tryResponse($handler, $request, $args);
}
// Check prefix routes for any routes that match. Use the longest matching prefix.
$handler = $this->getPrefixHandler($path);
if ($handler) {
return $this->tryResponse($handler, $request, $args);
}
// Try each of the routes.
foreach ($this->routes as $route) {
/** @var HandlerInterface $route */
$response = $this->tryResponse($route, $request, $args);
if ($response) {
return $response;
}
}
return null;
}
/**
* Wraps the getResponse method in a try-catch.
*
@ -281,27 +170,47 @@ class Router implements HandlerInterface
return $response;
}
////////////////
// Deprecated //
////////////////
/**
* @deprecated Use {@see addRoute} instead.
* @see addRoute
*/
public function setPrefixRoute($prefixes, $handler)
{
$this->addPrefixRoute(new PrefixRoute($prefixes, $handler));
$this->addRoute(new PrefixRoute($prefixes, $handler));
trigger_error("Router::setPrefixRoute is deprecated. Use addRoute", E_USER_DEPRECATED);
}
/**
* Append a new route to the route table.
*
* @param HandlerInterface $route
* @param string $method HTTP Method; * for any
*/
public function addRoute(HandlerInterface $route, $method = "*")
{
$table = $this->getRouteTable($method);
$table->addRoute($route);
}
////////////////
// Deprecated //
////////////////
private function getRouteTable($method = "*")
{
if (!isset($this->routeTables[$method])) {
$this->routeTables[$method] = new RouteTable();
}
return $this->routeTables[$method];
}
/**
* @deprecated Use {@see addRoute} instead.
* @see addRoute
*/
public function setStaticRoute($paths, $handler)
{
$this->addStaticRoute(new StaticRoute($paths, $handler));
$this->addRoute(new StaticRoute($paths, $handler));
trigger_error("Router::setStaticRoute is deprecated. Use addRoute", E_USER_DEPRECATED);
}
}

View File

@ -19,178 +19,92 @@ class RouterTest extends \PHPUnit_Framework_TestCase
public function setUp()
{
$this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface");
$this->request->getPath()->willReturn("/");
$this->request->getMethod()->willReturn("GET");
$this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface");
$this->route = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
}
public function testMatchesStaticRoute()
/**
* @dataProvider httpMethodProvider
*/
public function testAddsRouteToDefaultRouteTable($method)
{
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
$this->route->getPaths()->willReturn(["/cats/"]);
$this->route->getHandler()->willReturn($this->handler->reveal());
$this->request->getPath()->willReturn("/cats/");
$router = new Router();
$router->addRoute($this->route->reveal());
$router->getResponse($this->request->reveal());
$this->route->getHandler()->shouldHaveBeenCalled();
}
public function testMatchesPrefixRoute()
{
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
$this->route->getPrefixes()->willReturn(["/cats/"]);
$this->route->getHandler()->willReturn($this->handler->reveal());
$this->request->getPath()->willReturn("/cats/molly");
$router = new Router();
$router->addRoute($this->route->reveal());
$router->getResponse($this->request->reveal());
$this->route->getHandler()->shouldHaveBeenCalled();
}
public function testMatchesBestPrefixRoute()
{
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
$route1->getPrefixes()->willReturn(["/animals/"]);
$route1->getHandler()->willReturn($this->handler->reveal());
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
$route2->getPrefixes()->willReturn(["/animals/cats/"]);
$route2->getHandler()->willReturn($this->handler->reveal());
$this->request->getPath()->willReturn("/animals/cats/molly");
$router = new Router();
$router->addRoute($route1->reveal());
$router->addRoute($route2->reveal());
$router->getResponse($this->request->reveal());
$route1->getHandler()->shouldNotHaveBeenCalled();
$route2->getHandler()->shouldHaveBeenCalled();
}
public function testMatchesStaticRouteBeforePrefixRoute()
{
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
$route1->getPrefixes()->willReturn(["/animals/cats/"]);
$route1->getHandler()->willReturn($this->handler->reveal());
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface");
$route2->getPaths()->willReturn(["/animals/cats/molly"]);
$route2->getHandler()->willReturn($this->handler->reveal());
$this->request->getPath()->willReturn("/animals/cats/molly");
$router = new Router();
$router->addRoute($route1->reveal());
$router->addRoute($route2->reveal());
$router->getResponse($this->request->reveal());
$route1->getHandler()->shouldNotHaveBeenCalled();
$route2->getHandler()->shouldHaveBeenCalled();
}
public function testMatchesPrefixRouteBeforeHandlerRoute()
{
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface");
$route1->getPrefixes()->willReturn(["/animals/cats/"]);
$route1->getHandler()->willReturn($this->handler->reveal());
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route2->getResponse(Argument::cetera())->willReturn(null);
$this->request->getPath()->willReturn("/animals/cats/molly");
$router = new Router();
$router->addRoute($route1->reveal());
$router->addRoute($route2->reveal());
$router->getResponse($this->request->reveal());
$route1->getHandler()->shouldHaveBeenCalled();
$route2->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
}
public function testReturnsFirstNonNullResponse()
{
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route1->getResponse(Argument::cetera())->willReturn(null);
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route2->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route3->getResponse(Argument::cetera())->willReturn(null);
$this->request->getPath()->willReturn("/");
$this->request->getMethod()->willReturn($method);
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$router = new Router();
$router->addRoutes(
[
$route1->reveal(),
$route2->reveal(),
$route3->reveal()
]
);
$router->addRoute($this->route->reveal());
$response = $router->getResponse($this->request->reveal());
$this->assertNotNull($response);
$route1->getResponse(Argument::cetera())->shouldHaveBeenCalled();
$route2->getResponse(Argument::cetera())->shouldHaveBeenCalled();
$route3->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
}
public function testReturnsNullWhenNoRouteMatches()
public function httpMethodProvider()
{
$route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route1->getResponse(Argument::cetera())->willReturn(null);
return [
["GET"],
["POST"],
["PUT"],
["DELETE"],
["HEAD"],
["PATCH"],
["OPTIONS"],
["CUSTOM"]
];
}
$route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route2->getResponse(Argument::cetera())->willReturn(null);
$route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$route3->getResponse(Argument::cetera())->willReturn(null);
/**
* @dataProvider httpMethodListProvider
*/
public function testAddsRouteToSpecificRouteTable($registerMethod, $requestMethod, $expectedResponse)
{
$this->request->getPath()->willReturn("/");
$this->request->getMethod()->willReturn($requestMethod);
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$router = new Router();
$router->addRoutes(
[
$route1->reveal(),
$route2->reveal(),
$route3->reveal()
]
);
$router->addRoute($this->route->reveal(), $registerMethod);
$response = $router->getResponse($this->request->reveal());
$this->assertNull($response);
$route1->getResponse(Argument::cetera())->shouldHaveBeenCalled();
$route2->getResponse(Argument::cetera())->shouldHaveBeenCalled();
$route3->getResponse(Argument::cetera())->shouldHaveBeenCalled();
$this->assertEquals($expectedResponse, !is_null($response));
}
public function httpMethodListProvider()
{
return [
["GET", "GET", true],
["POST", "GET", false]
];
}
/**
* @dataProvider httpMethodProvider
*/
public function testMatchSpecificTableBeforeDefaultTable($method)
{
$this->request->getMethod()->willReturn("POST");
$this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal());
$genericRoute = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$genericRoute->getResponse()->willReturn(null);
$specificRoute = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface");
$specificRoute->getResponse(Argument::cetera())->willReturn(null);
$router = new Router();
$router->addRoute($genericRoute->reveal());
$router->addRoute($specificRoute->reveal(), "POST");
$router->getResponse($this->request->reveal());
$genericRoute->getResponse(Argument::cetera())->shouldNotHaveBeenCalled();
$specificRoute->getResponse(Argument::cetera())->shouldHaveBeenCalled();
}
public function testRespondsWithErrorResponseForHttpException()
{
$this->route->getResponse(Argument::cetera())->willThrow(new HttpException());
$this->request->getPath()->willReturn("/");
$router = new Router();
$router->addRoute($this->route->reveal());
@ -200,7 +114,6 @@ class RouterTest extends \PHPUnit_Framework_TestCase
public function testDispatchesErrorHandlerForStatusCode()
{
$this->request->getPath()->willReturn("/");
$this->response->getStatusCode()->willReturn(403);
$this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal());
@ -300,7 +213,7 @@ class RouterTest extends \PHPUnit_Framework_TestCase
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testRoutesStaticRequest()
public function testRoutesServerRequest()
{
$_SERVER["REQUEST_URI"] = "/cats/";
$_SERVER["HTTP_HOST"] = "localhost";