Begin RouteMap

This commit is contained in:
PJ Dietz 2015-05-07 21:36:35 -04:00
parent 66319218cb
commit d5eb044169
4 changed files with 279 additions and 24 deletions

View File

@ -2,30 +2,10 @@
namespace WellRESTed\Routing\Route; namespace WellRESTed\Routing\Route;
use WellRESTed\Routing\RouteTableInterface;
interface RouteFactoryInterface interface RouteFactoryInterface
{ {
/** /**
* Adds a new route to a route table. * Creates a route for the given target.
*
* 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
*/ */
public function registerRoute(RouteTableInterface $routeTable, $target, $middleware, $extra = null); public function create($target);
} }

View File

@ -2,10 +2,26 @@
namespace WellRESTed\Routing\Route; namespace WellRESTed\Routing\Route;
use WellRESTed\Routing\MethodMapInterface;
use WellRESTed\Routing\MiddlewareInterface; use WellRESTed\Routing\MiddlewareInterface;
interface RouteInterface extends 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 * Examines a path (request target) and returns whether or not the route
* should handle the request providing the target. * should handle the request providing the target.

View File

@ -4,9 +4,32 @@ namespace WellRESTed\Routing;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use WellRESTed\Routing\Route\RouteFactory;
use WellRESTed\Routing\Route\RouteFactoryInterface;
use WellRESTed\Routing\Route\RouteInterface;
class RouteMap implements RouteMapInterface 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. * 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) 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) 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;
} }
} }

View File

@ -0,0 +1,145 @@
<?php
namespace WellRESTed\Test\Unit\Routing;
use Prophecy\Argument;
use WellRESTed\Routing\Route\RouteInterface;
use WellRESTed\Routing\RouteMap;
/**
* @coversDefaultClass WellRESTed\Routing\RouteMap
* @uses WellRESTed\Routing\RouteMap
*/
class RouteMapTest extends \PHPUnit_Framework_TestCase
{
private $methodMap;
private $factory;
private $request;
private $response;
private $route;
private $routeMap;
public function setUp()
{
parent::setUp();
$this->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();
}
}