Writing a request router

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Writing a request router

Post by Chris Corbyn »

I'm currently in the process of going through a "framework" I have to work with on a daily basis and refactoring things as well as adding features I'd consider required in a framework. Anyway, one f these missing features is a router. At the moment all the URLs are structured, but messy and definitely not search engine friendly.... heck, there's even a fundamental part of the URL which contains [] characters to make an array.

I've never written a request router before although as a concept it seems like a trivial idea. I'd quite like to write it here, with unit tests and feedback (*cough* inout/help) as I go if anyone's interested?

Here's a requirements:

* Must be able to specify routes/URIs dynamically (i.e. configurable like mod_rewrite)
* Must be able to help our hyperlink() and url() functions tidy up URLs (not too sure what direction all go here)
* Must push variables directly into $_GET. Our Request object will see these anyway.
* Must be clever enough to tidy up trialing query strings without explicitly specifying them.
* (Bonus) Pluggable -- probably pointless.

I know I could go an look an exisiting implemetation but that would take originality away from it. Anyone interested in following this/helping out? :)
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

I wrote one of my own that does this, both in PHP and a different one in smalltalk. It's actually very easy if you just remember to single out the various behaviours into their own respective classes (like in any OO circumstance.)

The only difficulty I had, was deciding on an appropriate interface and logic flow between the router, it's routes and the controller receiving parameters.

In the end I opted for using a token object (ala value object) and nominating a "controller" parameter which must be set, and supplying a default controller with the route, if one is not parsed in the url.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Writing a request router

Post by Christopher »

I'm interested, if I understand you correctly (i.e., mapping PATH_INFO onto _GET). I think we need to define the number of different routing schemes that people use. Then build a clean, pluggable/extensible codebase to support those schemes.
(#10850)
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

Hopefully this might help get things started, this is a class I wrote awhile back that I extend and reuse quite a bit. I create an extended class for a given URL structure, then I use it on page load to get the current URL and split it into _GET parameters.

I also inject this object into my controllers and clone it whenever I create a link, then pass it to the view to be echoed. If I want to change the URL structure for the entire site all I need to do is create a new extended URL object that is the new format and pass that to the controllers instead.


This is the simple version, it just handles your standard run of the mill page.php?var=value type URLs but its easy to extend for something like /controller/action/var/value.

Code: Select all

<?php


/**
 * URL class for parsing and managing URLs.
 *
 * @author Jeff Dorsch
 * @package FCP
 * 
 */
class FCP_Framework_URL implements Countable
{
	
	/**
	 * The URL parameters in an associative array
	 *
	 * @var array
	 * @access private
	 */
	protected $params = array() ;
	
	/**
	 * The server port
	 *
	 * @var string
	 */
	private $port ;
	
	/**
	 * The URL prefix, i.e. ftp, http, https etc.
	 *
	 * @var string
	 */
	private $prefix ;
	
	/**
	 * The host : http://www.example.com
	 *
	 * @var string
	 */
	private $host ;
	
	/**
	 * The page fragment, this is the anchor tag name following a '#' at the end of a URL
	 *
	 * @var string
	 */
	private $fragment ;
	
	/**
	 * URL user name
	 *
	 * @var string
	 */
	private $user ;
	
	/**
	 * URL password
	 *
	 * @var string
	 */
	private $pass ;
	
	/**
	 * The path up to and including the file name being called on the server
	 * 
	 * i.e.
	 * /path/on/server
	 * /path/on/server/index.php
	 * /some/other/path/file.html
	 *
	 * @var string
	 */
	private $path ;
	
	
	/**
	 * Constructor
	 *
	 * @param string $stringURL Intitial URL used to build the object
	 */
	public function __construct($stringURL = null)
	{
		if ($stringURL !== null)
		{
			if (!$this->buildFromStringURL($stringURL)) throw new FCP_Exception('Invalid URL passed to FCP_URL::__construct()') ;
		}
	}
	
	
	/**
	 * Set the path
	 *
	 * @param string $path
	 */
	public function setPath($path)
	{
		$this->path = $path ;
	}
	
	
	/**
	 * Get the path, up to and including the file name being referenced
	 *
	 * @return string
	 */
	public function getPath()
	{
		return $this->path ;
	}
	
	
	/**
	 * Set the prefix, i.e. http, https, ftp etc.
	 *
	 * @param string $prefix
	 */
	public function setPrefix($prefix)
	{
		$this->prefix = $prefix ;
	}
	
	/**
	 * Get the prefix
	 *
	 * @return string
	 */
	public function getPrefix()
	{
		return $this->prefix ;
	}
	
	
	/**
	 * Set the host name i.e. http://www.example.com
	 *
	 * @param string $host
	 */
	public function setHost($host)
	{
		$this->host = $host ;
	}
	
	/**
	 * Get the hjost name
	 *
	 * @return string
	 */
	public function getHost()
	{
		return $this->host ;
	}
	
	/**
	 * Set the port
	 *
	 * @param string $port
	 */
	public function setPort($port)
	{
		$this->port = $port ;
	}
	
	
	/**
	 * Get the port
	 *
	 * @return string
	 */
	public function getPort()
	{
		return $this->port ;
	}
	
	
	/**
	 * Set the user
	 *
	 * @param string $user
	 */
	public function setUser($user)
	{
		$this->user = $user ;
	}
	
	
	/**
	 * Get the user
	 *
	 * @return string
	 */
	public function getUser()
	{
		return $this->user ;
	}
	
	/**
	 * Set the password
	 *
	 * @param string $pass
	 */
	public function setPass($pass)
	{
		$this->pass = $pass ;
	}
	
	/**
	 * Get the password
	 *
	 * @return string
	 */
	public function getPass()
	{
		return $this->pass ;
	}

	
	/**
	 * Set the query string, this should not be prepended with a '?'
	 *
	 * @param string $query
	 */
	public function setQueryString($query)
	{
		$this->params = array() ;
		$parts = explode('&', $query) ;
		$this->params = array() ;
		foreach ($parts as $part)
		{
			$pair = explode('=', $part) ;
			$this->params[$pair[0]] = $pair[1] ; 
		}
	}
	
	/**
	 * Get the query string, this is returned prepended iwth a '?' or blank
	 *
	 * @return string
	 */
	public function getQueryString()
	{
		$pairs = array() ;
		foreach($this->params as $key => $value)
		{
			$pairs[] = "{$key}={$value}" ;
		}
		$query = implode('&', $pairs) ;
		return (strlen($query) > 0) ? '?'.$query : '' ;		
	}
	
	
	/**
	 * Set the fragment (string following the '#' at the end of a URL)
	 *
	 * @param string $fragment
	 */
	public function setFragment($fragment)
	{
		$this->fragment = $fragment ;
	}
	
	
	/**
	 * Get the fragment
	 *
	 * @return string
	 */
	public function getFragment()
	{
		return $this->fragment ;
	}
	
	
	
	/**
	 * Magic string method, returns the URL as a string
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->getURLAsString() ;
	}
	
	
	/**
	 * Get the value of a parameter
	 *
	 * @param string $paramName
	 * @return mixed
	 */
	public function __get($paramName)
	{
		return $this->getParam($paramName) ;
	}
	
	/**
	 * Set a parameter value
	 *
	 * @param string $paramName
	 * @param mixed $value
	 */
	public function __set($paramName, $value)
	{
		$this->setParam($paramName, $value) ;
	}
	
	
	/**
	 * Unset a parameter
	 *
	 * @param string $paramName
	 */
	public function __unset($paramName)
	{
		$this->removeParam($paramName);
		return $this ;
	}
	
	
	/**
	 * Check if a parameter is set
	 *
	 * @param string $paramName
	 * @return bool
	 */
	public function __isset($paramName)
	{
		return isset($this->params[$paramName]) ;
	}
	
	
	/**
	 * Set the parameters from an associative array keyed by parameter name
	 *
	 * @param array $paramsArray
	 */
	public function setParamsFromArray($paramsArray)
	{
		foreach( $paramsArray as $param => $value)
		{
			if( $value !== null)
			{
				$this->setParam($param, $value);
			}
			else
			{
				$this->removeParam($param) ;
			}
		}
	}
	

	
	/**
	 * Get the URL as a string
	 *
	 * @return string
	 */
	public function getURLAsString()
	{
		$url = '' ;
		$url = ($this->prefix != '') 	? $this->prefix.'://' 									: '' ;
		$url.= ($this->user != '') 		? $this->user.($this->pass ? ':'.$this->pass:'').'@'	: '' ;
		$url.= ($this->host != '') 		? $this->host 											: '' ;
		$url.= ($this->port != '') 		? ':'.$this->port 										: '' ;
		$url.= ($this->path != '') 		? $this->path 											: '' ;
		$url.= $this->getQueryString() 																 ;
		$url.= ($this->fragment != '') 	? '#'.$this->fragment 									: '' ;
		return $url ;
	}
	
	
	/**
	 * Set a parameter value
	 *
	 * @param string $paramName
	 * @param mixed $value
	 */
	public function setParam($paramName, $value)
	{
		$this->params[$paramName] = $value ;
		return $this ;
	}
	
	
	/**
	 * Get a parameter value
	 *
	 * @param string $paramName
	 * @return mixed
	 */
	public function getParam($paramName)
	{
		return (isset($this->params[$paramName])) ? $this->params[$paramName] : null ;
	}
	
	
	/**
	 * Get the parameters as an array
	 * 
	 * @return array
	 */
	public function getParams()
	{
		return $this->params ;
	}
	
	/**
	 * Remove a parameter from the stack
	 *
	 * @param string $paramName
	 */
	public function removeParam($paramName)
	{
		unset($this->params[$paramName]) ;
		return $this ;
	}
	
	
	/**
	 * Get the number of parameters defined for this url
	 *
	 * @return int
	 */
	public function count()
	{
		return count($this->params) ;
	}
	
	
	/**
	 * Build the url object from an existing URL string
	 *
	 * @param string $url
	 */
	public function buildFromStringURL($stringURL)
	{
		if (!($parts = @parse_url($stringURL))) return false ;
		$this->setPrefix($parts['scheme']);
		$this->setHost($parts['host']);
		$this->setPort($parts['port']);
		$this->setUser($parts['user']);
		$this->setPass($parts['pass']);
		$this->setPath($parts['path']);
		$this->setQueryString($parts['query']);
		$this->setFragment($parts['fragment']);
		return true ;
	}
	
	
	/**
	 * Build the object using the current URL
	 *
	 */
	public function buildFromCurrentURL()
	{
	  $ports = array('https' => 443, 'http' => 80);
	  $prefix = empty($_SERVER['HTTPS']) ? 'http' : 'https';
	  $url = $prefix;
	  $url .= $_SERVER['SERVER_PORT'] != $ports[$prefix] ? ':' . $_SERVER['SERVER_PORT'] : '';
	  $url .= '://';
	  $url .= $_SERVER['HTTP_HOST'];
	  $url .= $_SERVER['REQUEST_URI'];
	  $this->buildFromStringURL($url) ;
	}
	 

	/**
	 * Clear out the URL params
	 *
	 */
	public function clearParams()
	{
		$this->params = array() ;
	}
	
}

?>
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I think the basics of that class might be useful, but for me it is too much. I have found that 99.9% of the time all I care about is the base URL and the parameters. Sometimes I might want support to maintain the scheme as HTTP or HTTPS. But that's about it. And there is not clean URL support.

This is unfortunately one of those classes that, in my opinion, its design is forced by the design the controllers and views -- it's a support class. That's probably why they are usually pretty non-standard from framework to framework.
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Yeah this is why I wasn't going to pull up a finished version first.... it jades the way you code. Start basic, then build on it. I'm just off to work but I'll come back later to discuss routing schemes as suggested by ~arborint :)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ok, here's the deal. The URL in our framework is vital to determining:

The module being loaded
The action being executed

Everything else is just appended to the URL. The unfriendly version looks something like:

index.php?___module___=behaviour&___action___=enterSlip&some=other&args

It wouldn't suffice to change that to:

/___module___/behaviour/___action___/enterSlip/some/other/args

In fact, I'd like to be able to entirely mask the names of the modules and actions and simply handle that internally, so we could maybe access the above page on:

/incidents/new/some/other/args

Tying it down to just the module/action names isn't good enough neither. To all intent and purpose those are just like any of the other arguments so I'd even like to be able to map things like:

index.php?___module___=behaviour&___action___=update&state=1
index.php?___module___=behaviour&___action___=update&state=0

To

/behaviour/update/enable
/behaviour/update/disable

It's all just regex work basically. The URLs in our app are handled by functions so it's easy for us to change the format of them by modifying these functions.

One thing I would like to see support for out of interest is this:

index.php?foo[]=something&foo[]=somethingElse&bar=1

Here $_GET['foo'] is an array with two elements. I wonder how that can be represented in a friendly URL? Maybe

/foo/something,somethingElse/bar/1

In terms of interface I'm thinkining along the lines of:

Code: Select all

$router->addRoute("some%pattern%here", "actual url here");
We could pull those from a config file.

Anyone got anything to add in terms of possible schemas which I might need to address? :)
User avatar
kyberfabrikken
Forum Commoner
Posts: 84
Joined: Tue Jul 20, 2004 10:27 am

Post by kyberfabrikken »

d11wtq wrote: Tying it down to just the module/action names isn't good enough neither. To all intent and purpose those are just like any of the other arguments so I'd even like to be able to map things like:
A URL is made up by a path and a query-string. Rather than limiting yourself to having one channel for addressing, it might be worth considering using both these standard ways?
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ah yes, I'm so used to working with a single index.php I never even considered that you might need a real path in there.
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

Why not bind your url's to just one object, then let that object decide what action to perform/method to invoke/costume to wear?

Code: Select all

$router->addRoute(new Route('/foo/:param1/:param2/:param3', 'SomeControllerClass'));
where "SomeControllerClass" is class name of the object that will perform, and will accept an array of parameters, denoted by the data at the ':' tokens, followed by the name..

Code: Select all

http://host/foo/somestuff/someotherstuff/somemorestuff
so the above translates to:

Code: Select all

//this is translated by the router/route and would appear like this if var_export()ed.
$params = array(
    'param1' => 'somestuff',
    'param2' => 'someotherstuff',
    'param3' => 'somemorestuff'
);
$controllerClass = $router->controller();
$control = new {$controllerClass}; // unsure of syntax errors but you get my drift.
$control->params($params);
$control->run();
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

That would not do what I want. I'm effectively trying to write basic mod_rewrite in PHP code. It's not only about what controller to use... what about actions and request parameters?

Code: Select all

$router->addRoute("/some/:thing/here/:and/:here", array("foo" => "bar", "zip" => "button"));
That route would yield $_GET variables:

thing = ?
and = ?
here = ?
foo = bar
zip = button

Where ? are the actual values in that part of the URL. I'd rather let my front controller decide which page controller should be used.... not my router.

EDIT | Sorry, I mis-read your post, but I still would rather keep controller choices out of the router... that's the front controller's job. The router would however specify the module which is loosely the same thing.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Here goes. Nothing functional yet, I'm just dreaming up an interface.

Test Case

Code: Select all

<?php

Mock::Generate("Request_Route", "MockRoute");
Mock::GeneratePartial("Request_Route", "PartialRoute", array("hydrate"));

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("hydrate");
		$router->addRoute($route1);
		
		$route2 = new PartialRoute();
		$route2->__construct("/zip/button", array());
		$route2->expectNever("hydrate");
		$router->addRoute($route2);
		
		$router->setUri("/foo/bar");
		$router->execute();
	}
}
Runner (for anyone who wants it :?)

Code: Select all

<?php

error_reporting(E_ALL); ini_set("display_errors", true);

define("SIMPLETEST_DIR", "/Users/d11wtq/PHPLibs/simpletest");
define("LIB_PATH", "../lib");

//SimpleTest files
require_once SIMPLETEST_DIR . "/unit_tester.php";
require_once SIMPLETEST_DIR . "/mock_objects.php";
require_once SIMPLETEST_DIR . "/reporter.php";

//Library files
require_once LIB_PATH . "/Request/Router.php";
require_once LIB_PATH . "/Request/Route.php";

//Test cases
require_once "cases/TestOfRouter.php";

$tests = new GroupTest("Router tests");
$tests->addTestCase(new TestOfRouter());
$tests->run(new HtmlReporter());
Router Classes

Code: Select all

<?php

/**
 * Web Request Router class.
 * @author Chris Corbyn
 */
class Request_Router
{
	protected $routes = array();
	
	protected $uri;
	
	public function addRoute(Request_Route $route)
	{
		$this->routes[] = $route;
	}
	
	public function getRoutes()
	{
		return $this->routes;
	}
	
	public function setUri($uri)
	{
		$this->uri = $uri;
	}
	
	public function getUri()
	{
		return $this->uri;
	}
	
	public function execute()
	{
		foreach ($this->getRoutes() as $route)
		{
			$uri = $route->getUri();
			if ($uri == $this->getUri())
			{
				$route->hydrate($_GET);
			}
		}
	}
}

Code: Select all

<?php

/**
 * An individual routing rule.
 * @author Chris Corbyn
 */
class Request_Route
{
	protected $uri;
	
	public function __construct($uri, Array $vars)
	{
		$this->setUri($uri);
	}
	
	public function setUri($uri)
	{
		$this->uri = $uri;
	}
	
	public function getUri()
	{
		return $this->uri;
	}
	
	public function hydrate(Array $request)
	{
		//
	}
}
I'll keep going. Grr!!! Jus noticed I've got text mate putting real tabs in :evil:
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:Ok, here's the deal. The URL in our framework is vital to determining:

The module being loaded
The action being executed

Everything else is just appended to the URL. The unfriendly version looks something like:

index.php?___module___=behaviour&___action___=enterSlip&some=other&args

It wouldn't suffice to change that to:

/___module___/behaviour/___action___/enterSlip/some/other/args
Ok ... I am starting out a little confused ... hopefully it will get better. The point of clean URLs is to replace the "name=value" scheme with just "/value/" where the "name" is implicit by position -- just like function parameters. It is a shorthand notation for parameters that always/mostly occur. So to me it would be: "/behaviour/enterSlip/some/other/args"
d11wtq wrote:In fact, I'd like to be able to entirely mask the names of the modules and actions and simply handle that internally, so we could maybe access the above page on:

/incidents/new/some/other/args
This requires that the system get the missing information from somewhere. It could be elsewhere in the URL as kyberfabrikken proposes or from the session or some default setting. I guess it could also be inferred from the parameter list (which might be interesting) so that if the list was a string and for integers then it went to this page, but two dates went to another.
d11wtq wrote:Tying it down to just the module/action names isn't good enough neither. To all intent and purpose those are just like any of the other arguments so I'd even like to be able to map things like:

index.php?___module___=behaviour&___action___=update&state=1
index.php?___module___=behaviour&___action___=update&state=0

To

/behaviour/update/enable
/behaviour/update/disable

It's all just regex work basically. The URLs in our app are handled by functions so it's easy for us to change the format of them by modifying these functions.
There are several ways to solve things like this. It could be regex, or a hash lookup/translation table, or a transform function. All should be supported, but is there a default?
d11wtq wrote:One thing I would like to see support for out of interest is this:

index.php?foo[]=something&foo[]=somethingElse&bar=1

Here $_GET['foo'] is an array with two elements. I wonder how that can be represented in a friendly URL? Maybe

/foo/something,somethingElse/bar/1
This could probably be handled by directly parsing the URL. PHP is what limits multiple params. This system could convert to an array if it got the same param more than once. That capability should probably be set OFF by default and need to be turned on per parameter.
d11wtq wrote:In terms of interface I'm thinkining along the lines of:

Code: Select all

$router->addRoute("some%pattern%here", "actual url here");
We could pull those from a config file.

Anyone got anything to add in terms of possible schemas which I might need to address? :)
usually this goes the RoR way with routes defined like Zend Framework is doing it. That is certainly one way to do it, but not always the best. Perhaps something more customizable, even as simple a change as:

Code: Select all

$router->addRoute(new Route_Regexp("some%pattern%here", "actual url here"));
$router->addRoute(new Route_Simple("actual url here"));
$router->add(new Route_Config($Config->get('routes')));
I prefer that the Router modifies the Request rather than feeds the Dispatcher. That reduces dependencies and increases customizabliity.

I also think it is important to acknowledge that the reason for this type of "routing" is mainly for a Front Controller dispatching Action Controllers. The PATH_INFO string usually starts with the following for practical reasons:

/class/method/
or
/module directory/class/method/
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Hi ~arborint, hopefully the confusion will be cleared up as the code evolves... I'm aiming towards something like symfony's request router :)

/:___module___/:___action___ would be a route which allows URLs like /incidents/delete

As for specifying the "missing" variables, they are defined in the routing rule. I'm fairly clear what direction I'm headed with this so I'll proceed with the code until get cries of confusion regarding the code :)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ok, now we're moving in the direction of allowing variables in the URL. I feel like I've walked into this in a bit of an unorthodox fashion but it may just work very nicely :)

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();
  }
}

Code: Select all

<?php

/**
 * Web Request Router class.
 * @author Chris Corbyn
 */
class Request_Router
{
  protected $routes = array();
  
  protected $uri;
  
  public function addRoute(Request_Route $route)
  {
    $this->routes[] = $route;
  }
  
  public function getRoutes()
  {
    return $this->routes;
  }
  
  public function setUri($uri)
  {
    $this->uri = $uri;
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function execute()
  {
    foreach ($this->getRoutes() as $route)
    {
      $uri = $route->getUri();
      if (empty($uri)) continue;
      
      $uri = preg_quote($uri, "~");
      //Create a regexp for the URI
      $uri = preg_replace("~\\\\:([a-zA-Z0-9_])+~", "(?P<\$1>[^/:\\?&]+)", $uri);
      $uri = "~^" . $uri . "\$~D";
      if (preg_match($uri, $this->getUri()))
      {
        $route->hydrateRequest($_GET);
      }
    }
  }
}
EDIT | I'll have to disable smilies in my post cos the GeSHi mod is broken with them.
Post Reply