Router
This PHP class matches url against predefined route patterns.
Features
- Routes can be attached to HTTP methods, and follow simple DSL syntax.
- Uses short codes for the frequently used patterns
- Provides possibility to add user defined tokens with matching regex.
- Reverse generate url from a specified route name.
Contents
Short codes
Router uses the following short codes for the common patterns:
- $ - match the string according to php variable syntax ([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)
- : - match alphanumeric string ([A-Za-z0-9]+)
- # - match numeric string ([0-9]+)
- ~ - match extension ([a-z]{1,5})
- ^ - match alphanumeric with hyphen ([A-Za-z0-9\-]+)
Predefined tokens
Router has the following predefined tokens:
- controller - match the string according to php variable syntax
- action - match the string according to php variable syntax
HTTP methods and RESTful routing
Router supports the following HTTP methods by default (and translates them to corresponding actions if needed):
- GET - view
- POST - create
- PUT - update
- DELETE - delete
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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => 'POST',
2 => 'PUT',
3 => '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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => 'POST'
),
'data' => array(
'controller' => 'news',
'action' => 'add'
)
);
?>
<?php
array(
'url' => array(
'controller' => 'news'
),
'id' => 'optional-controller-and-action',
'method' => array(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => 'POST'
),
'data' => array(
'controller' => 'news',
'action' => 'index'
)
);
?>
<?php
array(
'url' => array(
),
'id' => 'optional-controller-and-action-with-defaults',
'method' => array(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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:
<?php
array(
);
?>
<?php
array(
'url' => array(
'controller' => 'AB',
'action' => 'add'
),
'id' => 'in-place-regex',
'method' => array(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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(
0 => 'GET',
1 => '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:
- $params - associative array of keys and values that should be replaced
- $routeName - route name
- $skipOnEmpty - boolean indicating whether fill not provided parameters with default values (if set) or not
<?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'
?>