diff --git a/src/Routing/RouteMap.php b/src/Routing/RouteMap.php deleted file mode 100644 index 4575195..0000000 --- a/src/Routing/RouteMap.php +++ /dev/null @@ -1,167 +0,0 @@ -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; - } -} diff --git a/src/Routing/Router.php b/src/Routing/Router.php index 693819a..0fb4c72 100644 --- a/src/Routing/Router.php +++ b/src/Routing/Router.php @@ -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); + } + + $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)); } - // ------------------------------------------------------------------------ - // 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[(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; - /** - * @return array - */ - protected function getPreRouteHooks() - { - return []; - } - - /** - * @return array - */ - protected function getPostRouteHooks() - { - return []; - } - - /** - * @return array - */ - protected function getStatusHooks() - { - return []; - } - - /** - * @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); + // 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 dispatchPostRouteHooks(ServerRequestInterface $request, ResponseInterface &$response) + private function getStaticRoute($requestTarget) { - foreach ($this->postRouteHooks as $hook) { - $this->dispatcher->dispatch($hook, $request, $response); + if (isset($this->staticRoutes[$requestTarget])) { + return $this->staticRoutes[$requestTarget]; } + return null; } - private function dispatchFinalizationHooks(ServerRequestInterface $request, ResponseInterface &$response) + private function getPrefixRoute($requestTarget) { - foreach ($this->finalizationHooks as $hook) { - $this->dispatcher->dispatch($hook, $request, $response); - } - } + // 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); + } + ); - 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); + 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; } } diff --git a/src/Routing/RouteMapInterface.php b/src/Routing/RouterInterface.php similarity index 89% rename from src/Routing/RouteMapInterface.php rename to src/Routing/RouterInterface.php index 5e33792..ca73fd1 100644 --- a/src/Routing/RouteMapInterface.php +++ b/src/Routing/RouterInterface.php @@ -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); } diff --git a/test/tests/unit/Routing/RouteMapTest.php b/test/tests/unit/Routing/RouteMapTest.php deleted file mode 100644 index dc867cd..0000000 --- a/test/tests/unit/Routing/RouteMapTest.php +++ /dev/null @@ -1,222 +0,0 @@ -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); - } -} diff --git a/test/tests/unit/Routing/RouterTest.php b/test/tests/unit/Routing/RouterTest.php index 996dd2d..48e39ad 100644 --- a/test/tests/unit/Routing/RouterTest.php +++ b/test/tests/unit/Routing/RouterTest.php @@ -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->response->withBody(Argument::any())->willReturn($this->response->reveal()); + $this->router->register("GET", $target, "middleware"); + $this->router->dispatch($this->request->reveal(), $this->response->reveal(), $this->next); - $request = $this->request->reveal(); - $response = $this->response->reveal(); - $this->router->dispatch($request, $response); + $this->route->dispatch($this->request->reveal(), $this->response->reveal(), $this->next)->shouldHaveBeenCalled(); + } + /** + * @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); } }