Routing
The router is responsible for everything about URLs so that you no longer have to think about them. We will show:
- how to set up the router so that the URLs look like you want
- a few notes about SEO redirection
- and we'll show you how to write your own router
More human URLs (or cool or pretty URLs) are more usable, more memorable and contribute positively to SEO. Nette has this in mind and fully meets developers' desires. You can design your URL structure for your application exactly the way you want it. You can even design it after the app is ready, as it can be done without any code or template changes. It is defined in an elegant way in one single place, in the router, and is not scattered in the form of annotations in all presenters.
The router in Nette is special because it is bidirectional, it can both decode HTTP request URLs as well as create links. So it plays a vital role in Nette Application, because it decides which presenter and action will execute the current request, and is also used for URL generation in the template, etc.
However, the router is not limited to this use, you can use it in applications where presenters are not used at all, for REST APIs, etc. More in the section separated usage.
Route Collection
The most pleasant way to define the URL addresses in the application is via the class Nette\Application\Routers\RouteList. The definition consists of a list of so-called routes, ie masks of URL addresses and their associated presenters and actions using a simple API. We do not have to name the routes.
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
The example says that if we open https://any-domain.com/rss.xml
in the browser, the presenter Feed
with the action rss
will be displayed, if https://domain.com/article/12
, the Article
with
the view
action is displayed, etc. If no suitable route is found, Nette Application responds by throwing an exception
BadRequestException, which appears
to the user as a 404 Not Found error page.
In Nette 2.x, $router[] = new Route(...)
was used instead of
$router->addRoute(...)
.
Order of Routes
The order in which the routes are listed is very important because they are evaluated sequentially from top to bottom. The rule is that we declare routes from specific to general:
// WRONG: 'rss.xml' matches the first route and misunderstands this as <slug>
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');
// GOOD
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');
Routes are also evaluated from top to bottom when links are generated:
// WRONG: generates a link to 'Feed:rss' as 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');
// GOOD
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
We won't keep it a secret from you that it takes some skill to build a list correctly. Until you get into it, the routing panel will be a useful tool.
Mask and Parameters
The mask describes the relative path based on the site root. The simplest mask is a static URL:
$router->addRoute('products', 'Products:default');
Often masks contain so-called parameters. They are enclosed in angle brackets (e.g. <year>
) and are
passed to the target presenter, for example to the renderShow(int $year)
method or to persistent parameter
$year
:
$router->addRoute('chronicle/<year>', 'History:show');
The example says that if we open https://any-domain.com/chronicle/2020
in the browser, the presenter
History
and the action show
with parameter year => 2020
will be displayed.
We can specify a default value for the parameters directly in the mask and thus it becomes optional:
$router->addRoute('chronicle/<year=2020>', 'History:show');
The route will now accept the URL https://any-domain.com/chronicle/
, which will again display
History:show
with parameter year => 2020
.
Of course, the name of the presenter and the action can also be a parameter. For example:
$router->addRoute('<presenter>/<action>', 'Homepage:default');
This route accepts, for example, a URL in the form /article/edit
resp. /catalog/list
and translates
them to presenters and actions Article:edit
resp. Catalog:list
.
It also gives to parameters presenter
and action
default values Homepage
and
default
and therefore they are optional. So the route also accepts a URL /article
and translates it as
Article:default
. Or vice versa, a link to Product:default
generates a path /product
, a link
to the default Homepage:default
generates a path /
.
The mask can describe not only the relative path based on the site root, but also the absolute path when it begins with a slash, or even the entire absolute URL when it begins with two slashes:
// relative path to application document root
$router->addRoute('<presenter>/<action>', /* ... */);
// absolute path, relative to server hostname
$router->addRoute('/<presenter>/<action>', /* ... */);
// absolute URL including hostname (but scheme-relative)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);
// absolute URL including schema
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);
Validation Expressions
A validation condition can be specified for each parameter using regular expression. For example, let's set
id
to be only numerical, using \d+
regexp:
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
The default regular expression for all parameters is [^/]+
, ie everything except the slash. If a parameter is
supposed to match a slash as well, we set the regular expression to .+
.
// accepts https://example.com/a/b/c, path is 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
Optional Sequences
Square brackets denote optional parts of mask. Any part of mask may be set as optional, including those containing parameters:
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// Accepted URLs: Parameters:
// /en/download lang => en, name => download
// /download lang => null, name => download
Of course, when a parameter is part of an optional sequence, it also becomes optional. If it does not have a default value, it will be null.
Optional sections can also be in the domain:
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
Sequences may be freely nested and combined:
$router->addRoute(
'[<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, so what can be omitted is omitted. Therefore, for example, a route
index[.html]
generates a path /index
. You can reverse this behavior by writing an exclamation mark after
the left square bracket:
// accepts both /hello and /hello.html, generates /hello
$router->addRoute('<name>[.html]', /* ... */);
// accepts both /hello and /hello.html, generates /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
Optional parameters (ie. parameters having default value) without square brackets do behave as if wrapped like this:
$router->addRoute('<presenter=Homepage>/<action=default>/<id=>', /* ... */);
// equals to:
$router->addRoute('[<presenter=Homepage>/[<action=default>/[<id>]]]', /* ... */);
To change how the rightmost slash is generated, i.e. instead of /homepage/
get a /homepage
, adjust
the route this way:
$router->addRoute('[<presenter=Homepage>[/<action=default>[/<id>]]]', /* ... */);
Wildcards
In the absolute path mask, we can use the following wildcards to avoid, for example, the need to write a domain to the mask, which may differ in the development and production environment:
%tld%
= top level domain, e.g.com
ororg
%sld%
= second level domain, e.g.example
%domain%
= domain without subdomains, e.g.example.com
%host%
= whole host, e.g.www.example.com
%basePath%
= path to the root directory
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);
Advanced Notation
The target of a route, usually written in the form Presenter:action
, can also be expressed using an array that
defines individual parameters and their default values:
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Homepage',
'action' => 'default',
]);
For a more detailed specification, an even more extended form can be used, where in addition to default values, other parameter
properties can be set, such as a validation regular expression (see the id
parameter):
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::VALUE => 'Homepage',
],
'action' => [
Route::VALUE => 'default',
],
'id' => [
Route::PATTERN => '\d+',
],
]);
It is important to note that if the parameters defined in the array are not included in the path mask, their values cannot be changed, not even using query parameters specified after a question mark in the URL.
Filters and Translations
It's a good practice to write source code in English, but what if you need your website to have translated URL to different language? Simple routes such as:
$router->addRoute('<presenter>/<action>', 'Homepage:default');
will generate English URLs, such as /product/123
or /cart
. If we want to have presenters and actions
in the URL translated to Deutsch (e.g. /produkt/123
or /einkaufswagen
), we can use a translation
dictionary. To add it, we already need a „more talkative“ variant of the second parameter:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::VALUE => 'Homepage',
Route::FILTER_TABLE => [
// string in URL => presenter
'produkt' => 'Product',
'einkaufswagen' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::VALUE => 'default',
Route::FILTER_TABLE => [
'liste' => 'list',
],
],
]);
Multiple dictionary keys can by used for the same presenter. They will create various aliases for it. The last key is considered to be the canonical variant (i.e. the one that will be in the generated URL).
The translation table can be applied to any parameter in this way. However, if the translation does not exist, the original
value is taken. We can change this behavior by adding Route::FILTER_STRICT => true
and the route will then reject
the URL if the value is not in the dictionary.
In addition to the translation dictionary in the form of an array, it is possible to set own translation functions:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>/<id>', [
'presenter' => [
Route::VALUE => 'Homepage',
Route::FILTER_IN => function (string $s): string { /* ... */ },
Route::FILTER_OUT => function (string $s): string { /* ... */ },
],
'action' => 'default',
'id' => null,
]);
The function Route::FILTER_IN
converts between the parameter in the URL and the string, which is then passed to
the presenter, the function FILTER_OUT
ensures the conversion in the opposite direction.
The parameters presenter
, action
and module
already have predefined filters that convert
between the PascalCase resp. camelCase style and kebab-case used in the URL. The default value of the parameters is already
written in the transformed form, so, for example, in the case of a presenter, we write <presenter=ProductEdit>
instead of <presenter=product-edit>
.
General Filters
Besides filters for specific parameters, you can also define general filters that receive an associative array of all
parameters that they can modify in any way and then return. General filters are defined under null
key.
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Homepage',
'action' => 'default',
null => [
Route::FILTER_IN => function (array $params): array { /* ... */ },
Route::FILTER_OUT => function (array $params): array { /* ... */ },
],
]);
General filters give you the ability to adjust the behavior of the route in absolutely any way. We can use them, for example,
to modify parameters based on other parameters. For example, translation <presenter>
and
<action>
based on the current value of parameter <lang>
.
If a parameter has a custom filter defined and a general filter exists at the same time, custom FILTER_IN
is
executed before the general and vice versa general FILTER_OUT
is executed before the custom. Thus, inside the general
filter are the values of the parameters presenter
resp. action
written in PascalCase resp.
camelCase style.
ONE_WAY Flag
One-way routes are used to preserve the functionality of old URLs that the application no longer generates but still accepts.
We flag them with ONE_WAY
:
// old URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// new URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
When accessing the old URL, the presenter automatically redirects to the new URL so that search engines do not index these pages twice (see SEO and canonization).
Modules
If we have more routes that belong to one module, we can use withModule()
to
group them:
$router = new RouteList;
$router->withModule('Forum') // the following routers are part of the Forum module
->addRoute('rss', 'Feed:rss') // presenter is Forum:Feed
->addRoute('<presenter>/<action>')
->withModule('Admin') // the following routers are part of the Forum:Admin module
->addRoute('sign:in', 'Sign:in');
An alternative is to use the module
parameter:
// URL manage/dashboard/default maps to presenter Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin'
]);
Subdomains
Route collections can be grouped by subdomains:
$router = new RouteList;
$router->withDomain('example.com')
->addRoute('rss', 'Feed:rss')
->addRoute('<presenter>/<action>');
You can also use wildcards in your domain name:
$router = new RouteList;
$router->withDomain('example.%tld%')
// ...
Path Prefix
Route collections can be grouped by path in URL:
$router = new RouteList;
$router->withPath('eshop')
->addRoute('rss', 'Feed:rss') // matches URL /eshop/rss
->addRoute('<presenter>/<action>'); // matches URL /eshop/<presenter>/<action>
Combinations
The above usage can be combined:
$router = (new RouteList)
->withDomain('admin.example.com')
->withModule('Admin')
->addRoute(/* ... */)
->addRoute(/* ... */)
->end()
->withModule('Images')
->addRoute(/* ... */)
->end()
->end()
->withDomain('example.com')
->withPath('export')
->addRoute(/* ... */)
// ...
Query Parameters
Masks can also contain query parameters (parameters after the question mark in the URL). They cannot define a validation expression, but they can change the name under which they are passed to the presenter:
// use query parameter 'cat' as a 'categoryId' in application
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
Foo Parameters
We're going deeper now. Foo parameters are basically unnamed parameters which allow to match a regular expression. The
following route matches /index
, /index.html
, /index.htm
and /index.php
:
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
It's also possible to explicitly define a string which will be used for URL generation. 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 „generated value“.
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
Integration
In order to connect the our router into the application, we must tell the DI container about it. The easiest way is to prepare
the factory that will build the router object and tell the container configuration to use it. So let's say we write a method for
this purpose App\Core\RouterFactory::createRouter()
:
namespace App\Core;
use Nette\Application\Routers\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute(/* ... */);
return $router;
}
}
Then we write in configuration:
services:
- App\Core\RouterFactory::createRouter
In the case of Nette 3.0:
services:
router: App\Router\RouterFactory::createRouter
Any dependencies, such as a database connection etc., are passed to the factory method as its parameters using autowiring:
public static function createRouter(Nette\Database\Connection $db): RouteList
{
// ...
}
SimpleRouter
A much simpler router than the Route Collection 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.
Generates addresses in roughly this form:
http://example.com/?presenter=Product&action=detail&id=123
The 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');
We recommend defining SimpleRouter directly in configuration:
services:
- Nette\Application\Routers\SimpleRouter('Homepage:default')
SEO and Canonization
The framework increases SEO (search engine optimization) by preventing duplication of content at different URLs. If multiple
addresses link to a same destination, eg /index
and /index.html
, the framework determines the first one
as primary (canonical) and redirects the others to it using HTTP code 301. Thanks to this, search engines will not index pages
twice and do not break their page rank. .
This process is called canonization. The canonical URL is the one generated by the router, i.e. by the first matching route in the collection without the ONE_WAY flag. Therefore, in the collection, we list primary routes first.
Canonization is performed by the presenter, more in the chapter canonization.
HTTPS
In order to use the HTTPS protocol, it is necessary to activate it on hosting and to configure the server.
Redirection of the entire site to HTTPS must be performed at the server level, for example using the .htaccess file in the root directory of our application, with HTTP code 301. The settings may differ depending on the hosting and looks something like this:
<IfModule mod_rewrite.c>
RewriteEngine On
...
RewriteCond %{HTTPS} off
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
...
</IfModule>
The router generates a URL with the same protocol as the page was loaded, so there is no need to set anything else.
However, if we exceptionally need different routes to run under different protocols, we will put it in the route mask:
// Will generate an HTTP address
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);
// Will generate an HTTPS address
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
Debugging Router
The routing bar displayed in Tracy Bar is a useful tool that displays a list of routes and also the parameters that the router has obtained from the URL.
The green bar with symbol ✓ represents the route that matched the current URL, the blue bars with symbols ≈ indicate the routes that would also match the URL if green did not overtake them. We see the current presenter & action further.
At the same time, if there is an unexpected redirect due to canonicalization, it is useful to look in the redirect bar to see how the router originally understood the URL and why it redirected.
When debugging the router, it is recommended to open Developer Tools in the browser (Ctrl+Shift+I or Cmd+Option+I) and disable the cache in the Network panel so that redirects are not stored in it.
Performance
The number of routes affects the speed of the router. Their number should certainly not exceed a few dozen. If your site has an overly complicated URL structure, you can write a custom router.
If the router has no dependencies, such as on a database, and its factory has no arguments, we can serialize its compiled form directly into a DI container and thus make the application slightly faster.
routing:
cache: true
Custom Router
The following lines are intended for very advanced users. You can create your own router and naturally add it into your route collection. The router is an implementation of the Nette\Routing\Router interface with two methods:
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;
class MyRouter implements Nette\Routing\Router
{
public function match(HttpRequest $httpRequest): ?array
{
// ...
}
public function constructUrl(array $params, UrlScript $refUrl): ?string
{
// ...
}
}
The match
method processes the current $httpRequest, from which not only the URL, but
also headers etc. can be retrieved, into an array containing the presenter name and its parameters. If it cannot process the
request, it returns null. When processing the request, we must return at least the presenter and the action. The presenter name is
complete and includes any modules:
[
'presenter' => 'Front:Homepage',
'action' => 'default',
]
Method constructUrl
, on the other hand, generates an absolute URL from the array of parameters. It can use the
information from parameter $refUrl
, which is the current URL.
To add custom router to the route collection, use add()
:
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
Separated Usage
By separated usage, we mean the use of the router's capabilities in an application that does not use Nette Application and presenters. Almost everything we have shown in this chapter applies to it, with the following differences:
- for route collections we use class Nette\Routing\RouteList
- as a simple router class Nette\Routing\SimpleRouter
- because there is no pair
Presenter:action
, we use Advanced notation
So again we will create a method that will build a router, for example:
namespace App\Core;
use Nette\Routing\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute('rss.xml', [
'controller' => 'RssFeedController',
]);
$router->addRoute('article/<id \d+>', [
'controller' => 'ArticleController',
]);
// ...
return $router;
}
}
If you use a DI container, which we recommend, add the method to the configuration again and then get the router together with the HTTP request from the container:
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
Or we will create objects directly:
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
Now we have to let the router to work:
$params = $router->match($httpRequest);
if ($params === null) {
// no matching route found, we will send a 404 error
exit;
}
// we process the received parameters
$controller = $params['controller'];
// ...
And vice versa, we will use the router to create the link:
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());