Refactored which has inadvertedly made it pluggable :)
There's quite a few files... I tried to make a copy to put online by my filesystem seems to have created a recursive directory it won't copy (wtf??). I check for a recursive symlink but there's no symlink.
EDIT | Online copy (this isn't finished):
http://www.w3style.co.uk/~d11wtq/request_router.tar.gz
Anyway, here's the shiny new refactored code:
Test Case
Code: Select all
<?php
Mock::Generate("Request_Route", "MockRoute");
Mock::GeneratePartial("Request_Route", "PartialRoute", array("hydrateRequest"));
class TestOfRouter extends UnitTestCase
{
public function testEachRouteIsChecked()
{
$router = new Request_Router();
$route1 = new MockRoute();
$route1->__construct("/foo/bar", array());
$route1->expectAtLeastOnce("getUri");
$router->addRoute($route1);
$route2 = new MockRoute();
$route2->__construct("/zip/button", array());
$route2->expectAtLeastOnce("getUri");
$router->addRoute($route2);
$router->execute();
}
public function testRoutesAreExecutedOnUriMatch()
{
$router = new Request_Router();
$route1 = new PartialRoute();
$route1->__construct("/foo/bar", array());
$route1->expectOnce("hydrateRequest");
$router->addRoute($route1);
$route2 = new PartialRoute();
$route2->__construct("/zip/button", array());
$route2->expectNever("hydrateRequest");
$router->addRoute($route2);
$router->setUri("/foo/bar");
$router->execute();
}
public function testMatchCanContainVariables()
{
$router = new Request_Router();
$route1 = new PartialRoute();
$route1->__construct("/foobar/:bar", array());
$route1->expectNever("hydrateRequest");
$router->addRoute($route1);
$route2 = new PartialRoute();
$route2->__construct("/foo/:bar", array());
$route2->expectOnce("hydrateRequest");
$router->addRoute($route2);
$router->setUri("/foo/1234");
$router->execute();
}
public function testMultipleRoutesCanApply()
{
$router = new Request_Router();
$route1 = new PartialRoute();
$route1->__construct("/:foo/:bar", array());
$route1->expectOnce("hydrateRequest");
$router->addRoute($route1);
$route2 = new PartialRoute();
$route2->__construct("/foo/:bar", array());
$route2->expectOnce("hydrateRequest");
$router->addRoute($route2);
$router->setUri("/foo/1234");
$router->execute();
}
public function testListedValuesAreUsed()
{
$router = new Request_Router();
$route = new Request_Route("/foo/bar", array("order" => "1234", "date" => "2007-06-22"));
$router->addRoute($route);
$router->setUri("/foo/bar");
$array = array();
$router->execute($array);
$this->assertEqual($array["order"], "1234");
$this->assertEqual($array["date"], "2007-06-22");
$router = new Request_Router();
$route = new Request_Route("/foo/:bar", array("rodent" => "mouse", "where" => "clock"));
$router->addRoute($route);
$router->setUri("/foo/testing123");
$array = array();
$router->execute($array);
$this->assertEqual($array["rodent"], "mouse");
$this->assertEqual($array["where"], "clock");
}
public function testDynamicValuesAreUsed()
{
$router = new Request_Router();
$route = new Request_Route("/foo/:bar", array("rodent" => "mouse", "where" => "clock"));
$router->addRoute($route);
$router->setUri("/foo/testing123");
$array = array();
$router->execute($array);
$this->assertEqual($array["bar"], "testing123");
$router = new Request_Router();
$route = new Request_Route("/:module/:action/:id", array());
$router->addRoute($route);
$router->setUri("/incidents/update/123");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "incidents");
$this->assertEqual($array["action"], "update");
$this->assertEqual($array["id"], "123");
}
public function testWildcardsCanBeUsed()
{
$router = new Request_Router();
$route1 = new PartialRoute();
$route1->__construct("/:bar/*", array());
$route1->expectOnce("hydrateRequest");
$router->addRoute($route1);
$route2 = new PartialRoute();
$route2->__construct("/foo/*", array());
$route2->expectOnce("hydrateRequest");
$router->addRoute($route2);
$router->setUri("/foo/1234");
$router->execute();
}
public function testTrailingSlashIsIrrelevant()
{
$router = new Request_Router();
$route = new PartialRoute();
$route->__construct("/:foo/:bar");
$route->expectCallCount("hydrateRequest", 2);
$router->addRoute($route);
$router->setUri("/x/y");
$router->execute();
$router->setUri("/x/y/");
$router->execute();
}
public function testWildcardsAtEndOfUrlIgnoreLeadingSlash()
{
$router = new Request_Router();
$route = new PartialRoute();
$route->__construct("/:foo/:bar/*");
$route->expectCallCount("hydrateRequest", 2);
$router->addRoute($route);
$router->setUri("/x/y");
$router->execute();
$router->setUri("/x/y/");
$router->execute();
}
public function testMultipleRoutesCanHydrateRequest()
{
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/docs/:chapter/:pageNumber", array("action" => "view")));
$router->addRoute(new Request_Route("/docs/introduction/:page", array("action" => "view", "isIntro" => 1)));
$router->setUri("/docs/introduction/3");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "docs");
$this->assertEqual($array["action"], "view");
$this->assertEqual($array["chapter"], "introduction");
$this->assertEqual($array["pageNumber"], 3);
$this->assertEqual($array["page"], 3);
}
public function testArraysCanExistInRoutingScheme()
{
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/items/compare/(:id[],)"));
$router->setUri("/items/compare/23,54,92");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "items");
$this->assertEqual($array["action"], "compare");
$this->assertEqual($array["id"], array(23, 54, 92));
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/orders/dispatch/(:orders[]-)"));
$router->setUri("/orders/dispatch/4535-67890-21");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "orders");
$this->assertEqual($array["action"], "dispatch");
$this->assertEqual($array["orders"], array(4535, 67890, 21));
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/orders/dispatch/(:orders[]/)"));
$router->setUri("/orders/dispatch/4535/67890/21");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "orders");
$this->assertEqual($array["action"], "dispatch");
$this->assertEqual($array["orders"], array(4535, 67890, 21));
}
public function testNamedIndicesCanBeListedInArrays()
{
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/items/search/:pref[lang]/:pref[encoding]/(:pref[],)"));
$router->setUri("/items/search/en/utf-8/small,red,none");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "items");
$this->assertEqual($array["action"], "search");
$this->assertEqual($array["pref"], array("small", "red", "none", "lang" => "en", "encoding" => "utf-8"));
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/stores/locate/:options[x]/:options[y]"));
$router->setUri("/stores/locate/parking/city");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "stores");
$this->assertEqual($array["action"], "locate");
$this->assertEqual($array["options"], array("x" => "parking", "y" => "city"));
}
public function testWilcardIsEvaluated()
{
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->setUri("/listings/buyItNow/item/76/location/uk");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "listings");
$this->assertEqual($array["action"], "buyItNow");
$this->assertEqual($array["item"], 76);
$this->assertEqual($array["location"], "uk");
$router = new Request_Router();
$router->addRoute(new Request_Route("/:module/:action/*"));
$router->setUri("/bbs/createPost/thread/103/noSmilies");
$array = array();
$router->execute($array);
$this->assertEqual($array["module"], "bbs");
$this->assertEqual($array["action"], "createPost");
$this->assertEqual($array["thread"], 103);
$this->assertTrue(array_key_exists("noSmilies", $array));
$this->assertTrue(empty($array["noSmilies"]));
}
}
Request/Router.php
Code: Select all
<?php
require_once dirname(__FILE__) . "/Route/Strategy.php";
/**
* Web Request Router for mapping URL schemes to request variables.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Router
{
/**
* Defined routing rules (schemas)
* @var array,Request_Route
*/
protected $routes = array();
/**
* The current request URI of the web page.
* @var string
*/
protected $uri;
/**
* Loaded strategy objects.
* @var array,Request_Route_Strategy
*/
protected $strategies = array();
/**
* Ctor.
* Loads in any strategies found in the Strategy dir.
*/
public function __construct()
{
$strategy_dir = dirname(__FILE__) . "/Route/Strategy";
$strategy_prefix = "Request_Route_Strategy_";
$handle = opendir($strategy_dir);
while (false !== $file = readdir($handle))
{
if (substr($file, -4) == ".php")
{
require_once $strategy_dir . "/" . $file;
$strategy = $strategy_prefix . substr($file, 0, -4);
$this->addStrategy(new $strategy(), $strategy);
}
}
closedir($handle);
}
/**
* Add a new strategy for parsing the URL.
* @param Request_Route_Strategy
* @param string A key identifier
*/
public function addStrategy(Request_Route_Strategy $strategy, $key)
{
$this->strategies[$key] = $strategy;
ksort($this->strategies);
}
/**
* Remove a strategy after loading.
* @param string The key of the strategy
*/
public function removeStrategy($key)
{
unset($this->strategies[$key]);
}
/**
* Add a new routing rule.
* @param Request_Route
*/
public function addRoute(Request_Route $route)
{
$this->routes[] = $route;
}
/**
* Get all loaded routes.
* @return array,Request_Route
*/
public function getRoutes()
{
return $this->routes;
}
/**
* Set the URI of the web page now.
* @param string Request URI
*/
public function setUri($uri)
{
$this->uri = rtrim($uri, "/");
}
/**
* Get the Request URI now.
* @return string
*/
public function getUri()
{
return $this->uri;
}
/**
* Execute the router to populate (hydrate) the request array.
* @var array The request array, optional ($_GET is used by default)
*/
public function execute(&$request = null)
{
if ($request === null) $request =& $_GET;
foreach ($this->getRoutes() as $route)
{
$uri = $route->getUri();
if (empty($uri)) continue;
//Create a regexp for the URI
$uri = preg_quote($uri, "~");
foreach ($this->strategies as $strategy)
{
$uri = $strategy->updatePattern($uri);
}
$uri = "~^" . $uri . "\$~D";
if (preg_match($uri, $this->getUri(), $matches))
{
$store = $matches;
foreach ($this->strategies as $strategy)
{
$store = $strategy->updateStore($store);
}
$route->hydrateRequest($request, $store);
}
}
}
}
Request/Route.php
Code: Select all
<?php
/**
* An individual routing rule for the Request_Router.
* Maps a URI pattern to the corresponding GET variables.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Route
{
/**
* The URI pattern this rule must match
* @var string
*/
protected $uri;
/**
* A collection of fixed value to appear in the request (e.g. module name)
* @var array,string
*/
protected $defaults = array();
/**
* Ctor.
* @param string URI pattern to match
* @param array Default values, optional
*/
public function __construct($uri, Array $defaults = null)
{
if ($defaults === null) $defaults = array();
$this->setUri($uri);
$this->setDefaults($defaults);
}
/**
* Set the URI pattern to match
* @param string Pattern
*/
public function setUri($uri)
{
$this->uri = rtrim($uri, "/");
}
/**
* Get the URI pattern
* @return string
*/
public function getUri()
{
return $this->uri;
}
/**
* Set default values in the request.
* @param array,string Values
*/
public function setDefaults(Array $defaults)
{
$this->defaults = $defaults;
}
/**
* Get default request variables.
* @return array,string
*/
public function getDefaults()
{
return $this->defaults;
}
/**
* Hydrate a given request array with values provided.
* @param array The Request array (i.e. GET or REQUEST)
* @param array Values stored from the URL
*/
public function hydrateRequest(Array &$request, Array $store)
{
$request = array_merge($request, $store);
$request = array_merge($request, $this->getDefaults());
}
}
Request/Route/Strategy.php
Code: Select all
<?php
/**
* The interface required for any schema strategies.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
interface Request_Route_Strategy
{
/**
* Update the pattern used for matching the URI.
* @param string Old pattern
* @return string New pattern
*/
public function updatePattern($pattern);
/**
* Update the stored list of request variables.
* @param array Old stored variables
* @return array New stored variables
*/
public function updateStore(Array $store);
}
The naming of the following files is to aid in ordering them correctly (looks like a conf.d layout heh?)
Request/Route/Strategy/10Wildcard.php
Code: Select all
<?php
/**
* Handles wildcards on the end of a URL (/*)
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Route_Strategy_10Wildcard implements Request_Route_Strategy
{
/**
* The key used for temporary storage of the wildcard before evaluation.
* @var string
*/
protected $key;
/**
* The search pattern
* @var string
*/
protected $search;
/**
* The replacement string
* @var string
*/
protected $replace;
/**
* Ctor.
*/
public function __construct()
{
$this->key = md5('*');
$this->search = "~/\\\\\\*\$~D";
$this->replace = "(?P<" . $this->key . ">.*)?";
}
/**
* Modify the exisiting URI pattern to handle wildcards.
* @param string Current pattern
* @return string
*/
public function updatePattern($pattern)
{
$pattern = preg_replace($this->search, $this->replace, $pattern);
return $pattern;
}
/**
* Clean up the stored list of variables.
* Removes the temporary copy and expands the list.
* @param array Request variables
* @return array
*/
public function updateStore(Array $store)
{
if (isset($store[$this->key]))
{
$uri_value = $store[$this->key];
unset($store[$this->key]);
$uri_parts = explode("/", $uri_value);
//Start at 1 since there's a leading slash
for ($i = 1; $i < count($uri_parts); $i += 2)
{
$store[$uri_parts[$i]] = isset($uri_parts[$i+1]) ? $uri_parts[$i+1] : null;
}
}
return $store;
}
}
Request/Route/Strategy/20Collection.php
Code: Select all
<?php
/**
* Handles collections (arrays) in the URL.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Route_Strategy_20Collection implements Request_Route_Strategy
{
/**
* The search pattern for URI matching.
* @var string
*/
protected $search;
/**
* The replacement pattern.
* @var string
*/
protected $replace;
/**
* Request variables we need to handle.
* @var array
*/
protected $keys = array();
/**
* Ctor.
*/
public function __construct()
{
$this->search = "~\\\\\\(\\\\:([a-zA-Z0-9_]+)\\\\\\[\\\\\\]([^\\)]+)\\\\\\)~";
$this->replace = "(?P<\$1>(?:[^/:\\?&]*?\$2?)+)";
}
/**
* Modify the current URI pattern to support collections.
* @param string Pattern
* @return string
*/
public function updatePattern($pattern)
{
if (preg_match_all($this->search, $pattern, $matches))
{
$this->keys = array_combine($matches[1], $matches[2]);
$pattern = preg_replace($this->search, $this->replace, $pattern);
}
return $pattern;
}
/**
* Expand collections according to the listed separator.
* @param array The request variable store.
* @return array
*/
public function updateStore(Array $store)
{
foreach ($this->keys as $k => $v)
{
if (isset($store[$k]))
{
$store[$k] = explode($v, $store[$k]);
}
}
return $store;
}
}
Request/Route/Strategy/30NamedIndex.php
Code: Select all
<?php
/**
* Handles arrays with named indices in the URL.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Route_Strategy_30NamedIndex implements Request_Route_Strategy
{
/**
* The URI search pattern.
* @var string
*/
protected $search;
/**
* The URI replacement string.
* @var string
*/
protected $replace;
/**
* Hashed indices for use later.
* @var array
*/
protected $hashes = array();
/**
* Ctor.
*/
public function __construct()
{
$this->search = "~\\\\:([a-zA-Z0-9_]+\\\\\\[[a-zA-Z0-9_]+\\\\\\])~e";
$this->replace = "'(?P<' . md5('\$1') . '>[^/:\\?&]+)';";
}
/**
* Update the current URI scheme pattern to support named indices.
* @param string Pattern
* @return string
*/
public function updatePattern($pattern)
{
if (preg_match_all($this->search, $pattern, $matches))
{
foreach ($matches[1] as $v)
{
$this->hashes[md5($v)] = $v;
}
$pattern = preg_replace($this->search, $this->replace, $pattern);
}
return $pattern;
}
/**
* Update the existing set of request variables to contain the correct elements.
* @param array Current request variables
* @return array
*/
public function updateStore(Array $store)
{
foreach ($this->hashes as $hash => $v)
{
$uri_value = $store[$hash];
unset($store[$hash]);
if (preg_match("~([a-zA-Z0-9_]+)\\\\\\[([a-zA-Z0-9_]+)\\\\\\]~", $v, $matches))
{
if (!isset($store[$matches[1]])) $store[$matches[1]] = array();
$store[$matches[1]][$matches[2]] = $uri_value;
}
}
return $store;
}
}
Request/Route/Strategy/40Default.php
Code: Select all
<?php
/**
* Handles basic variables in the URL.
* @package Request
* @subpackage Router
* @author Chris Corbyn
* @license LGPL
*/
class Request_Route_Strategy_40Default implements Request_Route_Strategy
{
/**
* The search pattern for variables in the URL.
* @var string
*/
protected $search;
/**
* The replacement for the URI.
* @var string
*/
protected $replace;
/**
* Ctor.
*/
public function __construct()
{
$this->search = "~\\\\:([a-zA-Z0-9_]+)~";
$this->replace = "(?P<\$1>[^/:\\?&]+)";
}
/**
* Modify the exisiting URI pattern to support basic variables.
* @param string The current pattern
* @return string
*/
public function updatePattern($pattern)
{
$pattern = preg_replace($this->search, $this->replace, $pattern);
return $pattern;
}
/**
* Clean up the stored list of variables.
* @param array Request variables.
* @return array
*/
public function updateStore(Array $store)
{
foreach ($store as $k => $v)
{
if (is_int($k)) unset($store[$k]);
}
return $store;
}
}
TODO
URL creation/rewriting
Base paths in the URL