diff --git a/src/Routing/RouteMap.php b/src/Routing/RouteMap.php new file mode 100644 index 0000000..61fb9d4 --- /dev/null +++ b/src/Routing/RouteMap.php @@ -0,0 +1,46 @@ + middleware */ - private $statusHandlers; + private $statusHooks; - /** @var RouteTable Collection of routes */ - private $routeTable; - - /** @var RouteFactoryInterface */ - private $routeFactory; + /** @var RouteMapInterface */ + private $routeMap; // ------------------------------------------------------------------------ public function __construct() { - $this->responsePreparationHooks = $this->getResponsePreparationHooks(); - $this->routeFactory = $this->getRouteFactory(); - $this->routeTable = $this->getRouteTable(); - $this->statusHandlers = []; + $this->dispatcher = $this->getDispatcher(); + $this->finalizationHooks = $this->getFinalizationHooks(); + $this->postRouteHooks = $this->getPostRouteHooks(); + $this->preRouteHooks = $this->getPreRouteHooks(); + $this->statusHooks = $this->getStatusHooks(); + $this->routeMap = $this->getRouteMap(); } // ------------------------------------------------------------------------ - - /** - * Create and return a route given a string path, a handler, and optional - * extra arguments. - * - * The method will determine the most appropriate route subclass to use - * and will forward the arguments on to the subclass's constructor. - * - * - Paths with no special characters will generate StaticRoutes - * - Paths ending with * will generate PrefixRoutes - * - Paths containing URI variables (e.g., {id}) will generate TemplateRoutes - * - Regular exressions will generate RegexRoutes - * - * @param string $target Path, prefix, or pattern to match - * @param mixed $middleware Middleware to dispatch - * @param mixed $extra - */ - public function add($target, $middleware, $extra = null) - { - if (is_array($middleware)) { - $map = $this->getMethodMap(); - $map->addMap($middleware); - $middleware = $map; - } - $this->routeFactory->registerRoute($this->routeTable, $target, $middleware, $extra); - } - - public function addPreRouteHook($middleware) - { - if (!isset($this->preRouteHooks)) { - $this->preRouteHooks = []; - } - $this->preRouteHooks[] = $middleware; - } - - public function addPostRouteHook($middleware) - { - if (!isset($this->postRouteHooks)) { - $this->postRouteHooks = []; - } - $this->postRouteHooks[] = $middleware; - } - - public function addResponsePreparationHook($middleware) - { - $this->responsePreparationHooks[] = $middleware; - } - - public function setStatusHandler($statusCode, $middleware) - { - $this->statusHandlers[$statusCode] = $middleware; - } + // MiddlewareInterface public function dispatch(ServerRequestInterface $request, ResponseInterface &$response) { - $this->disptachPreRouteHooks($request, $response); + $this->dispatchPreRouteHooks($request, $response); try { - $this->routeTable->dispatch($request, $response); + $this->routeMap->dispatch($request, $response); } catch (HttpException $e) { $response = $response->withStatus($e->getCode()); $response = $response->withBody(new Stream($e->getMessage())); } - $statusCode = $response->getStatusCode(); - if (isset($this->statusHandlers[$statusCode])) { - $middleware = $this->statusHandlers[$statusCode]; - $dispatcher = $this->getDispatcher(); - $dispatcher->dispatch($middleware, $request, $response); - } - $this->disptachPostRouteHooks($request, $response); - $this->dispatchResponsePreparationHooks($request, $response); + $this->dispatchStatusHooks($request, $response); + $this->dispatchPostRouteHooks($request, $response); + $this->dispatchFinalizationHooks($request, $response); } + // ------------------------------------------------------------------------ + // RouteMapInterface + + /** + * 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::addMethod + * + * $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) + { + $this->routeMap->add($method, $target, $middleware); + } + + // ------------------------------------------------------------------------ + public function respond() { $request = $this->getRequest(); @@ -128,34 +104,91 @@ class Router implements MiddlewareInterface $responder->respond($response); } + // ------------------------------------------------------------------------ + // Hooks + + public function addPreRouteHook($middleware) + { + $this->preRouteHooks[] = $middleware; + } + + public function addPostRouteHook($middleware) + { + $this->postRouteHooks[] = $middleware; + } + + public function addFinalizationHook($middleware) + { + $this->finalizationHooks[] = $middleware; + } + + public function setStatusHook($statusCode, $middleware) + { + $this->statusHooks[$statusCode] = $middleware; + } + // ------------------------------------------------------------------------ // The following methods provide instaces the router will use. Override // to provide custom classes or configured instances. - // @codeCoverageIgnoreStart - /** * Return an instance that can dispatch middleware. + * * Override to provide a custom class. * * @return DispatcherInterface */ protected function getDispatcher() { - if (!isset($this->dispatcher)) { - $this->dispatcher = new Dispatcher(); - } - return $this->dispatcher; + return new Dispatcher(); } /** - * @return MethodMapInterface + * Return an instance that maps routes to middleware. + * + * Override to provide a custom class. + * + * @return RouteMapInterface */ - protected function getMethodMap() + protected function getRouteMap() { - return new MethodMap(); + return new RouteMap(); } + /** + * @return array + */ + protected function getPreRouteHooks() + { + return []; + } + + /** + * @return array + */ + protected function getPostRouteHooks() + { + return []; + } + + /** + * @return array + */ + protected function getStatusHooks() + { + return []; + } + + /** + * @return array + */ + protected function getFinalizationHooks() + { + return []; + } + + // @codeCoverageIgnoreStart + /** * @return ServerRequestInterface */ @@ -180,62 +213,38 @@ class Router implements MiddlewareInterface return new Response(); } - /** - * @return MiddlewareInterface[] - */ - protected function getResponsePreparationHooks() - { - return [ - new ContentLengthPrep(), - new HeadPrep() - ]; - } - - /** - * @return RouteFactoryInterface - */ - protected function getRouteFactory() - { - return new RouteFactory(); - } - - /** - * @return RouteTableInterface - */ - protected function getRouteTable() - { - return new RouteTable(); - } - // @codeCoverageIgnoreEnd // ------------------------------------------------------------------------ - private function disptachPreRouteHooks(ServerRequestInterface $request, ResponseInterface &$response) + private function dispatchPreRouteHooks(ServerRequestInterface $request, ResponseInterface &$response) { - if ($this->preRouteHooks) { - $dispatcher = $this->getDispatcher(); - foreach ($this->preRouteHooks as $hook) { - $dispatcher->dispatch($hook, $request, $response); - } + foreach ($this->preRouteHooks as $hook) { + $this->dispatcher->dispatch($hook, $request, $response); } } - private function disptachPostRouteHooks(ServerRequestInterface $request, ResponseInterface &$response) + private function dispatchPostRouteHooks(ServerRequestInterface $request, ResponseInterface &$response) { - if ($this->postRouteHooks) { - $dispatcher = $this->getDispatcher(); - foreach ($this->postRouteHooks as $hook) { - $dispatcher->dispatch($hook, $request, $response); - } + foreach ($this->postRouteHooks as $hook) { + $this->dispatcher->dispatch($hook, $request, $response); } } - private function dispatchResponsePreparationHooks(ServerRequestInterface $request, ResponseInterface &$response) + private function dispatchFinalizationHooks(ServerRequestInterface $request, ResponseInterface &$response) { - $dispatcher = $this->getDispatcher(); - foreach ($this->responsePreparationHooks as $hook) { - $dispatcher->dispatch($hook, $request, $response); + foreach ($this->finalizationHooks as $hook) { + $this->dispatcher->dispatch($hook, $request, $response); + } + } + + private function dispatchStatusHooks(ServerRequestInterface $request, ResponseInterface &$response) + { + $statusCode = $response->getStatusCode(); + if (isset($this->statusHooks[$statusCode])) { + $middleware = $this->statusHooks[$statusCode]; + $dispatcher = $this->getDispatcher(); + $dispatcher->dispatch($middleware, $request, $response); } } } diff --git a/test/tests/unit/Routing/RouterTest.php b/test/tests/unit/Routing/RouterTest.php index 52a30bd..bc4b75d 100644 --- a/test/tests/unit/Routing/RouterTest.php +++ b/test/tests/unit/Routing/RouterTest.php @@ -6,46 +6,26 @@ use Prophecy\Argument; use WellRESTed\HttpExceptions\NotFoundException; use WellRESTed\Routing\Router; +// TODO Tests that ensure hooks are called at correct times +// TODO Test default finalization hooks + /** * @coversDefaultClass WellRESTed\Routing\Router * @uses WellRESTed\Routing\Router * @uses WellRESTed\Message\Stream * @uses WellRESTed\Routing\Dispatcher - * @uses WellRESTed\Routing\MethodMap - * @uses WellRESTed\Routing\ResponsePrep\ContentLengthPrep - * @uses WellRESTed\Routing\ResponsePrep\HeadPrep - * @uses WellRESTed\Routing\Route\PrefixRoute - * @uses WellRESTed\Routing\Route\RegexRoute - * @uses WellRESTed\Routing\Route\Route - * @uses WellRESTed\Routing\Route\RouteFactory - * @uses WellRESTed\Routing\Route\StaticRoute - * @uses WellRESTed\Routing\Route\TemplateRoute - * @uses WellRESTed\Routing\RouteTable + * @uses WellRESTed\Routing\RouteMap */ class RouterTest extends \PHPUnit_Framework_TestCase { - private $dispatcher; - private $middleware; private $request; - private $responder; private $response; public function setUp() { - $this->dispatcher = $this->prophesize('WellRESTed\Routing\DispatcherInterface'); - $this->dispatcher->dispatch(Argument::any())->willReturn(); - $this->middleware = $this->prophesize('WellRESTed\Routing\MiddlewareInterface'); - $this->middleware->dispatch(Argument::cetera())->willReturn(); + parent::setUp(); $this->request = $this->prophesize('Psr\Http\Message\ServerRequestInterface'); - $this->request->getRequestTarget()->willReturn("/"); - $this->request->getMethod()->willReturn("GET"); - $this->responder = $this->prophesize('WellRESTed\Routing\ResponderInterface'); - $this->responder->respond(Argument::any())->willReturn(); $this->response = $this->prophesize('Psr\Http\Message\ResponseInterface'); - $this->response->withStatus(Argument::any())->willReturn($this->response->reveal()); - $this->response->withBody(Argument::any())->willReturn($this->response->reveal()); - $this->response->hasHeader("Content-length")->willReturn(true); - $this->response->getStatusCode()->willReturn(200); } // ------------------------------------------------------------------------ @@ -53,6 +33,12 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::__construct + * @covers ::getDispatcher + * @covers ::getRouteMap + * @covers ::getPreRouteHooks + * @covers ::getPostRouteHooks + * @covers ::getFinalizationHooks + * @covers ::getStatusHooks */ public function testCreatesInstance() { @@ -66,85 +52,26 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::add */ - public function testAddWithSimpleRouteRegistersRoute() + public function testAddRegistersRouteWithRouteMap() { - $factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactoryInterface'); - $factory->registerRoute(Argument::cetera())->willReturn(); + $routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface'); + $routeMap->add(Argument::cetera())->willReturn(); $router = $this->getMockBuilder('WellRESTed\Routing\Router') - ->setMethods(["getRouteFactory"]) + ->setMethods(["getRouteMap"]) ->disableOriginalConstructor() ->getMock(); $router->expects($this->any()) - ->method("getRouteFactory") - ->will($this->returnValue($factory->reveal())); + ->method("getRouteMap") + ->will($this->returnValue($routeMap->reveal())); $router->__construct(); - $target = "/cats/"; - $middleware = $this->middleware->reveal(); - $router->add($target, $middleware); + $method = "GET"; + $target = "/path/{id}"; + $middleware = "Middleware"; - $factory->registerRoute(Argument::any(), $target, $middleware, Argument::any())->shouldHaveBeenCalled(); - } - - /** - * @covers ::add - */ - public function testAddWithMapAddsMiddlewareToMethodMap() - { - $map = $this->prophesize('WellRESTed\Routing\MethodMapInterface'); - $map->addMap(Argument::any())->willReturn(); - - $factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactoryInterface'); - $factory->registerRoute(Argument::cetera())->willReturn(); - - $router = $this->getMockBuilder('WellRESTed\Routing\Router') - ->setMethods(["getRouteFactory", "getMethodMap"]) - ->disableOriginalConstructor() - ->getMock(); - $router->expects($this->any()) - ->method("getMethodMap") - ->will($this->returnValue($map->reveal())); - $router->expects($this->any()) - ->method("getRouteFactory") - ->will($this->returnValue($factory->reveal())); - $router->__construct(); - - $target = "/cats/"; - $middleware = ["GET" => $this->middleware->reveal()]; - $router->add($target, $middleware); - - $map->addMap($middleware)->shouldHaveBeenCalled(); - } - - /** - * @covers ::add - */ - public function testAddWithMapRegistersMethodMap() - { - $map = $this->prophesize('WellRESTed\Routing\MethodMapInterface'); - $map->addMap(Argument::any())->willReturn(); - - $factory = $this->prophesize('WellRESTed\Routing\Route\RouteFactoryInterface'); - $factory->registerRoute(Argument::cetera())->willReturn(); - - $router = $this->getMockBuilder('WellRESTed\Routing\Router') - ->setMethods(["getRouteFactory", "getMethodMap"]) - ->disableOriginalConstructor() - ->getMock(); - $router->expects($this->any()) - ->method("getMethodMap") - ->will($this->returnValue($map->reveal())); - $router->expects($this->any()) - ->method("getRouteFactory") - ->will($this->returnValue($factory->reveal())); - $router->__construct(); - - $target = "/cats/"; - $middleware = ["GET" => $this->middleware->reveal()]; - $router->add($target, $middleware); - - $factory->registerRoute(Argument::any(), $target, $map, Argument::any())->shouldHaveBeenCalled(); + $router->add($method, $target, $middleware); + $routeMap->add($method, $target, $middleware)->shouldHaveBeenCalled(); } // ------------------------------------------------------------------------ @@ -153,18 +80,25 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::dispatch */ - public function testDispatchesMatchedRoute() + public function testDispatchesRouteMap() { - $this->request->getRequestTarget()->willReturn("/cats/"); + $routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface'); + $routeMap->dispatch(Argument::cetera())->willReturn(); - $router = new Router(); - $router->add("/cats/", $this->middleware->reveal()); + $router = $this->getMockBuilder('WellRESTed\Routing\Router') + ->setMethods(["getRouteMap"]) + ->disableOriginalConstructor() + ->getMock(); + $router->expects($this->any()) + ->method("getRouteMap") + ->will($this->returnValue($routeMap->reveal())); + $router->__construct(); $request = $this->request->reveal(); - $response = $this->response->reveal(); - $router->dispatch($request, $response); + $resonse = $this->response->reveal(); + $router->dispatch($request, $resonse); - $this->middleware->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + $routeMap->dispatch($request, Argument::any())->shouldHaveBeenCalled(); } // ------------------------------------------------------------------------ @@ -172,9 +106,9 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::addPreRouteHook - * @covers ::disptachPreRouteHooks + * @covers ::dispatchPreRouteHooks */ - public function testDispatchesPreRouteHooks() + public function testDispatchesPreRouteHook() { $hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); $hook->dispatch(Argument::cetera())->willReturn(); @@ -183,7 +117,6 @@ class RouterTest extends \PHPUnit_Framework_TestCase $router = new Router(); $router->addPreRouteHook($hook->reveal()); - $router->add("/cats/", $this->middleware->reveal()); $request = $this->request->reveal(); $response = $this->response->reveal(); @@ -194,9 +127,9 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::addPostRouteHook - * @covers ::disptachPostRouteHooks + * @covers ::dispatchPostRouteHooks */ - public function testDispatchesPostRouteHooks() + public function testDispatchesPostRouteHook() { $hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); $hook->dispatch(Argument::cetera())->willReturn(); @@ -205,7 +138,6 @@ class RouterTest extends \PHPUnit_Framework_TestCase $router = new Router(); $router->addPostRouteHook($hook->reveal()); - $router->add("/cats/", $this->middleware->reveal()); $request = $this->request->reveal(); $response = $this->response->reveal(); @@ -215,10 +147,10 @@ class RouterTest extends \PHPUnit_Framework_TestCase } /** - * @covers ::addResponsePreparationHook - * @covers ::dispatchResponsePreparationHooks + * @covers ::addFinalizationHook + * @covers ::dispatchFinalizationHooks */ - public function testDispatchesResponsePreparationHooks() + public function testDispatchesFinalHooks() { $hook = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); $hook->dispatch(Argument::cetera())->willReturn(); @@ -226,8 +158,7 @@ class RouterTest extends \PHPUnit_Framework_TestCase $this->request->getRequestTarget()->willReturn("/cats/"); $router = new Router(); - $router->addResponsePreparationHook($hook->reveal()); - $router->add("/cats/", $this->middleware->reveal()); + $router->addFinalizationHook($hook->reveal()); $request = $this->request->reveal(); $response = $this->response->reveal(); @@ -241,9 +172,10 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::dispatch - * @covers ::setStatusHandler + * @covers ::dispatchStatusHooks + * @covers ::setStatusHook */ - public function testDispatchesHandlerForStatusCode() + public function testDispatchesHookForStatusCode() { $this->response->getStatusCode()->willReturn(403); @@ -251,8 +183,7 @@ class RouterTest extends \PHPUnit_Framework_TestCase $statusMiddleware->dispatch(Argument::cetera())->willReturn(); $router = new Router(); - $router->add("/cats/", $this->middleware->reveal()); - $router->setStatusHandler(403, $statusMiddleware->reveal()); + $router->setStatusHook(403, $statusMiddleware->reveal()); $request = $this->request->reveal(); $response = $this->response->reveal(); @@ -263,15 +194,33 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::dispatch - * @covers ::setStatusHandler + * @covers ::dispatchStatusHooks + * @covers ::setStatusHook */ - public function testDispatchesHandlerForStatusCodeForHttpException() + public function testDispatchesStatusHookForHttpException() { - $this->request->getRequestTarget()->willReturn("/cats/"); - $this->middleware->dispatch(Argument::cetera())->willThrow(new NotFoundException()); + $statusMiddleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); + $statusMiddleware->dispatch(Argument::cetera())->willReturn(); - $router = new Router(); - $router->add("/cats/", $this->middleware->reveal()); + $routeMap = $this->prophesize('WellRESTed\Routing\RouteMapInterface'); + $routeMap->dispatch(Argument::cetera())->willThrow(new NotFoundException()); + + $this->response->withStatus(Argument::any())->will( + function ($args) { + $this->getStatusCode()->willReturn($args[0]); + return $this; + } + ); + $this->response->withBody(Argument::any())->willReturn($this->response->reveal()); + + $router = $this->getMockBuilder('WellRESTed\Routing\Router') + ->setMethods(["getRouteMap"]) + ->disableOriginalConstructor() + ->getMock(); + $router->expects($this->any()) + ->method("getRouteMap") + ->will($this->returnValue($routeMap->reveal())); + $router->__construct(); $request = $this->request->reveal(); $response = $this->response->reveal(); @@ -285,16 +234,27 @@ class RouterTest extends \PHPUnit_Framework_TestCase /** * @covers ::respond + * @covers ::getRequest + * @covers ::getResponse + * @covers ::getResponder */ public function testRespondDispatchesRequest() { - $target = "/cats/"; + $middleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); + $middleware->dispatch(Argument::cetera())->willReturn(); - $this->request->getRequestTarget()->willReturn($target); - $this->responder->respond(Argument::any())->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"]) + ->setMethods(["getRequest", "getResponse", "getResponder", "getRouteMap"]) ->disableOriginalConstructor() ->getMock(); $router->expects($this->any()) @@ -305,25 +265,39 @@ class RouterTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue($this->response->reveal())); $router->expects($this->any()) ->method("getResponder") - ->will($this->returnValue($this->responder->reveal())); + ->will($this->returnValue($responder->reveal())); + $router->expects($this->any()) + ->method("getRouteMap") + ->will($this->returnValue($routeMap->reveal())); $router->__construct(); - $router->add($target, $this->middleware->reveal()); $router->respond(); - $this->middleware->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + $middleware->dispatch(Argument::cetera())->shouldHaveBeenCalled(); } /** * @covers ::respond + * @covers ::getRequest + * @covers ::getResponse + * @covers ::getResponder */ public function testSendsResponseToResponder() { - $target = "/cats/"; + $middleware = $this->prophesize('\WellRESTed\Routing\MiddlewareInterface'); + $middleware->dispatch(Argument::cetera())->willReturn(); - $this->request->getRequestTarget()->willReturn($target); + $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"]) + ->setMethods(["getRequest", "getResponse", "getResponder", "getRouteMap"]) ->disableOriginalConstructor() ->getMock(); $router->expects($this->any()) @@ -334,11 +308,13 @@ class RouterTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue($this->response->reveal())); $router->expects($this->any()) ->method("getResponder") - ->will($this->returnValue($this->responder->reveal())); + ->will($this->returnValue($responder->reveal())); + $router->expects($this->any()) + ->method("getRouteMap") + ->will($this->returnValue($routeMap->reveal())); $router->__construct(); - $router->add($target, $this->middleware->reveal()); $router->respond(); - $this->responder->respond($this->response->reveal())->shouldHaveBeenCalled(); + $responder->respond($this->response->reveal())->shouldHaveBeenCalled(); } }