From 14195355e37d6486a5258b160eeb52278dd11e74 Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Sat, 21 Feb 2015 07:13:09 -0500 Subject: [PATCH] Add RouteTable --- .idea/codeStyleSettings.xml | 9 ++ src/pjdietz/WellRESTed/RouteTable.php | 178 ++++++++++++++++++++++++++ test/RouteTableTest.php | 168 ++++++++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 .idea/codeStyleSettings.xml create mode 100644 src/pjdietz/WellRESTed/RouteTable.php create mode 100644 test/RouteTableTest.php diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..c4c9543 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/pjdietz/WellRESTed/RouteTable.php b/src/pjdietz/WellRESTed/RouteTable.php new file mode 100644 index 0000000..1fe2a99 --- /dev/null +++ b/src/pjdietz/WellRESTed/RouteTable.php @@ -0,0 +1,178 @@ + + * @copyright Copyright 2015 by PJ Dietz + * @license MIT + */ + +namespace pjdietz\WellRESTed; + +use pjdietz\WellRESTed\Interfaces\HandlerInterface; +use pjdietz\WellRESTed\Interfaces\RequestInterface; +use pjdietz\WellRESTed\Interfaces\ResponseInterface; +use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface; +use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface; + +/** + * RouteTable + * + * A RouteTable uses the request path to dispatche the best-matching handler. + */ +class RouteTable implements HandlerInterface +{ + /** @var array Array of Route objects */ + private $routes; + /** @var array Hash array mapping exact paths to routes */ + private $staticRoutes; + /** @var array Hash array mapping path prefixes to routes */ + private $prefixRoutes; + + /** Create a new RouteTable */ + public function __construct() + { + $this->routes = array(); + $this->prefixRoutes = array(); + $this->staticRoutes = array(); + } + + /** + * Return the response from the best matching route. + * + * @param RequestInterface $request + * @param array|null $args + * @return ResponseInterface + */ + public function getResponse(RequestInterface $request, array $args = null) + { + $response = null; + + $path = $request->getPath(); + + // First check if there is a handler for this exact path. + $handler = $this->getStaticHandler($path); + if ($handler) { + return $handler->getResponse($request, $args); + } + + // Check prefix routes for any routes that match. Use the longest matching prefix. + $handler = $this->getPrefixHandler($path); + if ($handler) { + return $handler->getResponse($request, $args); + } + + // Try each of the routes. + foreach ($this->routes as $route) { + /** @var HandlerInterface $route */ + $response = $route->getResponse($request, $args); + if ($response) { + return $response; + } + } + + return null; + } + + /** + * Return the handler associated with the matching static route, or null if none match. + * + * @param $path string The request's path + * @return HandlerInterface|null + */ + private function getStaticHandler($path) + { + if (isset($this->staticRoutes[$path])) { + $route = $this->staticRoutes[$path]; + return $route->getHandler(); + } + return null; + } + + /** + * Returning the best-matching prefix handler, or null if none match. + * + * @param $path string The request's path + * @return HandlerInterface|null + */ + private function getPrefixHandler($path) + { + // Find all prefixes that match the start of this path. + $prefixes = array_keys($this->prefixRoutes); + $matches = array_filter( + $prefixes, + function ($prefix) use ($path) { + return (strrpos($path, $prefix, -strlen($path)) !== false); + } + ); + + if ($matches) { + if (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); + } + // Instantiate and return the handler identified as the best match. + $route = $this->prefixRoutes[$matches[0]]; + return $route->getHandler(); + } + return null; + } + + /** + * Append a series of routes. + * + * @param HandlerInterface[] $routes List array of HandlerInterface instances + */ + public function addRoutes(array $routes) + { + foreach ($routes as $route) { + if ($route instanceof HandlerInterface) { + $this->addRoute($route); + } + } + } + + /** + * Append a new route. + * + * @param HandlerInterface $route + */ + public function addRoute(HandlerInterface $route) + { + if ($route instanceof StaticRouteInterface) { + $this->addStaticRoute($route); + } elseif ($route instanceof PrefixRouteInterface) { + $this->addPrefixRoute($route); + } else { + $this->routes[] = $route; + } + } + + /** + * Register a new static route. + * + * @param StaticRouteInterface $staticRoute + */ + private function addStaticRoute(StaticRouteInterface $staticRoute) + { + foreach ($staticRoute->getPaths() as $path) { + $this->staticRoutes[$path] = $staticRoute; + } + } + + /** + * Register a new prefix route. + * + * @param PrefixRouteInterface $prefixRoute + */ + private function addPrefixRoute(PrefixRouteInterface $prefixRoute) + { + foreach ($prefixRoute->getPrefixes() as $prefix) { + $this->prefixRoutes[$prefix] = $prefixRoute; + } + } +} diff --git a/test/RouteTableTest.php b/test/RouteTableTest.php new file mode 100644 index 0000000..f92e0c9 --- /dev/null +++ b/test/RouteTableTest.php @@ -0,0 +1,168 @@ +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 testReturnsNullWhenNoRoutesMatch() + { + $table = new RouteTable(); + $response = $table->getResponse($this->request->reveal()); + $this->assertNull($response); + } + + public function testMatchesStaticRoute() + { + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface"); + $this->route->getPaths()->willReturn(["/cats/"]); + $this->route->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/cats/"); + + $table = new RouteTable(); + $table->addRoute($this->route->reveal()); + $table->getResponse($this->request->reveal()); + + $this->route->getHandler()->shouldHaveBeenCalled(); + } + + public function testMatchesPrefixRoute() + { + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $this->route->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $this->route->getPrefixes()->willReturn(["/cats/"]); + $this->route->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/cats/molly"); + + $table = new RouteTable(); + $table->addRoute($this->route->reveal()); + $table->getResponse($this->request->reveal()); + + $this->route->getHandler()->shouldHaveBeenCalled(); + } + + public function testMatchesBestPrefixRoute() + { + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route2->getPrefixes()->willReturn(["/animals/cats/"]); + $route2->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addRoute($route1->reveal()); + $table->addRoute($route2->reveal()); + $table->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldNotHaveBeenCalled(); + $route2->getHandler()->shouldHaveBeenCalled(); + } + + public function testMatchesStaticRouteBeforePrefixRoute() + { + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/cats/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\StaticRouteInterface"); + $route2->getPaths()->willReturn(["/animals/cats/molly"]); + $route2->getHandler()->willReturn($this->handler->reveal()); + + $this->request->getPath()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addRoute($route1->reveal()); + $table->addRoute($route2->reveal()); + $table->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldNotHaveBeenCalled(); + $route2->getHandler()->shouldHaveBeenCalled(); + } + + public function testMatchesPrefixRouteBeforeHandlerRoute() + { + $this->handler->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->willImplement("\\pjdietz\\WellRESTed\\Interfaces\\Routes\\PrefixRouteInterface"); + $route1->getPrefixes()->willReturn(["/animals/cats/"]); + $route1->getHandler()->willReturn($this->handler->reveal()); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->getResponse(Argument::cetera())->willReturn(null); + + $this->request->getPath()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addRoute($route1->reveal()); + $table->addRoute($route2->reveal()); + $table->getResponse($this->request->reveal()); + + $route1->getHandler()->shouldHaveBeenCalled(); + $route2->getResponse(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + public function testReturnsFirstNonNullResponse() + { + $route1 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route1->getResponse(Argument::cetera())->willReturn(null); + + $route2 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route2->getResponse(Argument::cetera())->willReturn($this->response->reveal()); + + $route3 = $this->prophesize("\\pjdietz\\WellRESTed\\Interfaces\\HandlerInterface"); + $route3->getResponse(Argument::cetera())->willReturn(null); + + $this->request->getPath()->willReturn("/"); + + $table = new RouteTable(); + $table->addRoutes( + [ + $route1->reveal(), + $route2->reveal(), + $route3->reveal() + ] + ); + $response = $table->getResponse($this->request->reveal()); + + $this->assertNotNull($response); + $route1->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route2->getResponse(Argument::cetera())->shouldHaveBeenCalled(); + $route3->getResponse(Argument::cetera())->shouldNotHaveBeenCalled(); + } +}