Add RouteTable
This commit is contained in:
parent
56cf56c6c5
commit
14195355e3
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectCodeStyleSettingsManager">
|
||||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default (1)" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* pjdietz\WellRESTed\RouteTable
|
||||
*
|
||||
* @author PJ Dietz <pj@pjdietz.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace pjdietz\WellRESTed\Test;
|
||||
|
||||
use pjdietz\WellRESTed\RouteTable;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @covers pjdietz\WellRESTed\RouteTable
|
||||
*/
|
||||
class RouteTableTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $handler;
|
||||
private $request;
|
||||
private $response;
|
||||
private $route;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$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 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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue