Store PrefixRoutes to a separate array.

Prioritize routes in the order static, prefix, everything else.
This commit is contained in:
PJ Dietz 2015-01-02 13:13:08 -05:00
parent caef817535
commit ca2c8625ec
4 changed files with 218 additions and 8 deletions

View File

@ -0,0 +1,28 @@
<?php
/**
* pjdietz\WellRESTed\Interfaces\Route\PrefixRouteInterface
*
* @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2015 by PJ Dietz
* @license MIT
*/
namespace pjdietz\WellRESTed\Interfaces\Routes;
interface PrefixRouteInterface
{
/**
* Returns the target class this maps to.
*
* @return string Fully qualified name for a HandlerInterface
*/
public function getHandler();
/**
* Returns the path prefixes this maps to a target handler.
*
* @return array Array of path prefixes.
*/
public function getPrefixes();
}

View File

@ -4,7 +4,7 @@
* pjdietz\WellRESTed\Router * pjdietz\WellRESTed\Router
* *
* @author PJ Dietz <pj@pjdietz.com> * @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz * @copyright Copyright 2015 by PJ Dietz
* @license MIT * @license MIT
*/ */
@ -14,6 +14,7 @@ use pjdietz\WellRESTed\Exceptions\HttpExceptions\HttpException;
use pjdietz\WellRESTed\Interfaces\HandlerInterface; use pjdietz\WellRESTed\Interfaces\HandlerInterface;
use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface;
use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface;
use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface; use pjdietz\WellRESTed\Interfaces\Routes\StaticRouteInterface;
/** /**
@ -26,6 +27,9 @@ class Router implements HandlerInterface
/** @var array Array of Route objects */ /** @var array Array of Route objects */
private $routes; private $routes;
/** @var array Hash array mapping path prefixes to handlers */
private $prefixRoutes;
/** @var array Hash array mapping exact paths to handlers */ /** @var array Hash array mapping exact paths to handlers */
private $staticRoutes; private $staticRoutes;
@ -36,6 +40,7 @@ class Router implements HandlerInterface
public function __construct() public function __construct()
{ {
$this->routes = array(); $this->routes = array();
$this->prefixRoutes = array();
$this->staticRoutes = array(); $this->staticRoutes = array();
$this->errorHandlers = array(); $this->errorHandlers = array();
} }
@ -76,6 +81,8 @@ class Router implements HandlerInterface
{ {
if ($route instanceof StaticRouteInterface) { if ($route instanceof StaticRouteInterface) {
$this->setStaticRoute($route->getPaths(), $route->getHandler()); $this->setStaticRoute($route->getPaths(), $route->getHandler());
} elseif ($route instanceof PrefixRouteInterface) {
$this->setPrefixRoute($route->getPrefixes(), $route->getHandler());
} else { } else {
$this->routes[] = $route; $this->routes[] = $route;
} }
@ -96,7 +103,23 @@ class Router implements HandlerInterface
} }
/** /**
* A route for an exact match to a path. * Add a route for paths beginning with a given prefix.
*
* @param string|array $prefixes Prefix of a path to match
* @param string $handler Fully qualified name to an autoloadable handler class
*/
public function setPrefixRoute($prefixes, $handler)
{
if (is_string($prefixes)) {
$prefixes = array($prefixes);
}
foreach ($prefixes as $prefix) {
$this->prefixRoutes[$prefix] = $handler;
}
}
/**
* Add a route for an exact match to a path.
* *
* @param string|array $paths Path component of the URI or a list of paths * @param string|array $paths Path component of the URI or a list of paths
* @param string $handler Fully qualified name to an autoloadable handler class * @param string $handler Fully qualified name to an autoloadable handler class
@ -163,14 +186,48 @@ class Router implements HandlerInterface
return $response; return $response;
} }
/**
* Returning the matching static handler, or null if none match.
*
* @param $path string The request's path
* @return HandlerInterface|null
*/
private function getStaticHandler($path) private function getStaticHandler($path)
{ {
if (array_key_exists($path, $this->staticRoutes)) { if (array_key_exists($path, $this->staticRoutes)) {
// Instantiate and return the handler identified by the path.
return new $this->staticRoutes[$path](); return new $this->staticRoutes[$path]();
} }
return null; 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 there are multiple matches, sort them to find the one with the longest string length.
if (count($matches) > 0) {
usort($matches, function ($a, $b) {
return strlen($b) - strlen($a);
});
}
// Instantiate and return the handler identified as the best match.
return new $this->prefixRoutes[$matches[0]]();
}
return null;
}
private function getResponseFromRoutes(RequestInterface $request, array $args = null) private function getResponseFromRoutes(RequestInterface $request, array $args = null)
{ {
$response = null; $response = null;
@ -183,6 +240,12 @@ class Router implements HandlerInterface
return $this->tryResponse($handler, $request, $args); return $this->tryResponse($handler, $request, $args);
} }
// Check prefix routes for any routes that match. Use the longest matching prefix.
$handler = $this->getPrefixHandler($path);
if ($handler) {
return $this->tryResponse($handler, $request, $args);
}
// Try each of the routes. // Try each of the routes.
foreach ($this->routes as $route) { foreach ($this->routes as $route) {
/** @var HandlerInterface $route */ /** @var HandlerInterface $route */

View File

@ -1,22 +1,46 @@
<?php <?php
/** /**
* pjdietz\WellRESTed\PrefixRoute * pjdietz\WellRESTed\Routes\PrefixRoute
* *
* @author PJ Dietz <pj@pjdietz.com> * @author PJ Dietz <pj@pjdietz.com>
* @copyright Copyright 2014 by PJ Dietz * @copyright Copyright 2015 by PJ Dietz
* @license MIT * @license MIT
*/ */
namespace pjdietz\WellRESTed\Routes; namespace pjdietz\WellRESTed\Routes;
use InvalidArgumentException;
use pjdietz\WellRESTed\Interfaces\RequestInterface; use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\Routes\PrefixRouteInterface;
/** /**
* Maps a list of static URI paths to a Handler * Maps a list of static URI paths to a Handler
*/ */
class PrefixRoute extends StaticRoute class PrefixRoute extends BaseRoute implements PrefixRouteInterface
{ {
/** @var array List of static URI paths */
private $prefixes;
/**
* Create a new StaticRoute for a given path or paths and a handler class.
*
* @param string|array $prefixes Path or list of paths the request must match
* @param string $targetClassName Fully qualified name to an autoloadable handler class.
* @throws \InvalidArgumentException
*/
public function __construct($prefixes, $targetClassName)
{
parent::__construct($targetClassName);
if (is_string($prefixes)) {
$this->prefixes = array($prefixes);
} elseif (is_array($prefixes)) {
$this->prefixes = $prefixes;
} else {
throw new InvalidArgumentException("$prefixes must be a string or array of string");
}
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
/* HandlerInterface */ /* HandlerInterface */
@ -32,12 +56,32 @@ class PrefixRoute extends StaticRoute
public function getResponse(RequestInterface $request, array $args = null) public function getResponse(RequestInterface $request, array $args = null)
{ {
$requestPath = $request->getPath(); $requestPath = $request->getPath();
foreach ($this->paths as $path) { foreach ($this->prefixes as $prefix) {
if (substr($requestPath, 0, strlen($path)) === $path) { if (strrpos($requestPath, $prefix, -strlen($requestPath)) !== false) {
$target = $this->getTarget(); $target = $this->getTarget();
return $target->getResponse($request, $args); return $target->getResponse($request, $args);
} }
} }
return null; return null;
} }
/**
* Returns the path prefixes this maps to a target handler.
*
* @return array Array of path prefixes.
*/
public function getPrefixes()
{
return $this->prefixes;
}
/**
* Returns the target class this maps to.
*
* @return string Fully qualified name for a HandlerInterface
*/
public function getHandler()
{
return $this->getTarget();
}
} }

View File

@ -8,6 +8,7 @@ use pjdietz\WellRESTed\Interfaces\RequestInterface;
use pjdietz\WellRESTed\Interfaces\ResponseInterface; use pjdietz\WellRESTed\Interfaces\ResponseInterface;
use pjdietz\WellRESTed\Response; use pjdietz\WellRESTed\Response;
use pjdietz\WellRESTed\Router; use pjdietz\WellRESTed\Router;
use pjdietz\WellRESTed\Routes\PrefixRoute;
use pjdietz\WellRESTed\Routes\StaticRoute; use pjdietz\WellRESTed\Routes\StaticRoute;
use pjdietz\WellRESTed\Routes\TemplateRoute; use pjdietz\WellRESTed\Routes\TemplateRoute;
@ -48,6 +49,81 @@ class RouterTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(200, $resp->getStatusCode()); $this->assertEquals(200, $resp->getStatusCode());
} }
public function testAddStaticRoute()
{
$path = "/cats/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$router = new Router();
$router->setStaticRoute($path, __NAMESPACE__ . '\\RouterTestHandler');
$resp = $router->getResponse($mockRequest);
$this->assertNotNull($resp);
}
public function testAddPrefixRoute()
{
$path = "/cats/";
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue($path));
$router = new Router();
$router->setPrefixRoute($path, __NAMESPACE__ . '\\RouterTestHandler');
$resp = $router->getResponse($mockRequest);
$this->assertNotNull($resp);
}
public function testMatchStaticRouteBeforePrefixRoute()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/amimals/cats/molly"));
$router = new Router();
$router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler'));
$router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\NotFoundHandler'));
$router->addRoute(new StaticRoute("/amimals/cats/molly", __NAMESPACE__ . '\\RouterTestHandler'));
$resp = $router->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testMatchBestPrefixRoute()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/amimals/cats/molly"));
$router = new Router();
$router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler'));
$router->addRoute(new PrefixRoute("/amimals/dogs/", __NAMESPACE__ . '\\NotFoundHandler'));
$router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\RouterTestHandler'));
$resp = $router->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testMatchBestPrefixRouteBeforeTemplateRoute()
{
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
$mockRequest->expects($this->any())
->method('getPath')
->will($this->returnValue("/amimals/cats/molly"));
$router = new Router();
$router->addRoute(new PrefixRoute("/amimals/", __NAMESPACE__ . '\\NotFoundHandler'));
$router->addRoute(new PrefixRoute("/amimals/cats/", __NAMESPACE__ . '\\RouterTestHandler'));
$router->addRoute(new TemplateRoute("/amimals/cats/*", __NAMESPACE__ . '\\NotFoundHandler'));
$resp = $router->getResponse($mockRequest);
$this->assertEquals(200, $resp->getStatusCode());
}
public function testRespondWithDefaultErrorForException() public function testRespondWithDefaultErrorForException()
{ {
$mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface'); $mockRequest = $this->getMock('\pjdietz\WellRESTed\Interfaces\RequestInterface');
@ -337,7 +413,6 @@ class InjectionErrorHandler implements HandlerInterface
} }
} }
class InjectionHandler implements HandlerInterface class InjectionHandler implements HandlerInterface
{ {
public function getResponse(RequestInterface $request, array $args = null) public function getResponse(RequestInterface $request, array $args = null)