diff --git a/src/pjdietz/WellRESTed/Router.php b/src/pjdietz/WellRESTed/Router.php index 68e5de2..2c3ca18 100644 --- a/src/pjdietz/WellRESTed/Router.php +++ b/src/pjdietz/WellRESTed/Router.php @@ -16,6 +16,8 @@ use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface; use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface; use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface; +use pjdietz\WellRESTed\Routes\PrefixRoute; +use pjdietz\WellRESTed\Routes\StaticRoute; /** * Router @@ -27,13 +29,13 @@ class Router implements HandlerInterface /** @var array Array of Route objects */ private $routes; - /** @var array Hash array mapping path prefixes to handlers */ + /** @var array Hash array mapping path prefixes to routes */ private $prefixRoutes; - /** @var array Hash array mapping exact paths to handlers */ + /** @var array Hash array mapping exact paths to routes */ private $staticRoutes; - /** @var array Hash array of status code => qualified HandlerInterface names for error handling. */ + /** @var array Hash array of status code => error handler */ private $errorHandlers; /** Create a new Router. */ @@ -67,16 +69,16 @@ class Router implements HandlerInterface } /** - * Append a new route to the route route table. + * Append a new route to the route table. * * @param HandlerInterface $route */ public function addRoute(HandlerInterface $route) { if ($route instanceof StaticRouteInterface) { - $this->setStaticRoute($route->getPaths(), $route->getHandler()); + $this->addStaticRoute($route); } elseif ($route instanceof PrefixRouteInterface) { - $this->setPrefixRoute($route->getPrefixes(), $route->getHandler()); + $this->addPrefixRoute($route); } else { $this->routes[] = $route; } @@ -96,43 +98,11 @@ class Router implements HandlerInterface } } - /** - * Add a route for paths beginning with a given prefix. - * - * @param string|array $prefixes Prefix of a path to match - * @param string $handler Fully qualified name to an autoloadable handler class - */ - public function setPrefixRoute($prefixes, $handler) - { - if (is_string($prefixes)) { - $prefixes = array($prefixes); - } - foreach ($prefixes as $prefix) { - $this->prefixRoutes[$prefix] = $handler; - } - } - - /** - * Add a route for an exact match to a path. - * - * @param string|array $paths Path component of the URI or a list of paths - * @param string $handler Fully qualified name to an autoloadable handler class - */ - public function setStaticRoute($paths, $handler) - { - if (is_string($paths)) { - $paths = array($paths); - } - foreach ($paths as $path) { - $this->staticRoutes[$path] = $handler; - } - } - /** * Add a custom error handler. * - * @param integer $statusCode The error status code. - * @param string $errorHandler Fully qualified name to an autoloadable handler class. + * @param integer $statusCode The error status code + * @param callable|string|HandlerInterface $errorHandler */ public function setErrorHandler($statusCode, $errorHandler) { @@ -142,7 +112,7 @@ class Router implements HandlerInterface /** * Add custom error handlers. * - * @param array $errorHandlers Array mapping integer error codes to qualified handler names. + * @param array $errorHandlers Array mapping integer error codes to handlers */ public function setErrorHandlers(array $errorHandlers) { @@ -185,11 +155,25 @@ class Router implements HandlerInterface return $response; } + private function addStaticRoute(StaticRouteInterface $staticRoute) + { + foreach ($staticRoute->getPaths() as $path) { + $this->staticRoutes[$path] = $staticRoute; + } + } + + private function addPrefixRoute(PrefixRouteInterface $prefixRoute) + { + foreach ($prefixRoute->getPrefixes() as $prefix) { + $this->prefixRoutes[$prefix] = $prefixRoute; + } + } + private function getErrorResponse($status, $request, $args = null, $response = null) { if (isset($this->errorHandlers[$status])) { - /** @var HandlerInterface $errorHandler */ - $errorHandler = new $this->errorHandlers[$status](); + $unpacker = new HandlerUnpacker(); + $errorHandler = $unpacker->unpack($this->errorHandlers[$status]); // Pass the response triggering this along to the error handler. $errorArgs = array("response" => $response); if ($args) { @@ -201,7 +185,7 @@ class Router implements HandlerInterface } /** - * Returning the matching static handler, or null if none match. + * Returning the handler associated with the matching static route, or null if none match. * * @param $path string The request's path * @return HandlerInterface|null @@ -209,8 +193,8 @@ class Router implements HandlerInterface private function getStaticHandler($path) { if (isset($this->staticRoutes[$path])) { - // Instantiate and return the handler identified by the path. - return new $this->staticRoutes[$path](); + $route = $this->staticRoutes[$path]; + return $route->getHandler(); } return null; } @@ -237,7 +221,8 @@ class Router implements HandlerInterface }); } // Instantiate and return the handler identified as the best match. - return new $this->prefixRoutes[$matches[0]](); + $route = $this->prefixRoutes[$matches[0]]; + return $route->getHandler(); } return null; } @@ -295,4 +280,28 @@ class Router implements HandlerInterface } return $response; } + + //////////////// + // Deprecated // + //////////////// + + /** + * @deprecated Use {@see addRoute} instead. + * @see addRoute + */ + public function setPrefixRoute($prefixes, $handler) + { + $this->addPrefixRoute(new PrefixRoute($prefixes, $handler)); + trigger_error("Router::setPrefixRoute is deprecated. Use addRoute", E_USER_DEPRECATED); + } + + /** + * @deprecated Use {@see addRoute} instead. + * @see addRoute + */ + public function setStaticRoute($paths, $handler) + { + $this->addStaticRoute(new StaticRoute($paths, $handler)); + trigger_error("Router::setStaticRoute is deprecated. Use addRoute", E_USER_DEPRECATED); + } } diff --git a/test/RouterTest.php b/test/RouterTest.php index 011baf4..f30b8f5 100644 --- a/test/RouterTest.php +++ b/test/RouterTest.php @@ -2,245 +2,341 @@ namespace pjdietz\WellRESTed\Test; -use pjdietz\WellRESTed\Exceptions\HttpExceptions\ForbiddenException; -use pjdietz\WellRESTed\Interfaces\HandlerInterface; -use pjdietz\WellRESTed\Interfaces\RequestInterface; -use pjdietz\WellRESTed\Interfaces\ResponseInterface; -use pjdietz\WellRESTed\Response; +use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException; use pjdietz\WellRESTed\Router; -use pjdietz\WellRESTed\Routes\PrefixRoute; -use pjdietz\WellRESTed\Routes\StaticRoute; -use pjdietz\WellRESTed\Routes\TemplateRoute; +use Prophecy\Argument; +/** + * @covers pjdietz\WellRESTed\Router + */ class RouterTest extends \PHPUnit_Framework_TestCase { - public function testAddRoute() + private $handler; + private $request; + private $response; + private $route; + + public function setUp() { - $path = "/"; - - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue($path)); - - $route = new StaticRoute($path, __NAMESPACE__ . '\\RouterTestHandler'); - $router = new Router(); - $router->addRoute($route); - $resp = $router->getResponse($mockRequest); - $this->assertNotNull($resp); + $this->request = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\RequestInterface"); + $this->response = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\ResponseInterface"); + $this->route = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $this->handler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); } - public function testAddRoutes() + public function testMatchesStaticRoute() { - $path = "/"; + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue($path)); + $this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface"); + $this->route->getPaths()->willReturn(["/cats/"]); + $this->route->getHandler()->willReturn($this->handler->reveal()); - $routes = array(); - $routes[] = new StaticRoute("/", __NAMESPACE__ . '\\RouterTestHandler'); - $routes[] = new StaticRoute("/another/", __NAMESPACE__ . '\\RouterTestHandler'); + $this->request->getPath()->willReturn("/cats/"); $router = new Router(); - $router->addRoutes($routes); - $resp = $router->getResponse($mockRequest); - $this->assertEquals(200, $resp->getStatusCode()); + $router->addRoute($this->route->reveal()); + $router->getResponse($this->request->reveal()); + + $this->route->getHandler()->shouldHaveBeenCalled(); } - public function testAddStaticRoute() + public function testMatchesPrefixRoute() { - $path = "/cats/"; + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue($path)); + $this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $this->route->getPrefixes()->willReturn(["/cats/"]); + $this->route->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/cats/molly"); $router = new Router(); - $router->setStaticRoute($path, __NAMESPACE__ . '\\RouterTestHandler'); - $resp = $router->getResponse($mockRequest); - $this->assertNotNull($resp); + $router->addRoute($this->route->reveal()); + $router->getResponse($this->request->reveal()); + + $this->route->getHandler()->shouldHaveBeenCalled(); } - public function testAddPrefixRoute() + public function testMatchesBestPrefixRoute() { - $path = "/cats/"; + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue($path)); + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route2->getPrefixes()->willReturn(["/animals/cats/"]); + $route2->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/animals/cats/molly"); $router = new Router(); - $router->setPrefixRoute($path, __NAMESPACE__ . '\\RouterTestHandler'); - $resp = $router->getResponse($mockRequest); - $this->assertNotNull($resp); + $router->addRoute($route1->reveal()); + $router->addRoute($route2->reveal()); + $router->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldNotHaveBeenCalled(); + $route2->getHandler()->shouldHaveBeenCalled(); } - public function testMatchStaticRouteBeforePrefixRoute() + public function testMatchesStaticRouteBeforePrefixRoute() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/amimals/cats/molly")); + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/cats/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface"); + $route2->getPaths()->willReturn(["/animals/cats/molly"]); + $route2->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/animals/cats/molly"); $router = new Router(); - $router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler')); - $router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\NotFoundHandler')); - $router->addRoute(new StaticRoute("/amimals/cats/molly", __NAMESPACE__ . '\\RouterTestHandler')); - $resp = $router->getResponse($mockRequest); - $this->assertEquals(200, $resp->getStatusCode()); + $router->addRoute($route1->reveal()); + $router->addRoute($route2->reveal()); + $router->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldNotHaveBeenCalled(); + $route2->getHandler()->shouldHaveBeenCalled(); } - public function testMatchBestPrefixRoute() + public function testMatchesPrefixRouteBeforeHandlerRoute() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/amimals/cats/molly")); + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/cats/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->getResponse(Argument::cetera())->willReturn(null); + + $this->request->getPath()->willReturn("/animals/cats/molly"); $router = new Router(); - $router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler')); - $router->addRoute(new PrefixRoute("/amimals/dogs/", __NAMESPACE__ . '\\NotFoundHandler')); - $router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\RouterTestHandler')); - $resp = $router->getResponse($mockRequest); - $this->assertEquals(200, $resp->getStatusCode()); + $router->addRoute($route1->reveal()); + $router->addRoute($route2->reveal()); + $router->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldHaveBeenCalled(); + $route2->getResponse(Argument::cetera())->shouldNotHaveBeenCalled(); } - public function testMatchBestPrefixRouteBeforeTemplateRoute() + public function testReturnsFirstNonNullResponse() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/amimals/cats/molly")); + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->getResponse(Argument::cetera())->willReturn(null); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route3->getResponse(Argument::cetera())->willReturn(null); + + $this->request->getPath()->willReturn("/"); $router = new Router(); - $router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler')); - $router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\RouterTestHandler')); - $router->addRoute(new TemplateRoute("/amimals/cats/*", __NAMESPACE__ . '\\NotFoundHandler')); - $resp = $router->getResponse($mockRequest); - $this->assertEquals(200, $resp->getStatusCode()); + $router->addRoutes( + [ + $route1->reveal(), + $route2->reveal(), + $route3->reveal() + ] + ); + $response = $router->getResponse($this->request->reveal()); + + $this->assertNotNull($response); + $route1->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route2->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route3->getResponse(Argument::cetera())->shouldNotHaveBeenCalled(); } - public function testRespondWithDefaultErrorForException() + public function testReturnsNullWhenNoRouteMatches() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/")); + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->getResponse(Argument::cetera())->willReturn(null); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->getResponse(Argument::cetera())->willReturn(null); + + $route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route3->getResponse(Argument::cetera())->willReturn(null); $router = new Router(); - $router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenExceptionHandler')); - $resp = $router->getResponse($mockRequest); - $this->assertEquals(403, $resp->getStatusCode()); + $router->addRoutes( + [ + $route1->reveal(), + $route2->reveal(), + $route3->reveal() + ] + ); + $response = $router->getResponse($this->request->reveal()); + + $this->assertNull($response); + $route1->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route2->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route3->getResponse(Argument::cetera())->shouldHaveBeenCalled(); } - public function testRespondWithErrorHandlerForException() + public function testRespondsWithErrorResponseForHttpException() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/")); + $this->route->getResponse(Argument::cetera())->willThrow(new HttpException()); + $this->request->getPath()->willReturn("/"); $router = new Router(); - $router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenExceptionHandler')); - $router->setErrorHandler(403, __NAMESPACE__ . '\\ForbiddenErrorHandler'); - $resp = $router->getResponse($mockRequest); - $this->assertEquals("YOU SHALL NOT PASS!", $resp->getBody()); + $router->addRoute($this->route->reveal()); + $response = $router->getResponse($this->request->reveal()); + $this->assertEquals(500, $response->getStatusCode()); } - public function testRespondWithErrorHandlerForStatusCode() + public function testDispatchesErrorHandlerForStatusCode() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/")); + $this->request->getPath()->willReturn("/"); + $this->response->getStatusCode()->willReturn(403); + $this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); $router = new Router(); - $router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\ForbiddenHandler')); - $router->setErrorHandler(403, __NAMESPACE__ . '\\ForbiddenErrorHandler'); - $resp = $router->getResponse($mockRequest); - $this->assertEquals("YOU SHALL NOT PASS!", $resp->getBody()); + $router->addRoute($this->route->reveal()); + $router->setErrorHandlers([403 => $errorHandler->reveal()]); + $router->getResponse($this->request->reveal()); + + $errorHandler->getResponse(Argument::cetera())->shouldHaveBeenCalled(); } - public function testRespondWithErrorHandlerUsingOriginalResponse() + public function testDispatchesErrorHandlerWithOriginalRequest() { - $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); - $mockRequest->expects($this->any()) - ->method('getPath') - ->will($this->returnValue("/")); + $this->request->getPath()->willReturn("/"); + $this->response->getStatusCode()->willReturn(403); + $this->route->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $errorHandler = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $errorHandler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $request = $this->request->reveal(); $router = new Router(); - $router->addRoute(new StaticRoute("/", __NAMESPACE__ . '\\MessageHandler')); - $router->setErrorHandlers([404 => __NAMESPACE__ . '\\MessageErrorHandler']); - $resp = $router->getResponse($mockRequest); - $this->assertEquals("