Add TemplateRoute
This commit is contained in:
parent
c82acfa380
commit
e4ef1a8cb3
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WellRESTed\Routing\Route;
|
||||||
|
|
||||||
|
class TemplateRoute extends RegexRoute
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Regular expression matching URL friendly characters (i.e., letters,
|
||||||
|
* digits, hyphen and underscore)
|
||||||
|
*/
|
||||||
|
const RE_SLUG = '[0-9a-zA-Z\-_]+';
|
||||||
|
/** Regular expression matching digitis */
|
||||||
|
const RE_NUM = '[0-9]+';
|
||||||
|
/** Regular expression matching letters */
|
||||||
|
const RE_ALPHA = '[a-zA-Z]+';
|
||||||
|
/** Regular expression matching letters and digits */
|
||||||
|
const RE_ALPHANUM = '[0-9a-zA-Z]+';
|
||||||
|
/** Regular expression matching a URI template variable (e.g., {id}) */
|
||||||
|
const URI_TEMPLATE_EXPRESSION_RE = '/{([[a-zA-Z][a-zA-Z0-_]*)}/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new route that matches a URI template to a handler.
|
||||||
|
*
|
||||||
|
* Optionally provide patterns for the variables in the template.
|
||||||
|
*
|
||||||
|
* @param string $template URI template the path must match
|
||||||
|
* @param mixed $target Handler to dispatch
|
||||||
|
* @param string $defaultPattern Regular expression for variables
|
||||||
|
* @param array $variablePatterns Map of variable names and partial regular expression
|
||||||
|
*
|
||||||
|
* @see BaseRoute for details about $target
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
$template,
|
||||||
|
$middleware,
|
||||||
|
$defaultPattern = self::RE_SLUG,
|
||||||
|
$variablePatterns = null
|
||||||
|
) {
|
||||||
|
$pattern = $this->buildPattern($template, $defaultPattern, $variablePatterns);
|
||||||
|
parent::__construct($pattern, $middleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the URI template into a regular expression.
|
||||||
|
*
|
||||||
|
* @param string $template URI template the path must match
|
||||||
|
* @param string $defaultPattern Regular expression for variables
|
||||||
|
* @param array $variablePatterns Map of variable names and regular expression
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function buildPattern($template, $defaultPattern, $variablePatterns)
|
||||||
|
{
|
||||||
|
// Ensure $variablePatterns is an array.
|
||||||
|
if (is_null($variablePatterns)) {
|
||||||
|
$variablePatterns = array();
|
||||||
|
} elseif (is_object($variablePatterns)) {
|
||||||
|
$variablePatterns = (array) $variablePatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure a default is set.
|
||||||
|
if (!$defaultPattern) {
|
||||||
|
$defaultPattern = self::RE_SLUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the template into the pattern
|
||||||
|
$pattern = $template;
|
||||||
|
|
||||||
|
// Escape allowable characters with regex meaning.
|
||||||
|
$pattern = str_replace(
|
||||||
|
array("-", "."),
|
||||||
|
array("\\-", "\\."),
|
||||||
|
$pattern);
|
||||||
|
|
||||||
|
// Replace * with .* AFTER escaping to avoid escaping .*
|
||||||
|
$pattern = str_replace("*", ".*", $pattern);
|
||||||
|
|
||||||
|
// Surround the pattern with delimiters.
|
||||||
|
$pattern = "~^{$pattern}$~";
|
||||||
|
|
||||||
|
// Replace all template variables with matching subpatterns.
|
||||||
|
$callback = function ($matches) use ($variablePatterns, $defaultPattern) {
|
||||||
|
$key = $matches[1];
|
||||||
|
if (isset($variablePatterns[$key])) {
|
||||||
|
$pattern = $variablePatterns[$key];
|
||||||
|
} else {
|
||||||
|
$pattern = $defaultPattern;
|
||||||
|
}
|
||||||
|
return "(?<{$key}>{$pattern})";
|
||||||
|
};
|
||||||
|
$pattern = preg_replace_callback(self::URI_TEMPLATE_EXPRESSION_RE, $callback, $pattern);
|
||||||
|
|
||||||
|
return $pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace WellRESTed\Test\Unit\Routing\Route;
|
||||||
|
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use WellRESTed\Routing\Route\TemplateRoute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers WellRESTed\Routing\Route\TemplateRoute
|
||||||
|
* @uses WellRESTed\Routing\Route\RegexRoute
|
||||||
|
* @uses WellRESTed\Routing\Route\Route
|
||||||
|
*/
|
||||||
|
class TemplateRouteTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
private $request;
|
||||||
|
private $response;
|
||||||
|
private $middleware;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->request = $this->prophesize("\\Psr\\Http\\Message\\ServerRequestInterface");
|
||||||
|
$this->response = $this->prophesize("\\Psr\\Http\\Message\\ResponseInterface");
|
||||||
|
$this->middleware = $this->prophesize("\\WellRESTed\\Routing\\MiddlewareInterface");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider matchingTemplateProvider
|
||||||
|
*/
|
||||||
|
public function testMatchesTemplate($template, $default, $vars, $path)
|
||||||
|
{
|
||||||
|
$route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars);
|
||||||
|
$this->assertTrue($route->matchesRequestTarget($path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider matchingTemplateProvider
|
||||||
|
*/
|
||||||
|
public function testExtractsCaptures($template, $default, $vars, $path, $expectedCaptures)
|
||||||
|
{
|
||||||
|
$route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars);
|
||||||
|
$route->matchesRequestTarget($path, $captures);
|
||||||
|
$this->assertEquals(0, count(array_diff_assoc($expectedCaptures, $captures)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function matchingTemplateProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
["/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/12", ["id" => "12"]],
|
||||||
|
[
|
||||||
|
"/cat/{catId}/{dogId}",
|
||||||
|
TemplateRoute::RE_SLUG,
|
||||||
|
null,
|
||||||
|
"/cat/molly/bear",
|
||||||
|
[
|
||||||
|
"catId" => "molly",
|
||||||
|
"dogId" => "bear"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/cat/{catId}/{dogId}",
|
||||||
|
TemplateRoute::RE_NUM,
|
||||||
|
[
|
||||||
|
"catId" => TemplateRoute::RE_SLUG,
|
||||||
|
"dogId" => TemplateRoute::RE_SLUG
|
||||||
|
],
|
||||||
|
"/cat/molly/bear",
|
||||||
|
[
|
||||||
|
"catId" => "molly",
|
||||||
|
"dogId" => "bear"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"/cat/{catId}/{dogId}",
|
||||||
|
TemplateRoute::RE_NUM,
|
||||||
|
(object) [
|
||||||
|
"catId" => TemplateRoute::RE_SLUG,
|
||||||
|
"dogId" => TemplateRoute::RE_SLUG
|
||||||
|
],
|
||||||
|
"/cat/molly/bear",
|
||||||
|
[
|
||||||
|
"catId" => "molly",
|
||||||
|
"dogId" => "bear"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["/cat/{id}/*", null, null, "/cat/12/molly", ["id" => "12"]],
|
||||||
|
[
|
||||||
|
"/cat/{id}-{width}x{height}.jpg",
|
||||||
|
TemplateRoute::RE_NUM,
|
||||||
|
null,
|
||||||
|
"/cat/17-200x100.jpg",
|
||||||
|
[
|
||||||
|
"id" => "17",
|
||||||
|
"width" => "200",
|
||||||
|
"height" => "100"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
["/cat/{path}", ".*", null, "/cat/this/section/has/slashes", ["path" => "this/section/has/slashes"]]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider allowedVariableNamesProvider
|
||||||
|
*/
|
||||||
|
public function testMatchesAllowedVariablesNames($template, $path, $expectedCaptures)
|
||||||
|
{
|
||||||
|
$route = new TemplateRoute($template, $this->middleware->reveal());
|
||||||
|
$route->matchesRequestTarget($path, $captures);
|
||||||
|
$this->assertEquals(0, count(array_diff_assoc($expectedCaptures, $captures)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowedVariableNamesProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
["/{n}", "/lower", ["n" => "lower"]],
|
||||||
|
["/{N}", "/UPPER", ["N" => "UPPER"]],
|
||||||
|
["/{var1024}", "/digits", ["var1024" => "digits"]],
|
||||||
|
["/{variable_name}", "/underscore", ["variable_name" => "underscore"]],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider illegalVariableNamesProvider
|
||||||
|
*/
|
||||||
|
public function testFailsToMatchIllegalVariablesNames($template, $path)
|
||||||
|
{
|
||||||
|
$route = new TemplateRoute($template, $this->middleware->reveal());
|
||||||
|
$this->assertFalse($route->matchesRequestTarget($path, $captures));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function illegalVariableNamesProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
["/{not-legal}", "/hyphen"],
|
||||||
|
["/{1digitfirst}", "/digitfirst"],
|
||||||
|
["/{%2f}", "/percent-encoded"],
|
||||||
|
["/{}", "/empty"],
|
||||||
|
["/{{nested}}", "/nested"]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider nonmatchingTemplateProvider
|
||||||
|
*/
|
||||||
|
public function testFailsToMatchNonmatchingTemplate($template, $default, $vars, $path)
|
||||||
|
{
|
||||||
|
$route = new TemplateRoute($template, $this->middleware->reveal(), $default, $vars);
|
||||||
|
$this->assertFalse($route->matchesRequestTarget($path, $captures));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nonmatchingTemplateProvider()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array("/cat/{id}", TemplateRoute::RE_NUM, null, "/cat/molly"),
|
||||||
|
array("/cat/{catId}/{dogId}", TemplateRoute::RE_ALPHA, null, "/cat/12/13"),
|
||||||
|
array(
|
||||||
|
"/cat/{catId}/{dogId}",
|
||||||
|
TemplateRoute::RE_NUM,
|
||||||
|
array(
|
||||||
|
"catId" => TemplateRoute::RE_ALPHA,
|
||||||
|
"dogId" => TemplateRoute::RE_ALPHA
|
||||||
|
),
|
||||||
|
"/cat/12/13"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue