Page 2 of 2

Posted: Sat Jun 23, 2007 4:44 am
by Chris Corbyn
I'm breaking rules by successive posting here but I guess half the forum members are in bed now :P

Now it's copying values into a passed array. The last two tests should interest you :)

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

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(&$request = null)
  {
    if ($request === null) $request =& $_GET;
    
    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(), $matches))
      {
        $route->hydrateRequest($request, $matches);
      }
    }
  }
}

Code: Select all

<?php

/**
 * An individual routing rule.
 * @author Chris Corbyn
 */
class Request_Route
{
  protected $uri;
  protected $defaults = array();
  
  public function __construct($uri, Array $defaults)
  {
    $this->setUri($uri);
    $this->setDefaults($defaults);
  }
  
  public function setUri($uri)
  {
    $this->uri = $uri;
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function setDefaults(Array $defaults)
  {
    $this->defaults = $defaults;
  }
  
  public function getDefaults()
  {
    return $this->defaults;
  }
  
  public function hydrateRequest(Array &$request, Array $matches)
  {
    $uri_matches = array();
    foreach ($matches as $k => $v)
    {
      if (!is_int($k)) $uri_matches[$k] = $v;
    }
    $request = array_merge($request, $uri_matches);
    $request = array_merge($request, $this->getDefaults());
  }
}
EDIT |

Ok, and some more: Wildcards, multiple routes setting values:

Code: Select all

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

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 = rtrim($uri, "/");
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  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, "~");
      $uri = str_replace("\\*", ".*?", $uri);
      $uri = preg_replace("~\\\\:([a-zA-Z0-9_]+)~", "(?P<\$1>[^/:\\?&]+)", $uri);
      $uri = "~^" . $uri . "\$~D";
      if (preg_match($uri, $this->getUri(), $matches))
      {
        $route->hydrateRequest($request, $matches);
      }
    }
  }
}

Code: Select all

<?php

/**
 * An individual routing rule.
 * @author Chris Corbyn
 */
class Request_Route
{
  protected $uri;
  protected $defaults = array();
  
  public function __construct($uri, Array $defaults = null)
  {
    if ($defaults === null) $defaults = array();
    $this->setUri($uri);
    $this->setDefaults($defaults);
  }
  
  public function setUri($uri)
  {
    $this->uri = rtrim($uri, "/");
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function setDefaults(Array $defaults)
  {
    $this->defaults = $defaults;
  }
  
  public function getDefaults()
  {
    return $this->defaults;
  }
  
  public function hydrateRequest(Array &$request, Array $matches)
  {
    $uri_matches = array();
    foreach ($matches as $k => $v)
    {
      if (!is_int($k)) $uri_matches[$k] = $v;
    }
    $request = array_merge($request, $uri_matches);
    $request = array_merge($request, $this->getDefaults());
  }
}
TODO

Arrays in URL
Rewriting URLs
Tidying up remaining ?foo=bar&x=y&a=12 strings intelligently

Posted: Sat Jun 23, 2007 8:07 am
by Chris Corbyn
Hmm, dealing with arrays is awkward really because I need some way of representing repetition any number of times. I may address this very last because I'm a lazy programmer :P

I'm thinking only about the way the schema is represented here, but imagine this scenario.

http://site/index.php?module=items&acti ... ]=34&id=76

I can read all values in $_GET['id'] and do a comparison on them.

I might want to represent that in my clean URL as:

http://site/items/compare/12/34/76

So my schema would look like this maybe (imagine I have another router which has already picked up the module and action... my test cases already pass this scenario [EDIT | Added for clarification]):

Code: Select all

$router->addRoute(new Request_Route("/:module/:action/*"));
$router->addRoute(new Request_Route("/items/compare/:id[]/:id[]/:id[]"));
That would work, but now if I want to compare 5 items:

http://site/items/compare/12/34/76/101/150

Maybe something like:

Code: Select all

$router->addRoute(new Request_Route("/items/compare/(:id[]*)"));
Logical representation? Actually, maybe I could just use the parens to represent repetition without the * being there at all (since * already means wildcard in my current code).

I'm also wondering if you could maybe specify a separator:

Code: Select all

//Multiple values sepeated by slashes
$router->addRoute(new Request_Route("/items/compare/(:id[]/)"));

//Multiple values seperated by commas
$router->addRoute(new Request_Route("/items/compare/(:id[],)"));

//Multiple values seperated by dashes
$router->addRoute(new Request_Route("/items/compare/(:id[]-)"));
Thoughts? This is an interface choice, not an implementation detail.

EDIT | Actually, I really like that repetition separator idea... that looks amazing from an end-user point of view :) Will post code shortly.

Posted: Sat Jun 23, 2007 9:05 am
by Chris Corbyn
OK, here goes. Arrays w00t! :D (I actually am buzzing a bit, this is going better than I imagined). It's getting towards needing some refactoring though, but I'll wait for fear of optimizing too early.

Code: Select all

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

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 = rtrim($uri, "/");
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function execute(&$request = null)
  {
    if ($request === null) $request =& $_GET;
    
    $collection_search = "~\\\\\\(\\\\:([a-zA-Z0-9_]+)\\\\\\[\\\\\\](.)\\\\\\)~";
    $collection_replace = "(?P<\$1>(?:[^/:\\?&]*?\$2?)+)";
    $basic_search = "~\\\\:([a-zA-Z0-9_]+)~";
    $basic_replace = "(?P<\$1>[^/:\\?&]+)";
    
    foreach ($this->getRoutes() as $route)
    {
      $uri = $route->getUri();
      if (empty($uri)) continue;
      
      $explode_out = array();
      
      //Create a regexp for the URI
      $uri = preg_quote($uri, "~");
      $uri = str_replace("\\*", ".*?", $uri);
      //Arrays/collections
      if (preg_match_all($collection_search, $uri, $matches))
      {
        $explode_out = array_combine($matches[1], $matches[2]);
        $uri = preg_replace($collection_search, $collection_replace, $uri);
      }
      //Straightforward vars
      $uri = preg_replace($basic_search, $basic_replace, $uri);
      $uri = "~^" . $uri . "\$~D";
      if (preg_match($uri, $this->getUri(), $matches))
      {
        $parsed = $matches;
        foreach ($explode_out as $k => $v)
        {
          if (isset($parsed[$k]))
          {
            $parsed[$k] = explode($v, $parsed[$k]);
          }
        }
        $route->hydrateRequest($request, $parsed);
      }
    }
  }
}
For anyone who's not reading the test case, it basically allows URLs like:

http://www.site.com/items/compare/23,56,99
or
http://www.site.com/items/compare/23-56-99
or
http://www.site.com/items/compare/23+56+99

Or any other separator listed. The list becomes an array inside $_GET.

Now for named indices....

(PS: Is anyone reading this thread?)

TODO

Named array indices
URL rewriting/creation
Trailing query string cleanups
Allow base paths in URI

Posted: Sat Jun 23, 2007 10:16 am
by superdezign
d11wtq wrote:(PS: Is anyone reading this thread?)
I've been keeping up with it. I'm still reading the code.

Posted: Sat Jun 23, 2007 10:36 am
by Chris Corbyn
Ok, cool :) I was just starting to feel a bit like I was talking to myself :P

Named indices (definitely needs refactoring and commenting!!)

Code: Select all

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

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 = rtrim($uri, "/");
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function execute(&$request = null)
  {
    if ($request === null) $request =& $_GET;
    
    $named_index_hashes = array();
    $collection_search = "~\\\\\\(\\\\:([a-zA-Z0-9_]+)\\\\\\[\\\\\\]([^\\)]+)\\\\\\)~";
    $collection_replace = "(?P<\$1>(?:[^/:\\?&]*?\$2?)+)";
    $named_index_search = "~\\\\:([a-zA-Z0-9_]+\\\\\\[[a-zA-Z0-9_]+\\\\\\])~e";
    $named_index_replace = "'(?P<'.md5('\$1').'>[^/:\\?&]+)';";
    $basic_search = "~\\\\:([a-zA-Z0-9_]+)~";
    $basic_replace = "(?P<\$1>[^/:\\?&]+)";
    
    foreach ($this->getRoutes() as $route)
    {
      $uri = $route->getUri();
      if (empty($uri)) continue;
      
      $explode_out = array();
      
      //Create a regexp for the URI
      $uri = preg_quote($uri, "~");
      $uri = str_replace("\\*", ".*?", $uri);
      //Collections
      if (preg_match_all($collection_search, $uri, $matches))
      {
        $explode_out = array_combine($matches[1], $matches[2]);
        $uri = preg_replace($collection_search, $collection_replace, $uri);
      }
      //Named indices
      if (preg_match_all($named_index_search, $uri, $matches))
      {
        foreach ($matches[1] as $v)
        {
          $named_index_hashes[md5($v)] = $v;
        }
        $uri = preg_replace($named_index_search, $named_index_replace, $uri);
      }
      //Straightforward vars
      $uri = preg_replace($basic_search, $basic_replace, $uri);
      $uri = "~^" . $uri . "\$~D";
      
      if (preg_match($uri, $this->getUri(), $matches))
      {
        $parsed = $matches;
        foreach ($explode_out as $k => $v)
        {
          if (isset($parsed[$k]))
          {
            $parsed[$k] = explode($v, $parsed[$k]);
          }
        }
        foreach ($named_index_hashes as $hash => $v)
        {
          $uri_value = $parsed[$hash];
          unset($parsed[$hash]);
          if (preg_match("~([a-zA-Z0-9_]+)\\\\\\[([a-zA-Z0-9_]+)\\\\\\]~", $v, $matches))
          {
            if (!isset($parsed[$matches[1]])) $parsed[$matches[1]] = array();
            $parsed[$matches[1]][$matches[2]] = $uri_value;
          }
        }
        $route->hydrateRequest($request, $parsed);
      }
    }
  }
}
TODO

Trailing query string cleanups
URL creation/rewriting
Base paths in URL

Posted: Sat Jun 23, 2007 11:42 am
by Chris Corbyn
OK, trailing query strings handled (from wildcard evaluation):

Code: Select all

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"]));
  }

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 = rtrim($uri, "/");
  }
  
  public function getUri()
  {
    return $this->uri;
  }
  
  public function execute(&$request = null)
  {
    if ($request === null) $request =& $_GET;
    
    $named_index_hashes = array();
    $wildcard_index_hash = md5('*');
    $wildcard_search = "~/\\\\\\*\$~D";
    $wildcard_replace = "(?P<" . $wildcard_index_hash . ">.*)?";
    $collection_search = "~\\\\\\(\\\\:([a-zA-Z0-9_]+)\\\\\\[\\\\\\]([^\\)]+)\\\\\\)~";
    $collection_replace = "(?P<\$1>(?:[^/:\\?&]*?\$2?)+)";
    $named_index_search = "~\\\\:([a-zA-Z0-9_]+\\\\\\[[a-zA-Z0-9_]+\\\\\\])~e";
    $named_index_replace = "'(?P<' . md5('\$1') . '>[^/:\\?&]+)';";
    $basic_search = "~\\\\:([a-zA-Z0-9_]+)~";
    $basic_replace = "(?P<\$1>[^/:\\?&]+)";
    
    foreach ($this->getRoutes() as $route)
    {
      $uri = $route->getUri();
      if (empty($uri)) continue;
      
      $explode_out = array();
      
      //Create a regexp for the URI
      $uri = preg_quote($uri, "~");
      //Wildcards
      $uri = preg_replace($wildcard_search, $wildcard_replace, $uri);
      //Collections
      if (preg_match_all($collection_search, $uri, $matches))
      {
        $explode_out = array_combine($matches[1], $matches[2]);
        $uri = preg_replace($collection_search, $collection_replace, $uri);
      }
      //Named indices
      if (preg_match_all($named_index_search, $uri, $matches))
      {
        foreach ($matches[1] as $v)
        {
          $named_index_hashes[md5($v)] = $v;
        }
        $uri = preg_replace($named_index_search, $named_index_replace, $uri);
      }
      //Straightforward vars
      $uri = preg_replace($basic_search, $basic_replace, $uri);
      $uri = "~^" . $uri . "\$~D";
      
      if (preg_match($uri, $this->getUri(), $matches))
      {
        $parsed = $matches;
        foreach ($explode_out as $k => $v)
        {
          if (isset($parsed[$k]))
          {
            $parsed[$k] = explode($v, $parsed[$k]);
          }
        }
        foreach ($named_index_hashes as $hash => $v)
        {
          $uri_value = $parsed[$hash];
          unset($parsed[$hash]);
          if (preg_match("~([a-zA-Z0-9_]+)\\\\\\[([a-zA-Z0-9_]+)\\\\\\]~", $v, $matches))
          {
            if (!isset($parsed[$matches[1]])) $parsed[$matches[1]] = array();
            $parsed[$matches[1]][$matches[2]] = $uri_value;
          }
        }
        if (isset($parsed[$wildcard_index_hash]))
        {
          $uri_value = $parsed[$wildcard_index_hash];
          unset($parsed[$wildcard_index_hash]);
          $uri_parts = explode("/", $uri_value);
          //Start at 1 since there's a leading slash
          for ($i = 1; $i < count($uri_parts); $i += 2)
          {
            $parsed[$uri_parts[$i]] = isset($uri_parts[$i+1]) ? $uri_parts[$i+1] : null;
          }
        }
        $route->hydrateRequest($request, $parsed);
      }
    }
  }
}
I'm just going to take a few moments to refactor and to add some comments before I make a start on the other side of this (clean URL creation).

Posted: Sat Jun 23, 2007 1:02 pm
by Chris Corbyn
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

Posted: Sat Jun 23, 2007 2:13 pm
by Chris Corbyn
Base Paths in URIs are now dealt with in terms of request hydration at least. It's just a simple case of ignoring the base part.

Code: Select all

public function testBasePathCanBeSpecified()
  {
    $router = new Request_Router();
    $route1 = new PartialRoute();
    $route1->__construct("/:foo/:bar");
    $route1->expectOnce("hydrateRequest");
    $router->addRoute($route1);
    
    $route2 = new PartialRoute();
    $route2->__construct("/zip/button");
    $route2->expectNever("hydrateRequest");
    $router->addRoute($route2);
    
    $router->setBase("/path/to/index.php");
    $router->setUri("/path/to/index.php/abc/xyz");
    $router->execute();
  }

Code: Select all

<?php

require_once dirname(__FILE__) . "/Route/Strategy.php";
require_once dirname(__FILE__) . "/Route.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;
  /**
   * The base directory this router is being run from.
   * @var string
   */
  protected $base = "";
  /**
   * 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 base path in the URI.
   * @param string
   */
  public function setBase($base)
  {
    $this->base = $base;
  }
  /**
   * Get the base path on the URI
   * @return string
   */
  public function getBase()
  {
    return $this->base;
  }
  /**
   * 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";
      
      $page_uri = $this->getUri();
      if (($len = strlen($this->getBase())) > 0)
      {
        if (substr($page_uri, 0, $len) == $this->getBase())
        {
          $page_uri = substr($page_uri, $len);
        }
      }
      
      if (preg_match($uri, $page_uri, $matches))
      {
        $store = $matches;
        foreach ($this->strategies as $strategy)
        {
          $store = $strategy->updateStore($store);
        }
        $route->hydrateRequest($request, $store);
      }
    }
  }
}

Posted: Sat Jun 23, 2007 5:18 pm
by Chris Corbyn
Given the lack of feedback whilst working on this in-thread, I think I'll just quietly finish it and post in Critique ;)

Working the opposite direction, turning unclean URLs into clean URLs abiding (strictly) by these routing schemes is, shall we say fiddly, but I'm pretty sure I've come up with a suitable algorithm now :)

I probably won't post the finished code in Critique until tomorrow because it's 23:20 here now and I'm off to bed soon.

~arborint, does the code posted thus far clear up the earlier confusion about what I was doing?

Posted: Sat Jun 23, 2007 6:05 pm
by Jenk
I can see you've moved forward a lot, but I'll reply anyway..
d11wtq wrote: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.
Router is part of the front controller - nothing in the world of web applications will decide what your page will display, except for the request uri. Hell, it's even what is the decider of which website the user will see :P

I dislike the design of having an "action" (method) specified in the request, it just doesn't seem OO and makes your page controllers a collection of functions; I rather have the object linked to a particular route, be it dynamically ala Zend's method of ":controller" token, or by default ala my previous example, then pass all the parameters to that object and let it decide for itself which is the appropriate action to take. That is the role of page controllers after all.

Posted: Sat Jun 23, 2007 6:30 pm
by Chris Corbyn
Jenk wrote:I can see you've moved forward a lot, but I'll reply anyway..
d11wtq wrote: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.
Router is part of the front controller - nothing in the world of web applications will decide what your page will display, except for the request uri. Hell, it's even what is the decider of which website the user will see :P

I dislike the design of having an "action" (method) specified in the request, it just doesn't seem OO and makes your page controllers a collection of functions; I rather have the object linked to a particular route, be it dynamically ala Zend's method of ":controller" token, or by default ala my previous example, then pass all the parameters to that object and let it decide for itself which is the appropriate action to take. That is the role of page controllers after all.
I understand, but my direction does no disuade from being able to do what you want neither... I've deliberately tried to ensure that *any* URI scheme will work. What you do with the resulting request variables is up to you ;) Nobody says "module" and "action" have to be in there.

Code: Select all

$router = new Request_Router();
$router->addRoute(new Request_Route("/:controller/*"));
Now you could go to http://www.site.tld/preferences thus seeing $_GET['controller'] = 'preferences';. You can then make that decision to find the PreferencesController yourself. I get the feeling everybody is looking and wondering what I'm doing :lol: I'm put some working demos up when it's done.

Incidentally I'm sat here at 12:30am because I woke myself up thinking about the code for this :P

Posted: Sat Jun 23, 2007 10:34 pm
by Christopher
Jenk wrote:Router is part of the front controller - nothing in the world of web applications will decide what your page will display, except for the request uri. Hell, it's even what is the decider of which website the user will see :P
I commented on this earlier, but I am in the camp that thinks the Router modifies the Request and the Front Controller uses the Request. I have found that that simplifies the code and reduces dependencies. I also makes the system work with or without clean URLs
Jenk wrote:I dislike the design of having an "action" (method) specified in the request, it just doesn't seem OO and makes your page controllers a collection of functions; I rather have the object linked to a particular route, be it dynamically ala Zend's method of ":controller" token, or by default ala my previous example, then pass all the parameters to that object and let it decide for itself which is the appropriate action to take. That is the role of page controllers after all.
Specifying the "action" in the Request makes a lot of practical sense because they are not just collections, but methods that usually share common models and views. I have a default method like index() or run() for when no action is specified so you can do both.

Posted: Sun Jun 24, 2007 3:46 am
by Chris Corbyn
arborint wrote:I have a default method like index() or run() for when no action is specified so you can do both.
Same here, my controller's default action is plainly execute(), then there are executeAction() methods where "Action" is the name of the action being run. It falls back to the default very gracefully. And yes, we're managing just fine without clean URLs at the moment... the idea is that this router will just "plug-in" and work instantly, but if I remove it, we'll just be back to unclean URLs :)

Posted: Mon Jun 25, 2007 9:53 am
by Chris Corbyn
Just an update to say I'm not sitting on my hands :)

Almost done, but missing features are:

1. Pattern Requirements for URL components
2. Pluggable caching mechanisms to save time on lookups
3. Setting defaults for Arrays in the URL

Here's a demo of the unfinished version currently using *no* mod_rewrite. I use the <base /> tag to keep the background image functioning on each page, although mod_rewrite would be better for this :)

http://www.w3style.co.uk/~d11wtq/router ... o/demo.php

If you go to: http://www.w3style.co.uk/~d11wtq/router/trunk/

The you can click the .phps files to view source code.