Manual

Setting Up Routes

To setup Horde/Routes, it is assumed that you are using a web framework that has the Horde/Routes mechanism integrated for you. The web framework should have somewhere setup for you to add a Route to your Mapper.

Route (Horde_Routes_Route)
A Route is a mapping of a URL to a controller, action, and/or additional variables. Matching a Route will always result in a controller and action. Route objects are typically created and managed by the Mapper.
Mapper (Horde_Routes_Mapper)
The Mapper is the main class used to hold, organize, and match Routes. While you can create a Route object independently of the Mapper, its not nearly as useful. The Mapper is what you will use to add Routes and what the web framework uses to match incoming URLs.

We will also assume for this introduction that your Mapper instance is exposed to you as $m, for example:

$m = new Horde_Routes_Mapper();
$m->connect(':controller/:action/:id');

The above example covers one of the most common routes that is typically considered the default route. This very flexible route allows virtually all of your controllers and actions to be called. Adding more routes is done in a similar manner, by calling $m->connect(...) and giving the Mapper instance a set of arguments.

The following are all valid examples of adding routes:

  $m->connect('archives/:year/:month/:day', 
      array('controller'=>'archives', 'action'=>'view', 'year'=>2004,
      'requirements'=> array('year'=>'\d{2,4}', 'month'=>'\d{1,2}')));

  $m->connect('feeds/:category/atom.xml', 
      array('controller'=>'feeds', 'action'=>'atom'));

  $m->connect('history', 'archives/by_eon/:century', 
      array('controller'=>'archives', 'action'=>'aggregate', 'century'=>1800));

  $m->connect('article', 'article/:section/:slug/:page.html',
      array('controller'=>'article', 'action'=>'view'));

  $m->connect(':controller/:action/:id');

  $m->connect('home', '', 
      array('controller'=>'blog', 'action'=>'index'));

In the following sections, we'll highlight the section of the Route we're referring to in the first example.

Route Name

Optional

  $m->connect('history', 'archives/by_eon/:century', 
      array('controller'=>'archives...

A Route can have a name, this is also referred to as Named Routes and lets you quickly reference the Defaults that the route was configured with. This is the first non-keyword argument, and if not present the first non-keyword argument is assumed to be the route path. Route Names are mainly used when generating routes, and have no other effect on matching a URL.

Static Named Routes

Horde/Routes also supports static named routes. These are routes that do not involve actual URL generation, but instead allow you to quickly alias common URLs. For example:

$m->connect('google_search', 'http://www.google.com/search', 
  array('_static'=>True));

Static Named Routes are ignored entirely when matching a URL.

Filter Functions

Named routes can have functions associated with them that will operate on the arguments used during generation. If you have a route that requires multiple arguments to generate, like:

$m->connect('archives/:year/:month/:day', controller='archives',
          action='view', year=2004,
          requirements=dict(year='\d{2,4}', month='\d{1,2}'))

To generate a URL for this will require a month and day argument, and a year argument if you don't want to use 2004. When using Routes with a database or other objects that might have all this information, it's useful to let Routes expand that information so you don't have to.

Consider the case where you have a story object which has a year, month, and day attribute. You could generate the URL with:

$utils = $m->utils;
$utils->urlFor(array('year'  => $story->year, 
                    'month' => $story->month, 
                    'day'   => $story->day));

This isn't terribly convenient, and can be brittle if for some reason you need to change the story object's interface. Here's an example of setting up a filter function:

function story_expand($kargs) {
  // only alter $kargs if a story keyword arg is present
  if (! in_array('story', $kargs)) {
    return $kargs;
  }

  $story = $kargs['story'];
  unset ($kargs['story']);

  $kargs['year']  = $story->year;
  $kargs['month'] = $story->month;
  $kargs['day']   = $story->day;

  return $kargs;
}
    
$m->connect('archives', 'archives/:year/:month/:day',
  array('controller' => 'archives', 'action' => 'view', 'year' => 2004,
        'requirements' => array('year'=>'\d{2,4}', 'month'=>'\d{1,2}'),
        'filter'=> 'story_expand'));

This filter function will be used when using the named route archives. If a story keyword argument is present, it will use that and alter the keyword arguments used to generate the actual route.

If you have a story object with those attributes, making the route would now be done with the following arguments:

$utils = $m->utils;
$utils->urlFor('archives', array('story' => $myStory));

If the story interface changes, you can change how the arguments are pulled out in a single location. This also makes it substantially easier to generate the URL.

Warning Using the filter function requires the route to be a named route. This is due to how the filter function can affect the route that actually gets chosen. The only way to reliably ensure the proper filter function gets used is by naming the route, and using its route name with Horde_Routes_Utils->urlFor().

Route Path

Required

$m->connect('feeds/:category/atom.xml', 
  array('controller'=>'feeds', 'action'=>'atom'));

The Route Path determines the URL mapping for the Route. In the above example a URL like /feeds/electronics/atom.xml will match this route.

A Route Path is separated into parts that you define, the naming used when referencing the different types of route parts are:

Static Part

$m->connect('feeds/:category/atom.xml', 
  array('controller'=>'feeds', 'action'=>'atom'));

A plain-text part of the URL, this doesn't result in any Route variables.

Dynamic Part

$m->connect('feeds/:category/atom.xml', 
  array('controller'=>'feeds', 'action'=>'atom'))

A dynamic part matches text in that part of the URL, and assigns what it finds to the name after the : mark.

Wildcard Part

$m->connect('file/*url', 
  array('controller'=>'file', 'action'=>'serve'));

A wildcard part will match everything except the other parts around it.

Groupings

$m->connect('article', 'article/:section/:slug/:(page).html', ...

$m->connect('file/*(url).html', 
  array('controller'=>'file', 'action'=>'serve'));

Groupings let you define boundaries for the match with the () characters. This allows you to match wildcards and dynamics next to other static and dynamic parts. Care should be taken when using Groupings next to each other.

Defaults

Optional

$m->connect('history', 'archives/by_eon/:century', 
  array('controller'=>'archives', 'action'=>'aggregate', 'century'=>1800));
The keyword options in a route (not including the requirements keyword arg) that can determine the default for a route. If a default is specified for a variable that is not a dynamic part, then its not only a default but is also a hardcoded variable*. The controller and action are hardcoded variables in the example above because despite the URL, they will always be 'archives' and 'aggregate' respectively.
Hardcoded Variable
Default keyword that does not exist in the route path. This keyword variable cannot be changed by the URL coming in.

Requirements

Optional

$m->connect('archives/:year/:month/:day', 
  array('controller'=>'archives', 'action'=>'view', 'year'=>2004, 
        'requirements' => array(year='\d{2,4}', month='\d{1,2}')));

Requirements is a special keyword used by Routes to enforce a regular expression restriction on the dynamic part or wildcard part of a route path.

Note in the example above that the regular expressions do not have boundaries such as they would with a PHP function like preg_match(). The expression is simply \d{2,4} and not /\d{2,4}/.

Conditions

Optional

$m->connect('user/new;preview', 
  array('controller' => 'user', 'action' => 'preview', 
        'conditions' => array('method' => array('POST']))));

Conditions specifies a set of special conditions that must be met for the route to be accepted as a valid match for the URL. The conditions argument must always be a dictionary and can accept 3 different keys.

method
Request must be one of the HTTP methods defined here. This argument must be a list of HTTP methods, and should be upper-case.
subDomain
Can either be True or an array of sub-domains, one of which must be present.
function
A function that will be used to evaluate if the Route is a match. Must return True or False, and will be called with the environ and match_dict. The match_dict is a dict with all the Route variables for the request. Modifications to match_dict will appear identical to Route variables from the original match.

Examples:

// The method to be either GET or HEAD
m->connect('user/list', 
  array('controller' => 'user', 'action' => 'list',
        'conditions' => array('method' => array('GET', 'HEAD'))));


// A sub-domain should be present
$m->connect('', 
  array('controller' => 'user', 'action' => 'home',
        'conditions' => array('subDomain' => true)));

// Sub-domain should be either 'fred' or 'george'
$m->connect('', 
  array('controller' => 'user', 'action' => 'home',
        'conditions' => array('subDomain' => array('fred', 'george')));

  /**
   * Put the referrer into the resulting match dictionary, 
   * this won't stop the match since it always returns True
   */
  function referals($environ, $result) {
    $referer = isset($environ['HTTP_REFERER']) ? $environ['HTTP_REFERER'] : null;
    $result['referer'] = $referer;
    return true;
  }

  $m->connect(':controller/:action/:id', 
    array('conditions' => array('function'=>'referals')));