From 918e33bd0ad1bcbfa454b0ac2673a07b6a417bab Mon Sep 17 00:00:00 2001 From: PJ Dietz Date: Thu, 2 Apr 2015 21:49:01 -0400 Subject: [PATCH] Add RouteTable --- src/Routing/RouteTable.php | 103 +++++++++++++ test/tests/unit/Routing/RouteTableTest.php | 160 +++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 src/Routing/RouteTable.php create mode 100644 test/tests/unit/Routing/RouteTableTest.php diff --git a/src/Routing/RouteTable.php b/src/Routing/RouteTable.php new file mode 100644 index 0000000..1d27f91 --- /dev/null +++ b/src/Routing/RouteTable.php @@ -0,0 +1,103 @@ +routes = []; + $this->staticRoutes = []; + $this->prefixRoutes = []; + } + + public function addRoute(RouteInterface $route) + { + $this->routes[] = $route; + } + + public function addStaticRoute(StaticRouteInterface $staticRoute) + { + $this->staticRoutes[$staticRoute->getPath()] = $staticRoute; + } + + public function addPrefixRoute(PrefixRouteInterface $prefxRoute) + { + $this->prefixRoutes[$prefxRoute->getPrefix()] = $prefxRoute; + } + + public function dispatch(ServerRequestInterface $request, ResponseInterface &$response) + { + $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; + } + + // Try each of the routes. + foreach ($this->routes as $route) { + if ($route->matchesRequestTarget($requestTarget, $captures)) { + if (is_array($captures)) { + foreach ($captures as $key => $value) { + $request = $request->withAttribute($key, $value); + } + } + $route->dispatch($request, $response); + } + } + } + + 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/RouteTableTest.php b/test/tests/unit/Routing/RouteTableTest.php new file mode 100644 index 0000000..76fcc47 --- /dev/null +++ b/test/tests/unit/Routing/RouteTableTest.php @@ -0,0 +1,160 @@ +request = $this->prophesize("\\Psr\\Http\\Message\\ServerRequestInterface"); + $this->response = $this->prophesize("\\Psr\\Http\\Message\\ResponseInterface"); + } + + public function testMatchesStaticRoute() + { + $route = $this->prophesize("\\WellRESTed\\Routing\\Route\\StaticRouteInterface"); + $route->getPath()->willReturn("/cats/"); + $route->dispatch(Argument::cetera())->willReturn(); + + $this->request->getRequestTarget()->willReturn("/cats/"); + + $table = new RouteTable(); + $table->addStaticRoute($route->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route->dispatch($this->request->reveal(), $this->response->reveal())->shouldHaveBeenCalled(); + } + + public function testMatchesPrefixRoute() + { + $route = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface"); + $route->getPrefix()->willReturn("/cats/"); + $route->dispatch(Argument::cetera())->willReturn(); + + $this->request->getRequestTarget()->willReturn("/cats/molly"); + + $table = new RouteTable(); + $table->addPrefixRoute($route->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route->dispatch($this->request->reveal(), $this->response->reveal())->shouldHaveBeenCalled(); + } + + public function testMatchesBestPrefixRoute() + { + $route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface"); + $route1->getPrefix()->willReturn("/animals/"); + $route1->dispatch(Argument::cetera())->willReturn(); + + $route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface"); + $route2->getPrefix()->willReturn("/animals/cats/"); + $route2->dispatch(Argument::cetera())->willReturn(); + + $this->request->getRequestTarget()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addPrefixRoute($route1->reveal()); + $table->addPrefixRoute($route2->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + $route2->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + } + + public function testMatchesStaticRouteBeforePrefixRoute() + { + $route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface"); + $route1->getPrefix()->willReturn("/animals/cats/"); + $route1->dispatch(Argument::cetera())->willReturn(); + + $route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\StaticRouteInterface"); + $route2->getPath()->willReturn("/animals/cats/molly"); + $route2->dispatch(Argument::cetera())->willReturn(); + + $this->request->getRequestTarget()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addPrefixRoute($route1->reveal()); + $table->addStaticRoute($route2->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + $route2->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + } + + public function testMatchesPrefixRouteBeforeRoute() + { + $route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\PrefixRouteInterface"); + $route1->getPrefix()->willReturn("/animals/cats/"); + $route1->dispatch(Argument::cetera())->willReturn(); + + $route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface"); + $route2->matchesRequestTarget(Argument::cetera())->willReturn(true); + + $this->request->getRequestTarget()->willReturn("/animals/cats/molly"); + + $table = new RouteTable(); + $table->addPrefixRoute($route1->reveal()); + $table->addRoute($route2->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route1->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + $route2->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + /** + * @uses WellRESTed\Routing\Route\TemplateRoute + * @uses WellRESTed\Routing\Route\RegexRoute + * @uses WellRESTed\Routing\Route\Route + */ + public function testAddsCapturesAsRequestAttributes() + { + // This test needs to read the result of the $captures parameter which + // is passed by reference. This is not so eary so mock, so the test + // will use an actual TemplateRoute. + + $middleware = $this->prophesize("\\WellRESTed\\Routing\\MiddlewareInterface"); + $route = new TemplateRoute("/cats/{id}", $middleware->reveal()); + + $this->request->withAttribute(Argument::cetera())->willReturn($this->request->reveal()); + $this->request->getRequestTarget()->willReturn("/cats/molly"); + + $table = new RouteTable(); + $table->addRoute($route); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $this->request->withAttribute("id", "molly")->shouldHaveBeenCalled(); + } + + public function testDispatchedFirstMatchingRoute() + { + $route1 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface"); + $route1->matchesRequestTarget(Argument::cetera())->willReturn(false); + + $route2 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface"); + $route2->matchesRequestTarget(Argument::cetera())->willReturn(true); + $route2->dispatch(Argument::cetera())->willReturn(); + + $route3 = $this->prophesize("\\WellRESTed\\Routing\\Route\\RouteInterface"); + $route3->matchesRequestTarget(Argument::cetera())->willReturn(false); + + $this->request->getRequestTarget()->willReturn("/"); + + $table = new RouteTable(); + $table->addRoute($route1->reveal()); + $table->addRoute($route2->reveal()); + $table->addRoute($route3->reveal()); + $table->dispatch($this->request->reveal(), $this->response->reveal()); + + $route1->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + $route2->dispatch(Argument::cetera())->shouldHaveBeenCalled(); + $route3->dispatch(Argument::cetera())->shouldNotHaveBeenCalled(); + } +}