Routing
Routing is a two-way conversion between URL and presenter action. Two-way means we can both determine what presenter URL links to, but also vice versa: generate URL for given action. This article contains:
- how to define routes and create links
- a few notes about SEO redirection
- how to debug defined routes
- how to create your own router
What is Routing?
Routing is a two-way conversion between URL and an application request.
Nette\Http\IRequest
(includes URL) →Nette\Application\Request
Nette\Application\Request
→ absolute URL
Thanks to bidirectional routing you don't have to hardcode URLs into templates anymore, you simply link to presenters' actions and framework generates the URLs for you:
{* creates a link to presenter 'Product' and action 'detail' *}
<a n:href="Product:detail $productId">product detail</a>
Learn more about creating links.
Routing is a separate application layer. This allows you to very efficiently manipulate with the URL structure without the need to modify the application itself. It's simple to change routes anytime, while keeping the original addresses preserved and automatically redirect to the new variants. So hey, who's got that? :-)
SimpleRouter
Desired URL format is set by a router. The most plain implementation of router is SimpleRouter. It can be used when there's no
need for a specific URL format, when mod_rewrite
(or alternatives) is not available or when we simply do not want to
bother with user-friendly URLs yet.
Generated addresses will look like this:
http://example.com/?presenter=Product&action=detail&id=123
The first parameter of the SimpleRouter
constructor is a default presenter action, ie. action to be executed if we
open e.g. http://example.com/
without additional parameters.
// defaults to presenter 'Homepage' and action 'default'
$router = new Nette\Application\Routers\SimpleRouter('Homepage:default');
The second constructor parameter is optional and is used to pass additional flags (only
SimpleRouter::ONE_WAY
for unidirectional route and SimpleRouter::SECURED
for HTTPS enabled route are
supported).
The recommended way to configure application to use SimpleRouter
is to use configuration file (e.g. config.neon
):
services:
router: Nette\Application\Routers\SimpleRouter('Homepage:default')
Route: for prettier URLs
Human-friendly URLs (also more cool & prettier) are easier to remember and do help SEO. Nette Framework keeps current trends in mind and fully meets developers' desires.
All requests must be handled by index.php
file. This can be accomplished e.g. by using Apache module
mod_rewrite
or Nginx's try_files
directive (see how to configure a server for nice URLs).
Class Route is able to create addresses in pretty
much any format one can though of. Let's start with a simple example, generating the following pretty URL for action
Product:default
with id = 123
:
http://example.com/product/detail/123
The following snippet creates a Route
object, passing path mask as the first argument and specifying default
action in the second argument. We may pass additional flags using the third argument.
// action defaults to presenter Homepage and action default
$route = new Route('<presenter>/<action>[/<id>]', 'Homepage:default');
// alternatively written using an array
$route = new Route('<presenter>/<action>[/<id>]', array(
'presenter' => 'Homepage',
'action' => 'default'
));
This route is usable by all presenters and actions. Accepts paths such as /article/edit/10
or
/catalog/list
, because the id
part is wrapped in square brackets, which marks it as optional.
Because other parameters (presenter
and action
) do have default values (Homepage
and
default
), they are optional too. If their value is the same as the default one, they are skipped while URL is
generated. Link to Product:default
generates only http://example.com/product
and link to
Homepage:default
generates only http://example.com/
.
Path Mask
The simplest path mask consists only of a static URL and a target presenter action.
$route = new Route('products', 'Products:default');
Most real masks however contain some parameters. Parameters are enclosed in angle brackets (e.g.
<year>
) and are passed to the target presenter.
$route = new Route('history/<year>', 'History:view');
Mask can also contain traditional GET arguments (query after a question mark). Neither validation expressions nor more complex structures are supported in this part of path mask, but you can set what key belongs to which variable:
// use GET parameter "cat" as a "categoryId" in our application
$route = new Route('<presenter>/<action> ? id=<productId> & cat=<categoryId>', ...);
The parameters before a question mark are called path parameters and the parameters after a question mark are called query parameters.
Mask can not only describe path relative to application document root (web root), but can as well contain path relative to server document root (starts with a single slash) or absolute path with domain (starts with a double slash).
// relative to application document root (www directory)
$route = new Route('<presenter>/<action>', ...);
// relative to server document root
$route = new Route('/<presenter>/<action>', ...);
// absolute path including hostname
$route = new Route('//<subdomain>.example.com/<presenter>/<action>', ...);
Absolute path mask may utilize the following variables:
%tld%
= top level domain, e.g.com
ororg
%domain%
= second level domain, e.g.example.com
%basePath%
$route = new Route('//www.%domain%/%basePath%/<presenter>/<action>', ...);
Route Collection
Because we usually define more than one route, we wrap them into a RouteList.
use Nette\Application\Routers\RouteList,
Nette\Application\Routers\Route;
$router = new RouteList();
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('article/<id>', 'Article:view');
$router[] = new Route('<presenter>/<action>[/<id>]', 'Homepage:default');
Unlike other frameworks, Nette does not require routes to be named.
It's important in which order are the routes defined as they are evaluated from top to bottom. The rule of thumb here is that routes are declared from the most specific at the top to the most vague at the bottom.
Keep in mind that huge amount of routes can negatively affect application speed, mostly when generating links. It's worth to keep routes as simple as possible.
If no route is found, a BadRequestException is thrown, which is shown as 404 Not Found to the user.
Router Factory
The recommended way to configure the application router is to write a factory (located e.g. in
app/router/RouterFactory.php
) and register it to system DI container in a
configuration file (located e.g. in app/config/config.neon
).
File app/router/RouterFactory.php:
<?php
namespace App;
use Nette\Application\Routers\RouteList,
Nette\Application\Routers\Route;
class RouterFactory
{
/**
* @return \Nette\Application\IRouter
*/
public function createRouter()
{
$router = new RouteList();
$router[] = new Route('<presenter>/<action>', 'Homepage:default');
return $router;
}
}
The @return
annotation is important and required for getting the code to work.
File app/config/config.neon:
services:
routerFactory: App\RouterFactory
router: @routerFactory::createRouter
Default Values
Each parameter may have defined a default value in the mask:
$route = new Route('<presenter=Homepage>/<action=default>/<id=>');
Or utilizing an array:
$route = new Route('<presenter>/<action>/<id>', array(
'presenter' => 'Homepage',
'action' => 'default',
'id' => NULL,
));
// equals to the following complex notation
$route = new Route('<presenter>/<action>/<id>', array(
'presenter' => array(
Route::VALUE => 'Homepage',
),
'action' => array(
Route::VALUE => 'default',
),
'id' => array(
Route::VALUE => NULL,
),
));
Default values for <presenter>
and <action>
can also be written as a string in the second
constructor parameter.
$route = new Route('<presenter>/<action>/<id=>', 'Homepage:default');
Validation Expressions
Each parameter may have defined a regular expression which it needs to match. This regular expression is checked both
when matching and generating URL. For example let's set id
to be only numerical, using \d+
regexp:
// regexp can be defined directly in the path mask after parameter name
$route = new Route('<presenter>/<action>[/<id \d+>]', 'Homepage:default');
// equals to the following complex notation
$route = new Route('<presenter>/<action>[/<id>]', array(
'presenter' => 'Homepage',
'action' => 'default',
'id' => array(
Route::PATTERN => '\d+',
),
));
Default validation expression for path parameters is [^/]+
, meaning all characters but a slash. If a
parameter is supposed to match a slash as well, we can set the regular expression to .+
.
Regular expressions are case-insensitive by default. You need to set Route::CASE_SENSITIVE
flag to make them case-sensitive.
Optional Sequences
Square brackets denote optional parts of mask. Any part of mask may be set as optional, including those containing parameters:
$route = new Route('[<lang [a-z]{2}>/]<name>', 'Article:view');
// Accepted URLs: Parameters:
// /en/download action => view, lang => en, name => download
// /download action => view, lang => NULL, name => download
Obviously, if a parameter is inside an optional sequence, it's optional too and defaults to NULL
. Sequence should
define it's surroundings, in this case a slash which must follow a parameter, if set. The technique may be used for example for
optional language subdomains:
$route = new Route('//[<lang=en>.]%domain%/<presenter>/<action>', ...);
Sequences may be freely nested and combined:
$route = new Route(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
'Homepage:default'
);
// Accepted URLs:
// /cs/hello
// /en-us/hello
// /hello
// /hello/page-12
URL generator tries to keep the URL as short as possible (while unique), so what can be omitted is not used. That's why
index[.html]
route generates /index
. This behavior can be inverted by writing an exclamation mark after
the leftmost square bracket that denotes the respective optional sequence:
// accepts both /hello and /hello.html, generates /hello
$route = new Route('<name>[.html]');
// accepts both /hello and /hello.html, generates /hello.html
$route = new Route('<name>[!.html]');
Optional parameters (ie. parameters having default value) without square brackets do behave as if wrapped like this:
$route = new Route('<presenter=Homepage>/<action=default>/<id=>');
// equals to:
$route = new Route('[<presenter=Homepage>/[<action=default>/[<id>]]]');
If we would like to change how the rightmost slashes are generated, that is instead of /homepage/
get a
/homepage
, we can adjust the route:
$route = new Route('[<presenter=Homepage>[/<action=default>[/<id>]]]');
Flags
Route
default behavior can be altered by several flags which can be passed as the third constructor parameter.
ONE_WAY flag – One way routes are usually used to preserve old URLs functionality, when an application is rewritten.
Flag Route::ONE_WAY
marks routes, which are not used for URL generation.
// old URL /product-info?id=123, new URL /product/123
$router[] = new Route('product-info', 'Product:detail', Route::ONE_WAY);
$router[] = new Route('product/<id>', 'Product:detail');
SECURED flag – Routes can be forced to use HTTPS by Route::SECURED
flag. That's useful for
authentication pages, as well as administration etc. HTTPS must be supported by your hosting/server.
$route = new Route('admin/<presenter>/<action>', 'Admin:default', Route::SECURED);
CASE_SENSITIVE flag – Routes are case-insensitive by default. Use Route::CASE_SENSITIVE
to make a
route case-sensitive.
// accepts all '/loRem', '/LOREM' and '/lorem'
$route = new Route('<slug lorem>', 'Page:view');
// accepts only '/lorem'
$route = new Route('<slug lorem>', 'Page:view', Route::CASE_SENSITIVE);
Flags can be freely combined using bitwise OR operator (|
) and it is possible to set some flags as default for all
routes using Route::$defaultFlags
property.
// makes all routes case sensitive
Route::$defaultFlags |= Route::CASE_SENSITIVE;
Filters and Translation
It's a good practice to write source code in English, but what if you need your application to run in a different environment? Simple routes such as:
$route = new Route('<presenter>/<action>/<id>', array(
'presenter' => 'Homepage',
'action' => 'default',
'id' => NULL,
));
will generate English URLs, such as /product/detail/123
, /cart
or /catalog/view
. If we
would like to translate those URLs, we can use a dictionary defined under Route::FILTER_TABLE
key. We'd extend
the route so:
$route = new Route('<presenter>/<action>/<id>', array(
'presenter' => array(
Route::VALUE => 'Homepage', // default value
Route::FILTER_TABLE => array(
// translated string in URL => presenter
'produkt' => 'Product',
'einkaufswagen' => 'Cart',
'katalog' => 'Catalog',
),
),
'action' => array(
Route::VALUE => 'default',
Route::FILTER_TABLE => array(
'sehen' => 'view',
),
),
'id' => NULL,
));
Multiple keys under Route::FILTER_TABLE
may have the same value. That's how aliases are created. The
last value is the canonical one (used for generating links).
Dictionaries may be applied to any path parameter. If a translation is not found, the original (non-translated) value is used.
The route by default accepts both translated (e.g. /einkaufswagen
) and original (e.g. /cart
) URLs. If
you would like to accept only translated URLs, you need to add Route::FILTER_STRICT => TRUE
to the route
definition.
Besides setting dictionaries as arrays, it's possible to set input and output filters. Input and output filter are
callbacks defined under Route::FILTER_IN
and Route::FILTER_OUT
keys respectively.
$route = new Route('<presenter=Homepage>/<action=default>', array(
'action' => array(
Route::FILTER_IN => function ($action) {
return strrev($action);
},
Route::FILTER_OUT => function ($action) {
return strrev($action);
},
),
));
The input filter accepts value from URL and should return a value which will be passed to a presenter. The output
filter accepts value from presenter and should return a value which will be used in URL. If any of those filters is unable to
filter the value (usually because it is invalid), it should return NULL
.
Global filters
Besides filters for specific parameters, you can also define global filters which accepts an associative array with all
parameters and returns an array with filtered parameters. Global filters are defined under NULL
key.
$route = new Route('<presenter>/<action>', array(
'presenter' => 'Homepage',
'action' => 'default',
NULL => array(
Route::FILTER_IN => function (array $params) {
// ...
return $params;
},
Route::FILTER_OUT => function (array $params) {
// ...
return $params;
},
),
));
You can use global filters to filter certain parameter based on a value of another parameter, e.g.
translate <presenter>
and <action>
based on <lang>
.
Foo Parameters
Foo parameters are basically unnamed parameters which allow you to match a regular expression. The following route matches
/index
, /index.html
, /index.htm
and /index.php
:
$route = new Route('index<? \.html?|\.php|>', 'Homepage:default');
It's also possible to explicitly define a string which will be used for URL generation (similar to setting default value for
real parameters). The string must be placed directly after the question mark. The following route is similar to the previous one,
but generates /index.html
instead of /index
, because the string .html
is set as a
“default value”.
$route = new Route('index<?.html \.html?|\.php|>', 'Homepage:default');
Routing Debugger
Working with routes may seem a bit magical at first. That's why you'll appreciate the value of Routing Debugger. It's a Debugger bar panel which gives you a list of all parameters a router got and a list of all defined routes. It also shows on which presenter and action you are currently on.

Routing debugger is enabled by default if the application runs in a debug mode. You can however disable it in a configuration file:
nette:
routing:
debugger: off # on by default
SEO and Canonicalization
Framework increases SEO (search engine optimization) as it prevents multiple URLs to link to different content (without a
proper redirect). If more than one addresses link to the same target (/index
and /index.html
), framework
choses the first (makes it canonical) and redirects the other one to it with an HTTP code 301. Thanks to that your page won't
have duplicities on search engines and their rank won't be split.
This whole process is called canonicalization. Default (canonical) URL is the one router generates, that is the first route in
collection which does not return NULL
and does not have a ONE_WAY
flag.
Canonicalization is done by Presenter and
it's switched on by default. You may disable it by setting Presenter::$autoCanonicalize to
FALSE
, e.g. in startup()
.
Ajax and POST requests are not redirected as user would suffer either a data loss, or it would yield no additional SEO value.
Modularity
If we want to add separate modules into our application that have their own routes, for example a discussion forum, we may
define routes elsewhere, possibly in createRoutes()
method:
class Forum
{
static function createRoutes($router, $prefix)
{
$router[] = new Route($prefix . 'index.php', 'Forum:Homepage:default');
$router[] = new Route($prefix . 'admin.php', 'Forum:Admin:default');
...
}
}
“Routing-in” the forum into existing application can be as easy as calling this method: (bootstrap.php
)
$router = new RouteList;
// our routes
...
// adding forum module
Forum::createRoutes($router, '//forum.example.com/');
$container->addService('router', $router);
Custom Router
If these offered routes do not fit your needs, you may create your own router and add it to your router collection. Router is nothing more than an implementation of IRouter with it's two methods:
use Nette\Application\Request as AppRequest,
Nette\Http\IRequest as HttpRequest,
Nette\Http\Url;
class MyRouter implements Nette\Application\IRouter
{
function match(HttpRequest $httpRequest)
{
// ...
}
function constructUrl(AppRequest $appRequest, Url $refUrl)
{
// ...
}
}
Method match
does process an HttpRequest (which
offers more than just a Url) into an internal Nette\Application\Request which contains presenter name and
it's parameters. If the HTTP request could not be processed, it should return NULL
.
Method constructUrl
generates an absolute URL from application request, possibly utilizing information from
$refUrl
argument.
Possibilities of custom routers are unlimited, for example it's possible to implement a router based on database records.
Related blog posts
HTTP requests and responses – Part 3
In the first and second sections of this mini-series I describe the possibilities of controlling HTTP protocol from the presenter in Nette…
HTTP requests and responses – Part 2
In the first part I describe presenter methods, by which we can control HTTP responses of the application. In this section, I will address the…
HTTP requests and responses – Part 1
Nette offers two layers of abstraction to work with HTTP. The first, low-level, is provided by namespace classes Nette\Http. They offer a nice API…
Link generation in emails with Nette 2.3
Since Nette 2.3, there is a LinkGenerator tool, which allows you to generate links without the need to use presenters and still in a very comfy way.…