Remove Router and rename RouteMap to Router

Remove Router
Remove RouterInterface
Rename RouteMapInterface to RouterInterface
Rename RouteMap to Router
Rename add() to register()
Make register fluid
This commit is contained in:
PJ Dietz 2015-05-09 20:20:43 -04:00
parent b0db3cbcdd
commit 2adcbd8636
5 changed files with 367 additions and 870 deletions

View File

@ -1,167 +0,0 @@
<?php
namespace WellRESTed\Routing;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\Routing\Route\RouteFactory;
use WellRESTed\Routing\Route\RouteFactoryInterface;
use WellRESTed\Routing\Route\RouteInterface;
class RouteMap implements RouteMapInterface
{
/** @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()
{
$this->factory = $this->getRouteFactory();
$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
*/
public function add($method, $target, $middleware)
{
$route = $this->getRouteForTarget($target);
$route->getMethodMap()->register($method, $middleware);
}
public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$requestTarget = $request->getRequestTarget();
$route = $this->getStaticRoute($requestTarget);
if ($route) {
return $route->dispatch($request, $response, $next);
}
$route = $this->getPrefixRoute($requestTarget);
if ($route) {
return $route->dispatch($request, $response, $next);
}
// Try each of the routes.
foreach ($this->patternRoutes as $route) {
if ($route->matchesRequestTarget($requestTarget)) {
return $route->dispatch($request, $response, $next);
}
}
// If no route exists, set the status code of the response to 404.
return $next($request, $response->withStatus(404));
}
/**
* @return RouteFactoryInterface
*/
protected function getRouteFactory()
{
return new RouteFactory();
}
/**
* 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;
}
}

View File

@ -4,65 +4,32 @@ namespace WellRESTed\Routing;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\HttpExceptions\HttpException;
use WellRESTed\Message\Response;
use WellRESTed\Message\ServerRequest;
use WellRESTed\Message\Stream;
use WellRESTed\Routing\Hook\ContentLengthHook;
use WellRESTed\Routing\Hook\HeadHook;
use WellRESTed\Routing\Route\RouteFactory;
use WellRESTed\Routing\Route\RouteFactoryInterface;
use WellRESTed\Routing\Route\RouteInterface;
class Router implements MiddlewareInterface, RouteMapInterface
class Router implements RouterInterface
{
/** @var DispatcherInterface */
private $dispatcher;
/** @var mixed[] List of middleware to dispatch immediately before concluding the request-response cycle. */
private $finalizationHooks;
/** @var mixed[] List of middleware to dispatch after the router dispatches the matched route. */
private $postRouteHooks;
/** @var mixed[] List of middleware to dispatch before the router dispatches the matched route. */
private $preRouteHooks;
/** @var array Hash array of status code => middleware */
private $statusHooks;
/** @var RouteMapInterface */
private $routeMap;
// ------------------------------------------------------------------------
/** @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()
{
$this->dispatcher = $this->getDispatcher();
$this->finalizationHooks = $this->getFinalizationHooks();
$this->postRouteHooks = $this->getPostRouteHooks();
$this->preRouteHooks = $this->getPreRouteHooks();
$this->statusHooks = $this->getStatusHooks();
$this->routeMap = $this->getRouteMap();
$this->factory = $this->getRouteFactory();
$this->routes = [];
$this->staticRoutes = [];
$this->prefixRoutes = [];
$this->patternRoutes = [];
}
// ------------------------------------------------------------------------
// MiddlewareInterface
public function dispatch(ServerRequestInterface $request, ResponseInterface &$response)
{
$this->dispatchPreRouteHooks($request, $response);
try {
$this->routeMap->dispatch($request, $response);
} catch (HttpException $e) {
$response = $response->withStatus($e->getCode());
$response = $response->withBody(new Stream($e->getMessage()));
}
$this->dispatchStatusHooks($request, $response);
$this->dispatchPostRouteHooks($request, $response);
$this->dispatchFinalizationHooks($request, $response);
}
// ------------------------------------------------------------------------
// RouteMapInterface
/**
* Register middleware with the router for a given path and method.
*
@ -89,167 +56,114 @@ class Router implements MiddlewareInterface, RouteMapInterface
* @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 add($method, $target, $middleware)
public function register($method, $target, $middleware)
{
$this->routeMap->add($method, $target, $middleware);
$route = $this->getRouteForTarget($target);
$route->getMethodMap()->register($method, $middleware);
return $this;
}
// ------------------------------------------------------------------------
public function respond()
public function dispatch(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$request = $this->getRequest();
$response = $this->getResponse();
$this->dispatch($request, $response);
$responder = $this->getResponder();
$responder->respond($response);
$requestTarget = $request->getRequestTarget();
$route = $this->getStaticRoute($requestTarget);
if ($route) {
return $route->dispatch($request, $response, $next);
}
// ------------------------------------------------------------------------
// Hooks
public function addPreRouteHook($middleware)
{
$this->preRouteHooks[] = $middleware;
$route = $this->getPrefixRoute($requestTarget);
if ($route) {
return $route->dispatch($request, $response, $next);
}
public function addPostRouteHook($middleware)
{
$this->postRouteHooks[] = $middleware;
// Try each of the routes.
foreach ($this->patternRoutes as $route) {
if ($route->matchesRequestTarget($requestTarget)) {
return $route->dispatch($request, $response, $next);
}
}
public function addFinalizationHook($middleware)
{
$this->finalizationHooks[] = $middleware;
// If no route exists, set the status code of the response to 404.
return $next($request, $response->withStatus(404));
}
public function setStatusHook($statusCode, $middleware)
{
$this->statusHooks[(int) $statusCode] = $middleware;
}
// ------------------------------------------------------------------------
// The following methods provide instaces the router will use. Override
// to provide custom classes or configured instances.
/**
* Return an instance that can dispatch middleware.
* @return RouteFactoryInterface
*/
protected function getRouteFactory()
{
return new RouteFactory();
}
/**
* Return the route for a given target.
*
* Override to provide a custom class.
*
* @return DispatcherInterface
* @param $target
* @return RouteInterface
*/
protected function getDispatcher()
private function getRouteForTarget($target)
{
return new Dispatcher();
if (isset($this->routes[$target])) {
$route = $this->routes[$target];
} else {
$route = $this->factory->create($target);
$this->registerRouteForTarget($route, $target);
}
return $route;
}
/**
* Return an instance that maps routes to middleware.
*
* Override to provide a custom class.
*
* @return RouteMapInterface
*/
protected function getRouteMap()
private function registerRouteForTarget($route, $target)
{
return new RouteMap();
// 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;
}
}
/**
* @return array
*/
protected function getPreRouteHooks()
private function getStaticRoute($requestTarget)
{
return [];
if (isset($this->staticRoutes[$requestTarget])) {
return $this->staticRoutes[$requestTarget];
}
return null;
}
/**
* @return array
*/
protected function getPostRouteHooks()
private function getPrefixRoute($requestTarget)
{
return [];
// 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);
}
);
/**
* @return array
*/
protected function getStatusHooks()
{
return [];
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);
}
/**
* @return array
*/
protected function getFinalizationHooks()
{
return [
new ContentLengthHook(),
new HeadHook()
];
}
// @codeCoverageIgnoreStart
/**
* @return ServerRequestInterface
*/
protected function getRequest()
{
return ServerRequest::getServerRequest();
}
/**
* @return ResponderInterface
*/
protected function getResponder()
{
return new Responder();
}
/**
* @return ResponseInterface
*/
protected function getResponse()
{
return new Response();
}
// @codeCoverageIgnoreEnd
// ------------------------------------------------------------------------
private function dispatchPreRouteHooks(ServerRequestInterface $request, ResponseInterface &$response)
{
foreach ($this->preRouteHooks as $hook) {
$this->dispatcher->dispatch($hook, $request, $response);
}
}
private function dispatchPostRouteHooks(ServerRequestInterface $request, ResponseInterface &$response)
{
foreach ($this->postRouteHooks as $hook) {
$this->dispatcher->dispatch($hook, $request, $response);
}
}
private function dispatchFinalizationHooks(ServerRequestInterface $request, ResponseInterface &$response)
{
foreach ($this->finalizationHooks as $hook) {
$this->dispatcher->dispatch($hook, $request, $response);
}
}
private function dispatchStatusHooks(ServerRequestInterface $request, ResponseInterface &$response)
{
$statusCode = (int) $response->getStatusCode();
if (isset($this->statusHooks[$statusCode])) {
$middleware = $this->statusHooks[$statusCode];
$dispatcher = $this->getDispatcher();
$dispatcher->dispatch($middleware, $request, $response);
$route = $this->prefixRoutes[$matches[0]];
return $route;
}
return null;
}
}

View File

@ -2,7 +2,7 @@
namespace WellRESTed\Routing;
interface RouteMapInterface extends MiddlewareInterface
interface RouterInterface extends MiddlewareInterface
{
/**
* Register middleware with the router for a given path and method.
@ -30,6 +30,7 @@ interface RouteMapInterface extends MiddlewareInterface
* @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 add($method, $target, $middleware);
public function register($method, $target, $middleware);
}

View File

@ -1,222 +0,0 @@
<?php
namespace WellRESTed\Test\Unit\Routing;
use Prophecy\Argument;
use WellRESTed\Routing\Route\RouteInterface;
use WellRESTed\Routing\RouteMap;
// Test dispatch orders (static before prefix, prefix before pattern)
// Test dispatches first matching pattern route
/**
* @coversDefaultClass WellRESTed\Routing\RouteMap
* @uses WellRESTed\Routing\RouteMap
*/
class RouteMapTest extends \PHPUnit_Framework_TestCase
{
private $methodMap;
private $factory;
private $request;
private $response;
private $route;
private $routeMap;
private $next;
public function setUp()
{
parent::setUp();
$this->methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
$this->methodMap->register(Argument::cetera());
$this->route = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$this->route->dispatch(Argument::cetera())->willReturn();
$this->route->getMethodMap()->willReturn($this->methodMap->reveal());
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
$this->route->getTarget()->willReturn("/");
$this->factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactory');
$this->factory->create(Argument::any())->willReturn($this->route->reveal());
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
$this->next = function ($request, $response) {
return $response;
};
$this->routeMap = $this->getMockBuilder('WellRESTed\Routing\RouteMap')
->setMethods(["getRouteFactory"])
->disableOriginalConstructor()
->getMock();
$this->routeMap->expects($this->any())
->method("getRouteFactory")
->will($this->returnValue($this->factory->reveal()));
$this->routeMap->__construct();
}
// ------------------------------------------------------------------------
// Construction
/**
* @covers ::__construct
* @covers ::getRouteFactory
*/
public function testCreatesInstance()
{
$routeMap = new RouteMap();
$this->assertNotNull($routeMap);
}
// ------------------------------------------------------------------------
// Populating
/**
* @covers ::add
* @covers ::getRouteForTarget
* @covers ::registerRouteForTarget
*/
public function testAddCreatesRouteForTarget()
{
$this->routeMap->add("GET", "/", "middleware");
$this->factory->create("/")->shouldHaveBeenCalled();
}
/**
* @covers ::add
* @covers ::getRouteForTarget
*/
public function testAddDoesNotRecreateRouteForExistingTarget()
{
$this->routeMap->add("GET", "/", "middleware");
$this->routeMap->add("POST", "/", "middleware");
$this->factory->create("/")->shouldHaveBeenCalledTimes(1);
}
/**
* @covers ::add
*/
public function testAddPassesMethodAndMiddlewareToMethodMap()
{
$this->routeMap->add("GET", "/", "middleware");
$this->methodMap->register("GET", "middleware")->shouldHaveBeenCalled();
}
// ------------------------------------------------------------------------
// Dispatching
/**
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::registerRouteForTarget
*/
public function testDispatchesStaticRoute()
{
$target = "/";
$this->request->getRequestTarget()->willReturn($target);
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
$this->routeMap->add("GET", $target, "middleware");
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::getPrefixRoute
* @covers ::registerRouteForTarget
*/
public function testDispatchesPrefixRoute()
{
$target = "/animals/cats/*";
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$this->routeMap->add("GET", $target, "middleware");
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::getPrefixRoute
*/
public function testDispatchesLongestMatchingPrefixRoute()
{
$routeAnimals = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$routeAnimals->getMethodMap()->willReturn($this->methodMap->reveal());
$routeAnimals->getTarget()->willReturn("/animals/*");
$routeAnimals->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$routeAnimals->dispatch(Argument::cetera())->willReturn();
$routeCats = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$routeCats->getMethodMap()->willReturn($this->methodMap->reveal());
$routeCats->getTarget()->willReturn("/animals/cats/*");
$routeCats->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$routeCats->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
$this->factory->create("/animals/*")->willReturn($routeAnimals->reveal());
$this->factory->create("/animals/cats/*")->willReturn($routeCats->reveal());
$this->routeMap->add("GET", "/animals/*", "middleware");
$this->routeMap->add("GET", "/animals/cats/*", "middleware");
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$routeCats->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::registerRouteForTarget
*/
public function testDispatchesPatternRoute()
{
$target = "/";
$this->request->getRequestTarget()->willReturn($target);
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
$this->routeMap->add("GET", $target, "middleware");
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::getPrefixRoute
*/
public function testResponds404WhenNoRouteMatches()
{
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->response->withStatus(404)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::getPrefixRoute
*/
public function testCallsNextWhenNoRouteMatches()
{
$calledNext = false;
$next = function ($request, $response) use (&$calledNext) {
$calledNext = true;
return $response;
};
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
$this->routeMap->dispatch($this->request->reveal(), $this->response->reveal(), $next);
$this->assertTrue($calledNext);
}
}

View File

@ -3,48 +3,52 @@
namespace WellRESTed\Test\Unit\Routing;
use Prophecy\Argument;
use WellRESTed\HttpExceptions\NotFoundException;
use WellRESTed\Routing\Route\RouteInterface;
use WellRESTed\Routing\Router;
// TODO Tests that ensure hooks are called at correct times
/**
* @coversDefaultClass WellRESTed\Routing\Router
* @uses WellRESTed\Routing\Router
* @uses WellRESTed\Message\Stream
* @uses WellRESTed\Routing\Dispatcher
* @uses WellRESTed\Routing\RouteMap
* @uses WellRESTed\Routing\Hook\ContentLengthHook
* @uses WellRESTed\Routing\Hook\HeadHook
*/
class RouterTest extends \PHPUnit_Framework_TestCase
{
private $methodMap;
private $factory;
private $request;
private $response;
private $routeMap;
private $route;
private $router;
private $next;
public function setUp()
{
parent::setUp();
$this->methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface');
$this->methodMap->register(Argument::cetera());
$this->route = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$this->route->dispatch(Argument::cetera())->willReturn();
$this->route->getMethodMap()->willReturn($this->methodMap->reveal());
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
$this->route->getTarget()->willReturn("/");
$this->factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactory');
$this->factory->create(Argument::any())->willReturn($this->route->reveal());
$this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface');
$this->response = $this->prophesize('Psr\Http\Message\ResponseInterface');
$this->response->hasHeader("Content-length")->willReturn(true);
$this->response->getStatusCode()->willReturn(200);
$this->routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface');
$this->routeMap->add(Argument::cetera())->willReturn();
$this->routeMap->dispatch(Argument::cetera())->willReturn();
$this->next = function ($request, $response) {
return $response;
};
$this->router = $this->getMockBuilder('WellRESTed\Routing\Router')
->setMethods(["getRouteMap"])
->setMethods(["getRouteFactory"])
->disableOriginalConstructor()
->getMock();
$this->router->expects($this->any())
->method("getRouteMap")
->will($this->returnValue($this->routeMap->reveal()));
->method("getRouteFactory")
->will($this->returnValue($this->factory->reveal()));
$this->router->__construct();
}
@ -53,329 +57,296 @@ class RouterTest extends \PHPUnit_Framework_TestCase
/**
* @covers ::__construct
* @covers ::getDispatcher
* @covers ::getRouteMap
* @covers ::getPreRouteHooks
* @covers ::getPostRouteHooks
* @covers ::getFinalizationHooks
* @covers ::getStatusHooks
* @covers ::getRouteFactory
*/
public function testCreatesInstance()
{
$router = new Router();
$this->assertNotNull($router);
$routeMap = new Router();
$this->assertNotNull($routeMap);
}
// ------------------------------------------------------------------------
// Routes
// Populating
/**
* @covers ::add
* @covers ::register
* @covers ::getRouteForTarget
* @covers ::registerRouteForTarget
*/
public function testAddRegistersRouteWithRouteMap()
public function testCreatesRouteForTarget()
{
$method = "GET";
$target = "/path/{id}";
$middleware = "Middleware";
$this->router->add($method, $target, $middleware);
$this->routeMap->add($method, $target, $middleware)->shouldHaveBeenCalled();
$this->router->register("GET", "/", "middleware");
$this->factory->create("/")->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::register
* @covers ::getRouteForTarget
*/
public function testDispatchesRouteMap()
public function testDoesNotRecreateRouteForExistingTarget()
{
$request = $this->request->reveal();
$resonse = $this->response->reveal();
$this->router->dispatch($request, $resonse);
$this->routeMap->dispatch($request, Argument::any())->shouldHaveBeenCalled();
$this->router->register("GET", "/", "middleware");
$this->router->register("POST", "/", "middleware");
$this->factory->create("/")->shouldHaveBeenCalledTimes(1);
}
/**
* @covers ::register
*/
public function testPassesMethodAndMiddlewareToMethodMap()
{
$this->router->register("GET", "/", "middleware");
$this->methodMap->register("GET", "middleware")->shouldHaveBeenCalled();
}
// ------------------------------------------------------------------------
// Hooks
// Dispatching
/**
* @covers ::addPreRouteHook
* @covers ::dispatchPreRouteHooks
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::registerRouteForTarget
*/
public function testDispatchesPreRouteHooks()
public function testDispatchesStaticRoute()
{
$hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$hook->dispatch(Argument::cetera())->willReturn();
$target = "/";
$this->router->addPreRouteHook($hook->reveal());
$this->request->getRequestTarget()->willReturn($target);
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_STATIC);
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$this->router->register("GET", $target, "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$hook->dispatch(Argument::cetera())->shouldHaveBeenCalled();
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::dispatchStatusHooks
* @covers ::setStatusHook
* @covers ::getPrefixRoute
* @covers ::registerRouteForTarget
*/
public function testDispatchesHookForStatusCode()
public function testDispatchesPrefixRoute()
{
$this->response->getStatusCode()->willReturn(403);
$target = "/animals/cats/*";
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$statusMiddleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$statusMiddleware->dispatch(Argument::cetera())->willReturn();
$this->router->register("GET", $target, "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->router->setStatusHook(403, $statusMiddleware->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$statusMiddleware->dispatch(Argument::cetera())->shouldHaveBeenCalled();
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::dispatch
* @covers ::dispatchStatusHooks
* @covers ::setStatusHook
* @covers ::registerRouteForTarget
*/
public function testConvertsHttpExceptionToResponse()
public function testDispatchesPatternRoute()
{
$statusMiddleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$statusMiddleware->dispatch(Argument::cetera())->willReturn();
$target = "/";
$this->routeMap->dispatch(Argument::cetera())->willThrow(new NotFoundException());
$this->request->getRequestTarget()->willReturn($target);
$this->route->getTarget()->willReturn($target);
$this->route->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$this->route->matchesRequestTarget(Argument::cetera())->willReturn(true);
$this->response->withStatus(Argument::any())->will(
function ($args) {
$this->getStatusCode()->willReturn($args[0]);
return $this;
$this->router->register("GET", $target, "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
);
$this->response->withBody(Argument::any())->willReturn($this->response->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
/**
* @coversNothing
*/
public function testDispatchesStaticRouteBeforePrefixRoute()
{
$staticRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$staticRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$staticRoute->getTarget()->willReturn("/cats/");
$staticRoute->getType()->willReturn(RouteInterface::TYPE_STATIC);
$staticRoute->dispatch(Argument::cetera())->willReturn();
$prefixRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$prefixRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$prefixRoute->getTarget()->willReturn("/cats/*");
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$prefixRoute->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/cats/");
$this->factory->create("/cats/")->willReturn($staticRoute->reveal());
$this->factory->create("/cats/*")->willReturn($prefixRoute->reveal());
$this->router->register("GET", "/cats/", "middleware");
$this->router->register("GET", "/cats/*", "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$staticRoute->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @covers ::getPrefixRoute
*/
public function testDispatchesLongestMatchingPrefixRoute()
{
// Note: The longest route is also good for 2 points in Settlers of Catan.
$shortRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$shortRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$shortRoute->getTarget()->willReturn("/animals/*");
$shortRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$shortRoute->dispatch(Argument::cetera())->willReturn();
$longRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$longRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$longRoute->getTarget()->willReturn("/animals/cats/*");
$longRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$longRoute->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/animals/cats/molly");
$this->factory->create("/animals/*")->willReturn($shortRoute->reveal());
$this->factory->create("/animals/cats/*")->willReturn($longRoute->reveal());
$this->router->register("GET", "/animals/*", "middleware");
$this->router->register("GET", "/animals/cats/*", "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$longRoute->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @coversNothing
*/
public function testDispatchesPrefixRouteBeforePatternRoute()
{
$prefixRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$prefixRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$prefixRoute->getTarget()->willReturn("/cats/*");
$prefixRoute->getType()->willReturn(RouteInterface::TYPE_PREFIX);
$prefixRoute->dispatch(Argument::cetera())->willReturn();
$patternRoute = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$patternRoute->getMethodMap()->willReturn($this->methodMap->reveal());
$patternRoute->getTarget()->willReturn("/cats/{id}");
$patternRoute->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$patternRoute->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/cats/");
$this->factory->create("/cats/*")->willReturn($prefixRoute->reveal());
$this->factory->create("/cats/{id}")->willReturn($patternRoute->reveal());
$this->router->register("GET", "/cats/*", "middleware");
$this->router->register("GET", "/cats/{id}", "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$prefixRoute->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @coversNothing
*/
public function testDispatchesFirstMatchingPatternRoute()
{
$patternRoute1 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$patternRoute1->getMethodMap()->willReturn($this->methodMap->reveal());
$patternRoute1->getTarget()->willReturn("/cats/{id}");
$patternRoute1->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
$patternRoute1->dispatch(Argument::cetera())->willReturn();
$patternRoute2 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$patternRoute2->getMethodMap()->willReturn($this->methodMap->reveal());
$patternRoute2->getTarget()->willReturn("/cats/{name}");
$patternRoute2->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
$patternRoute2->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/cats/molly");
$this->factory->create("/cats/{id}")->willReturn($patternRoute1->reveal());
$this->factory->create("/cats/{name}")->willReturn($patternRoute2->reveal());
$this->router->register("GET", "/cats/{id}", "middleware");
$this->router->register("GET", "/cats/{name}", "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$patternRoute1->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled();
}
/**
* @coversNothing
*/
public function testStopsTestingPatternsAfterFirstSuccessfulMatch()
{
$patternRoute1 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$patternRoute1->getMethodMap()->willReturn($this->methodMap->reveal());
$patternRoute1->getTarget()->willReturn("/cats/{id}");
$patternRoute1->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$patternRoute1->matchesRequestTarget(Argument::any())->willReturn(true);
$patternRoute1->dispatch(Argument::cetera())->willReturn();
$patternRoute2 = $this->prophesize('WellRESTed\Routing\Route\RouteInterface');
$patternRoute2->getMethodMap()->willReturn($this->methodMap->reveal());
$patternRoute2->getTarget()->willReturn("/cats/{name}");
$patternRoute2->getType()->willReturn(RouteInterface::TYPE_PATTERN);
$patternRoute2->matchesRequestTarget(Argument::any())->willReturn(true);
$patternRoute2->dispatch(Argument::cetera())->willReturn();
$this->request->getRequestTarget()->willReturn("/cats/molly");
$this->factory->create("/cats/{id}")->willReturn($patternRoute1->reveal());
$this->factory->create("/cats/{name}")->willReturn($patternRoute2->reveal());
$this->router->register("GET", "/cats/{id}", "middleware");
$this->router->register("GET", "/cats/{name}", "middleware");
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$patternRoute2->matchesRequestTarget(Argument::any())->shouldNotHaveBeenCalled();
}
// ------------------------------------------------------------------------
// No Matching Routes
/**
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::getPrefixRoute
*/
public function testResponds404WhenNoRouteMatches()
{
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next);
$this->response->withStatus(404)->shouldHaveBeenCalled();
}
/**
* @covers ::addPostRouteHook
* @covers ::dispatchPostRouteHooks
* @covers ::dispatch
* @covers ::getStaticRoute
* @covers ::getPrefixRoute
*/
public function testDispatchesPostRouteHooks()
public function testCallsNextWhenNoRouteMatches()
{
$hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$hook->dispatch(Argument::cetera())->willReturn();
$calledNext = false;
$next = function ($request, $response) use (&$calledNext) {
$calledNext = true;
return $response;
};
$this->router->addPostRouteHook($hook->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$hook->dispatch(Argument::cetera())->shouldHaveBeenCalled();
$this->response->withStatus(Argument::any())->willReturn($this->response->reveal());
$this->router->dispatch($this->request->reveal(), $this->response->reveal(), $next);
$this->assertTrue($calledNext);
}
/**
* @covers ::addFinalizationHook
* @covers ::dispatchFinalizationHooks
*/
public function testDispatchesFinalizationHooks()
public function testRegisterIsFluid()
{
$hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$hook->dispatch(Argument::cetera())->willReturn();
$this->router->addFinalizationHook($hook->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$hook->dispatch(Argument::cetera())->shouldHaveBeenCalled();
}
/**
* @coversNothing
*/
public function testDispatchesMiddlewareInCorrectSequence()
{
// Each middleware will push a value onto this array.
$stack = [];
$response = $this->response;
$this->routeMap->dispatch(Argument::cetera())->will(function () use ($response, &$stack) {
$stack[] = "routeMap";
$response->getStatusCode()->willReturn(404);
});
$this->router->addPreRouteHook($this->createStackHook("pre1", $stack));
$this->router->addPreRouteHook($this->createStackHook("pre2", $stack));
$this->router->addPostRouteHook($this->createStackHook("post1", $stack));
$this->router->addPostRouteHook($this->createStackHook("post2", $stack));
$this->router->addFinalizationHook($this->createStackHook("final1", $stack));
$this->router->addFinalizationHook($this->createStackHook("final2", $stack));
$this->router->setStatusHook(404, $this->createStackHook("404", $stack));
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$this->assertEquals(["pre1","pre2","routeMap","404","post1","post2","final1","final2"], $stack);
}
private function createStackHook($name, &$stack)
{
$hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$hook->dispatch(Argument::cetera())->will(function () use ($name, &$stack) {
$stack[] = $name;
});
return $hook->reveal();
}
/**
* @coversNothing
*/
public function testProvidesContentLengthHeader()
{
$this->request->getMethod()->willReturn("HEAD");
$body = $this->prophesize('Psr\Http\Message\StreamInterface');
$body->getSize()->willReturn(1024);
$this->response->getBody()->willReturn($body->reveal());
$this->response->hasHeader("Content-length")->willReturn(false);
$this->response->getHeaderLine("Transfer-encoding")->willReturn("");
$this->response->withHeader(Argument::cetera())->will(
function () {
$this->hasHeader("Content-length")->willReturn(true);
return $this;
}
);
$this->response->withBody(Argument::any())->willReturn($this->response->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$this->response->withHeader(Argument::cetera())->shouldHaveBeenCalled();
}
/**
* @coversNothing
*/
public function testRemovesBodyForHeadRequest()
{
$this->request->getMethod()->willReturn("HEAD");
$body = $this->prophesize('Psr\Http\Message\StreamInterface');
$body->getSize()->willReturn(1024);
$this->response->getBody()->willReturn($body->reveal());
$this->response->hasHeader("Content-length")->willReturn(false);
$this->response->getHeaderLine("Transfer-encoding")->willReturn("");
$this->response->withHeader(Argument::cetera())->will(
function () {
$this->hasHeader("Content-length")->willReturn(true);
return $this;
}
);
$this->response->withBody(Argument::any())->willReturn($this->response->reveal());
$request = $this->request->reveal();
$response = $this->response->reveal();
$this->router->dispatch($request, $response);
$this->response->withBody(Argument::that(function ($body) {
return $body->getSize() === 0;
}))->shouldHaveBeenCalled();
}
// ------------------------------------------------------------------------
// Respond
/**
* @covers ::respond
* @covers ::getRequest
* @covers ::getResponse
* @covers ::getResponder
*/
public function testRespondDispatchesRequest()
{
$middleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$middleware->dispatch(Argument::cetera())->willReturn();
$responder = $this->prophesize('WellRESTed\Routing\ResponderInterface');
$responder->respond(Argument::any())->willReturn();
$routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface');
$routeMap->dispatch(Argument::cetera())->will(
function ($args) use ($middleware) {
$middleware->reveal()->dispatch($args[0], $args[1]);
}
);
$router = $this->getMockBuilder('WellRESTed\Routing\Router')
->setMethods(["getRequest", "getResponse", "getResponder", "getRouteMap"])
->disableOriginalConstructor()
->getMock();
$router->expects($this->any())
->method("getRequest")
->will($this->returnValue($this->request->reveal()));
$router->expects($this->any())
->method("getResponse")
->will($this->returnValue($this->response->reveal()));
$router->expects($this->any())
->method("getResponder")
->will($this->returnValue($responder->reveal()));
$router->expects($this->any())
->method("getRouteMap")
->will($this->returnValue($routeMap->reveal()));
$router->__construct();
$router->respond();
$middleware->dispatch(Argument::cetera())->shouldHaveBeenCalled();
}
/**
* @covers ::respond
* @covers ::getRequest
* @covers ::getResponse
* @covers ::getResponder
*/
public function testSendsResponseToResponder()
{
$middleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface');
$middleware->dispatch(Argument::cetera())->willReturn();
$responder = $this->prophesize('WellRESTed\Routing\ResponderInterface');
$responder->respond(Argument::any())->willReturn();
$routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface');
$routeMap->dispatch(Argument::cetera())->will(
function ($args) use ($middleware) {
$middleware->reveal()->dispatch($args[0], $args[1]);
}
);
$router = $this->getMockBuilder('WellRESTed\Routing\Router')
->setMethods(["getRequest", "getResponse", "getResponder", "getRouteMap"])
->disableOriginalConstructor()
->getMock();
$router->expects($this->any())
->method("getRequest")
->will($this->returnValue($this->request->reveal()));
$router->expects($this->any())
->method("getResponse")
->will($this->returnValue($this->response->reveal()));
$router->expects($this->any())
->method("getResponder")
->will($this->returnValue($responder->reveal()));
$router->expects($this->any())
->method("getRouteMap")
->will($this->returnValue($routeMap->reveal()));
$router->__construct();
$router->respond();
$responder->respond($this->response->reveal())->shouldHaveBeenCalled();
$router = $this->router
->register("GET", "/", "middleware")
->register("POST", "/", "middleware");
$this->assertSame($this->router, $router);
}
}