Router

This PHP class matches url against predefined route patterns.

Features

Contents

Short codes

Router uses the following short codes for the common patterns:

Predefined tokens

Router has the following predefined tokens:

HTTP methods and RESTful routing

Router supports the following HTTP methods by default (and translates them to corresponding actions if needed):

Here we use predefined controller token, and indicate that route will accept only GET and POST requests:

<?php
$router 
= new Bike\Router();

$router->add('controller-only',
    array(
        
'method' => 'GET, POST',
        
'route' => '/controller'
    
)
);

$result $router->match('GET''/news');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news'
  
),
  
'id' => 'controller-only',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'view'
  
)
);
?>

Note that action is set to 'view' by default, as there's no default value for 'action' parameter, so it's possible to implement RESTful routing using this approach.

If no method is specified, the route will accept only GET requests by default.

You can mix static and dynamic parts in the route:

<?php
$router 
= new Bike\Router();

$router->add('static-and-dynamic',
    array(
        
'method' => 'GET, POST',
        
'route' => '/r/$subreddit/comments/#thread_id/$thread_slug/'
    
)
);

$result $router->match('GET''/r/php/comments/12/router/');
?>

Result:

<?php 
array(
  
'url' => array(
    
'static0' => 'r',
    
'subreddit' => 'php',
    
'static1' => 'comments',
    
'thread_id' => '12',
    
'thread_slug' => 'router'
  
),
  
'id' => 'static-and-dynamic',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'static0' => 'r',
    
'subreddit' => 'php',
    
'static1' => 'comments',
    
'thread_id' => '12',
    
'thread_slug' => 'router',
    
'action' => 'view'
  
)
);
?>

If route will be requested using not acceptable HTTP method, RouterException with code 2 will be thrown (which can be used to send appropriate HTTP header):

<?php
use Bike\Router;
use 
Bike\RouterException;

try {
    
$router = new Router();

    
$router->add('controller-only',
        array(
            
'method' => 'GET, POST',
            
'route' => '/controller'
        
)
    );

    
$result $router->match('PUT''/news');
} catch (
RouterException $e) {
    if (
$e->getCode() === 2) {
        
$result $e->getMessage();
    }
}

// output: 'Method PUT is not allowed'
?>

If route method will contain unspecified HTTP method, RouterException with code 1 will be thrown:

<?php
use Bike\Router;
use 
Bike\RouterException;

try {
    
$router = new Router();

    
$router->add('controller-only',
        array(
            
'method' => 'GET, TEST',
            
'route' => '/controller'
        
)
    );

    
$result $router->match('GET''/news');
} catch (
RouterException $e) {
    if (
$e->getCode() === 1) {
        
$result $e->getMessage();
    }
}

// output: 'Method TEST is not supported'
?>

If route accepts all headers, an asterisk (*) could be set as a method:

<?php
use Bike\Router;
use 
Bike\RouterException;

try {
    
$router = new Router();

    
$router->add('all-methods',
        array(
            
'method' => '*',
            
'route' => '/controller'
        
)
    );

    
$result $router->match('PUT''/news');
} catch (
RouterException $e) {
    
$result $e->getMessage();
}
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news'
  
),
  
'id' => 'all-methods',
  
'method' => array(
    
=> 'GET',
    
=> 'POST',
    
=> 'PUT',
    
=> 'DELETE'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'update'
  
)
);
?>

Named, optional and default parameters

Named placeholders should consist from uppercase and lowercase letters and underscores only, without spaces, hyphens etc. e.g. $my_controller, :my_action, #some_id

<?php
$router 
= new Bike\Router();

$router->add('controller-and-action',
    array(
        
'method' => 'GET, POST',
        
'route' => '/$my_controller/:my_action'
    
)
);

$result $router->match('GET''/news/add');
?>

Results:

<?php 
array(
  
'url' => array(
    
'my_controller' => 'news',
    
'my_action' => 'add'
  
),
  
'id' => 'controller-and-action',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'my_controller' => 'news',
    
'my_action' => 'add',
    
'action' => 'view'
  
)
);
?>

Optional parameters example

<?php
$router 
= new Bike\Router();

$router->add('optional-controller-and-action',
    array(
        
'method' => 'GET, POST',
        
'route' => '/(controller(/action))'
    
)
);

$result1 $router->match('GET''/news/add');
$result2 $router->match('GET''/news');
?>

Results:

<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add'
  
),
  
'id' => 'optional-controller-and-action',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'add'
  
)
);
?>
<?php 
array(
  
'url' => array(
    
'controller' => 'news'
  
),
  
'id' => 'optional-controller-and-action',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'view'
  
)
);
?>

When using optional parameters, it makes sense to provide an array of default values:

<?php
$router 
= new Bike\Router();

$router->add('optional-controller-and-action-with-defaults',
    array(
        
'method' => 'GET, POST',
        
'route' => '/(controller(/action))',
        
'defaults' => array(
            
'controller' => 'index',
            
'action' => 'index'
        
)
    )
);

$result1 $router->match('GET''/news');
$result2 $router->match('GET''/');
?>

Results:

<?php 
array(
  
'url' => array(
    
'controller' => 'news'
  
),
  
'id' => 'optional-controller-and-action-with-defaults',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'index'
  
)
);
?>
<?php 
array(
  
'url' => array(
  ),
  
'id' => 'optional-controller-and-action-with-defaults',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'index',
    
'action' => 'index'
  
)
);
?>

Query strings, inline regular expressions and user defined tokens

If url contains query string, it will be appended to the resulting data array (query string parameters will not override parameters parsed by regular expression)

<?php
$router 
= new Bike\Router();

$router->add('query-string',
    array(
        
'method' => 'GET, POST',
        
'route' => '/(controller(/action))',
        
'defaults' => array(
            
'controller' => 'index',
            
'action' => 'index'
        
)
    )
);

$result $router->match('GET''/news/add?slug=some-slug&id=12');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add'
  
),
  
'id' => 'query-string',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'slug' => 'some-slug',
    
'id' => '12',
    
'controller' => 'news',
    
'action' => 'add'
  
)
);
?>

It is possible to use inline regular expressions:

<?php
$router 
= new Bike\Router();

$router->add('in-place-regex',
    array(
        
'method' => 'GET, POST',
        
'route' => '(/controller<[A-Z]{2}>(/action))'
    
)
);

$result1 $router->match('GET''/news/add');
$result2 $router->match('GET''/AB/add');
?>

Results:

No match
<?php 
array(
);
?>
Has match
<?php 
array(
  
'url' => array(
    
'controller' => 'AB',
    
'action' => 'add'
  
),
  
'id' => 'in-place-regex',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'AB',
    
'action' => 'add'
  
)
);
?>

It is possible to add user defined tokens using Router::addToken() method for frequently used patterns:

<?php
$router 
= new Bike\Router();

$router->addToken('page''[0-9]+');

$router->add('user-defined-token',
    array(
        
'method' => 'GET, POST',
        
'route' => '(/controller(/action(/page)))'
    
)
);

$result $router->match('GET''/news/view/12');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'view',
    
'page' => '12'
  
),
  
'id' => 'user-defined-token',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'view',
    
'page' => '12'
  
)
);
?>

If route will use undefined token, Router will consider it as a static part of the route:

<?php
$router 
= new Bike\Router();

$router->addToken('non_static_1''[a-z]+');
$router->addToken('non_static_2''[a-z]+');

$router->add('user-defined-token',
    array(
        
'method' => 'GET, POST',
        
'route' => '/non_static_1/undefined/non_static_2'
    
)
);

$result $router->match('GET''/some/undefined/tokens');
?>

Result:

<?php 
array(
  
'url' => array(
    
'non_static_1' => 'some',
    
'static0' => 'undefined',
    
'non_static_2' => 'tokens'
  
),
  
'id' => 'user-defined-token',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'non_static_1' => 'some',
    
'static0' => 'undefined',
    
'non_static_2' => 'tokens',
    
'action' => 'view'
  
)
);
?>

Other examples

<?php
$router 
= new Bike\Router();

$router->add('article-with-slug',
    array(
        
'method' => 'GET, POST',
        
'route' => '/controller-action(/^slug)',
        
'defaults' => array(
            
'controller' => 'index',
            
'action' => 'index',
            
'format' => 'html'
        
)
    )
);

$result $router->match('GET''/news-add/some-article-title');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'slug' => 'some-article-title'
  
),
  
'id' => 'article-with-slug',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'format' => 'html',
    
'slug' => 'some-article-title'
  
)
);
?>
<?php
$router 
= new Bike\Router();

$router->addToken('y''[0-9]{4}');
$router->addToken('m''[0-9]{2}');
$router->addToken('d''[0-9]{2}');

$router->add('article-with-date-and-slug',
    array(
        
'method' => 'GET, POST',
        
'route' => '(/controller)(/action(.~format))(/y-m-d(/^slug))',
        
'defaults' => array(
            
'controller' => 'index',
            
'action' => 'index',
            
'format' => 'html'
        
)
    )
);

$result $router->match('GET''/articles/2009-01-01/some-slug-for-article');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'articles',
    
'y' => '2009',
    
'm' => '01',
    
'd' => '01',
    
'slug' => 'some-slug-for-article'
  
),
  
'id' => 'article-with-date-and-slug',
  
'method' => array(
    
=> 'GET',
    
=> 'POST'
  
),
  
'data' => array(
    
'controller' => 'articles',
    
'action' => 'index',
    
'format' => 'html',
    
'y' => '2009',
    
'm' => '01',
    
'd' => '01',
    
'slug' => 'some-slug-for-article'
  
)
);
?>

Routes order, mass assignment and compilation

As the Router works on a first-match basis, it's recommended to define routes in order of specificity, from most specific to general ones.

<?php
$router 
= new Bike\Router();

$router->add('controller-action-id',
    array(
        
'method' => 'GET, POST',
        
'route' => '/controller/action/#id'
    
)
);

$router->add('controller-action',
    array(
        
'method' => 'GET, POST',
        
'route' => '/controller/action'
    
)
);

$router->add('controller',
    array(
        
'method' => 'GET, POST',
        
'route' => '/controller'
    
)
);
?>

Predefined routes

It's possible to have predefined routes in some php file, and provide them to Router as a parameter for constructor:

<?php
return array(
    
'controller-action-id' => array(
        
'route'     => '/controller/action/#id'
    
),
    
'controller-action' => array(
        
'route'     => '/controller/action'
    
),
    
'general' => array(
        
'route'     => '(/controller)(/action(.format<[a-z]{2,4}>))(/#id)(/slug<[A-Za-z0-9\-]+>)',
        
'defaults'  => array(
            
'controller' => 'index',
            
'action' => 'index',
            
'format' => 'html',
            
'id' => 1,
            
'slug' => 'default-slug'
        
)
    )    
);
?>
<?php
$routes 
= include 'predefined_routes.php';

$router = new Bike\Router($routes);

$result1 $router->match('GET''/news/add.xml/12/some-slug');
$result2 $router->match('GET''/news/add/12');
$result3 $router->match('GET''/news/add');
?>

Result:

<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'format' => 'xml',
    
'id' => '12',
    
'slug' => 'some-slug'
  
),
  
'id' => 'general',
  
'method' => 'GET',
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'format' => 'xml',
    
'id' => '12',
    
'slug' => 'some-slug'
  
)
);
?>
<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'id' => '12'
  
),
  
'id' => 'controller-action-id',
  
'method' => 'GET',
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'add',
    
'id' => '12'
  
)
);
?>
<?php 
array(
  
'url' => array(
    
'controller' => 'news',
    
'action' => 'add'
  
),
  
'id' => 'controller-action',
  
'method' => 'GET',
  
'data' => array(
    
'controller' => 'news',
    
'action' => 'add'
  
)
);
?>

Using Router::compile method it's possible to get php code of routes array with compiled regular expressions, so Router will skip route to regex conversion thus increase its performance

<?php
$routes 
= include 'predefined_routes.php';

$router = new Bike\Router($routes);

$result $router->compile(false);
?>

Result:

<?php return array (
  
'controller-action-id' => 
  array (
    
'route' => '/controller/action/#id',
    
'regex' => '/^\/(?P<controller>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\/(?P<action>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\/(?P<id>[0-9]+)$/D',
  ),
  
'controller-action' => 
  array (
    
'route' => '/controller/action',
    
'regex' => '/^\/(?P<controller>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\/(?P<action>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/D',
  ),
  
'general' => 
  array (
    
'route' => '(/controller)(/action(.format<[a-z]{2,4}>))(/#id)(/slug<[A-Za-z0-9\-]+>)',
    
'defaults' => 
    array (
      
'controller' => 'index',
      
'action' => 'index',
      
'format' => 'html',
      
'id' => 1,
      
'slug' => 'default-slug',
    ),
    
'regex' => '/^(?:\/(?P<controller>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))?(?:\/(?P<action>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:\.(?P<format>[a-z]{2,4}))?)?(?:\/(?P<id>[0-9]+))?(?:\/(?P<slug>[A-Za-z0-9\-]+))?$/D',
  ),
); 
?>

Reverse URL generation for a given route

URLs can be generated using Router::url() method, which accepts 3 arguments:

<?php
$router 
= new Bike\Router();

$router->addToken('page''[0-9]+');

$router->add('url',
    array(
        
'method' => 'GET',
        
'route' => '(/controller(/action(/page)))',
        
'defaults' => array(
            
'controller' => 'index',
            
'action' => 'index',
            
'page' => 1
        
)
    )
);

$router->add('static-and-dynamic',
    array(
        
'method' => 'GET, POST',
        
'route' => '/r/$subreddit/comments/$thread_id/$thread_slug/'
    
)
);

$result1 $router->url(array(
    
'controller' => 'news',
    
'page' => 2
), 'url');

// output: '/news/index/2'

$result2 $router->url(array(
    
'controller' => 'news',
    
'page' => 2
), 'url'true);

// output: '/news/2'

$result3 $router->url(array(
    
'subreddit' => 'javascript',
    
'thread_id' => '10',
    
'thread_slug' => 'router',
), 
'static-and-dynamic');

// output: '/r/javascript/comments/10/router'
?>