diff --git a/src/Routing/Route/RouteFactoryInterface.php b/src/Routing/Route/RouteFactoryInterface.php index b7ee4f8..9e47ca5 100644 --- a/src/Routing/Route/RouteFactoryInterface.php +++ b/src/Routing/Route/RouteFactoryInterface.php @@ -2,30 +2,10 @@ namespace WellRESTed\Routing\Route; -use WellRESTed\Routing\RouteTableInterface; - interface RouteFactoryInterface { /** - * Adds a new route to a route table. - * - * This method SHOULD parse $target to determine to the type of route to - * use and MUST create the route with the provided $middleware. - * - * Once the implementation has created the route the route, it MUST - * the route with $routeTable by calling an appropriate RouteTable::add- - * method. - * - * $extra MAY be passed to route constructors that use an extra option, - * such as TemplateRoute. - * - * This method MAY register any instance implementing - * WellRESTed\Routing\Route\RouteInterface. - * - * @param RouteTableInterface $routeTable Table to add the route to - * @param string $target Path, prefix, or pattern to match - * @param mixed $middleware Middleware to dispatch - * @param mixed $extra Additional options to pass to a route constructor + * Creates a route for the given target. */ - public function registerRoute(RouteTableInterface $routeTable, $target, $middleware, $extra = null); + public function create($target); } diff --git a/src/Routing/Route/RouteInterface.php b/src/Routing/Route/RouteInterface.php index bf08043..c488819 100644 --- a/src/Routing/Route/RouteInterface.php +++ b/src/Routing/Route/RouteInterface.php @@ -2,10 +2,26 @@ namespace WellRESTed\Routing\Route; +use WellRESTed\Routing\MethodMapInterface; use WellRESTed\Routing\MiddlewareInterface; interface RouteInterface extends MiddlewareInterface { + const TYPE_STATIC = 0; + const TYPE_PREFIX = 1; + const TYPE_PATTERN = 2; + + public function getTarget(); + + public function getType(); + + /** + * Return the instance mapping methods to middleware for this route. + * + * @return MethodMapInterface + */ + public function getMethodMap(); + /** * Examines a path (request target) and returns whether or not the route * should handle the request providing the target. diff --git a/src/Routing/RouteMap.php b/src/Routing/RouteMap.php index f06a345..6042da7 100644 --- a/src/Routing/RouteMap.php +++ b/src/Routing/RouteMap.php @@ -4,9 +4,32 @@ namespace WellRESTed\Routing; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use WellRESTed\Routing\Route\RouteFactory; +use WellRESTed\Routing\Route\RouteFactoryInterface; +use WellRESTed\Routing\Route\RouteInterface; class RouteMap implements RouteMapInterface { + /** @var RouteFactoryInterface */ + private $factory; + /** @var RouteInterface[] Array of Route objects */ + private $routes; + /** @var RouteInterface[] Hash array mapping exact paths to routes */ + private $staticRoutes; + /** @var RouteInterface[] Hash array mapping path prefixes to routes */ + private $prefixRoutes; + /** @var RouteInterface[] Hash array mapping path prefixes to routes */ + private $patternRoutes; + + public function __construct() + { + $this->factory = $this->getRouteFactory(); + $this->routes = []; + $this->staticRoutes = []; + $this->prefixRoutes = []; + $this->patternRoutes = []; + } + /** * Register middleware with the router for a given path and method. * @@ -36,11 +59,102 @@ class RouteMap implements RouteMapInterface */ public function add($method, $target, $middleware) { - // TODO: Implement addRoute() method. + $route = $this->getRouteForTarget($target); + $route->getMethodMap()->setMethod($method, $middleware); } public function dispatch(ServerRequestInterface $request, ResponseInterface &$response) { - // TODO: Implement dispatch() method. + $requestTarget = $request->getRequestTarget(); + + $route = $this->getStaticRoute($requestTarget); + if ($route) { + $route->dispatch($request, $response); + return; + } + + $route = $this->getPrefixRoute($requestTarget); + if ($route) { + $route->dispatch($request, $response); + return; + } + + } + + /** + * @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/test/tests/unit/Routing/RouteMapTest.php b/test/tests/unit/Routing/RouteMapTest.php new file mode 100644 index 0000000..25aa07d --- /dev/null +++ b/test/tests/unit/Routing/RouteMapTest.php @@ -0,0 +1,145 @@ +methodMap = $this->prophesize('WellRESTed\Routing\MethodMapInterface'); + $this->methodMap->setMethod(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->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->setMethod("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"); + + $request = $this->request->reveal(); + $response = $this->response->reveal(); + $this->routeMap->dispatch($request, $response); + + $this->route->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + } + + /** + * @covers ::dispatch + * @covers ::getPrefixRoute + * @covers ::registerRouteForTarget + */ + public function testDispatchesPrefixRoute() + { + $target = "/*"; + + $this->request->getRequestTarget()->willReturn($target); + $this->route->getTarget()->willReturn($target); + $this->route->getType()->willReturn(RouteInterface::TYPE_PREFIX); + + $this->routeMap->add("GET", $target, "middleware"); + + $request = $this->request->reveal(); + $response = $this->response->reveal(); + $this->routeMap->dispatch($request, $response); + + $this->route->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + } +}