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 Framework keeps this in mind and fully meets developers' desires.

Let's start technically. A router is an object that implements the Nette\Application\IRouter interface, which can decompose a URL into parameters encapsulated in the Nette\Application\Request objecta (method match()) and, conversely, build a URL from Nette\Application\Request (method constructUrl()). Therefore, it is also said that the router is bidirectional. Nette brings a very elegant way to define how the URLs of your application look like.

The router plays an important role in Nette Application. Thanks to router, it will find out which presenter and action to run. And it also uses the router to generate URLs in the template, for example:

<a n:href="Product:detail $productId">detail produktu</a>

The router compiles the resulting URL from these parameters. Read more in the chapter creating links.

Thus, routing is a separate and sophisticated layer of the application, thanks to which the look of URL addresses can be easily designed or changed when the entire application is ready, because it can be done without modification of the code or templates. Which gives developers huge freedom.

Route Collection

The most pleasant way to define the URL addresses in the application is via the classes Nette\Application\Routers\RouteList and Nette\Application\Routers\Route. The big advantage is that the whole router is defined in one place and is not so scattered in the form of annotations in all presenters.

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.

use Nette\Application\Routers\Route;

$router = new Nette\Application\Routers\RouteList;
$router[] = new Route('rss.xml', 'Feed:rss');
$router[] = new Route('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, 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.

Order of routes is important because they are tried sequentially from the first one to the last one. Basic rule is to declare routes from the most specific to the most general.

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\RouterFactory::createRouter():

namespace App;

use Nette\Application\Routers\RouteList;
use Nette\Application\Routers\Route;

class RouterFactory
{
	/**
	 * @return Nette\Application\IRouter
	 */
	public static function createRouter()
	{
		$router = new RouteList;
		$router[] = new Route(...);
		return $router;
	}
}

Then we write in configuration:

services:
	router: App\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)
{
	...
}

Mask and Parameters

The mask describes the relative path based on the site root. The simplest mask is a static URL:

$router[] = new Route('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[] = new Route('chronicle/<year>', 'History:show');

http://example.com/…t/detail/123


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:

```php
$router[] = new Route('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[] = new Route('<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[] = new Route('<presenter>/<action>', ...);

// absolute path, relative to server hostname
$router[] = new Route('/<presenter>/<action>', ...);

// absolute URL including hostname (but scheme-relative)
$router[] = new Route('//<lang>.example.com/<presenter>/<action>', ...);

// absolute URL including schema
$router[] = new Route('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[] = new Route('<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[] = new Route('<path .+>', ...);

Optional Sequences

Square brackets denote optional parts of mask. Any part of mask may be set as optional, including those containing parameters:

$router[] = new Route('[<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[] = new Route('//[<lang=en>.]example.com/<presenter>/<action>', ...);

Sequences may be freely nested and combined:

$router[] = 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, 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[] = new Route('<name>[.html]', ...);

// accepts both /hello and /hello.html, generates /hello.html
$router[] = new Route('<name>[!.html]', ...);

Optional parameters (ie. parameters having default value) without square brackets do behave as if wrapped like this:

$router[] = new Route('<presenter=Homepage>/<action=default>/<id=>', ...);

// equals to:
$router[] = new Route('[<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[] = new Route('[<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 or org
  • %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[] = new Route('//www.%domain%/%basePath%/<presenter>/<action>', ...);
$router[] = new Route('//www.%sld%.%tld%/%basePath%/<presenter>/<action', ...);

Advanced notation

The second parameter of the route, which we often write in the format Presenter:action, is an abbreviation, which we can also write in the form of a field, where we directly state the (default) values ​​of individual parameters:

$router[] = new Route('<presenter>/<action>[/<id \d+>]', [
	'presenter' => 'Homepage',
	'action' => 'default',
]);

Or we can use this form, notice the rewriting of the validation regular expression:

$router[] = new Route('<presenter>/<action>[/<id>]', [
	'presenter' => [
		Route::VALUE => 'Homepage',
	],
	'action' => [
		Route::VALUE => 'default',
	],
	'id' => [
		Route::PATTERN => '\d+',
	],
]);

These more talkative formats are useful for adding other metadata.

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[] = new Route('<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:

$router[] = new Route('<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 Router::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:

$router[] = new Route('<presenter>/<action>/<id>', [
	'presenter' => [
		Route::VALUE => 'Homepage',
		Route::FILTER_IN => function ($s) { ... },
		Route::FILTER_OUT => function ($s) { ... },
	],
	'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>.

Global Filters

Besides filters for specific parameters, you can also define global filters that receive an associative array of all parameters that they can modify in any way and then return. Global filters are defined under null key.

$router[] = new Route('<presenter>/<action>', [
	'presenter' => 'Homepage',
	'action' => 'default',
	null => [
		Route::FILTER_IN => function (array $params) { ... },
		Route::FILTER_OUT => function (array $params) { ... },
	],
]);

Global 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 global filter exists at the same time, custom FILTER_IN is executed before the global and vice versa global FILTER_OUT is executed before the custom. Thus, inside the global 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, new URL /product/123
$router[] = new Route('product-info', 'Product:detail', Route::ONE_WAY);
$router[] = new Route('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 we want to group together in a module, we can use RouteList with name of module in constructor:

class RouterFactory
{
	/**
	 * @return Nette\Application\IRouter
	 */
	public static function createRouter()
	{
		$router = new RouteList;
		$router[] = self::createForumRouter();
		...
		$router[] = new Route('<presenter>/<action>', 'Homepage:default');
		return $router;
	}

	public static function createForumRouter()
	{
		$router = new RouteList('Forum');
		// http://forum.example.com/homepage/default se mapuje na presenter Forum:Homepage
		$router[] = new Route('//forum.example.com/<presenter>/<action>');
		return $router;
	}
}

An alternative is to use the module parameter:

// URL manage/dashboard/default maps to presenter Admin:Dashboard
new Route('manage/<presenter>/<action>', [
	'module' => 'Admin'
]);

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[] = new Route('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[] = new Route('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[] = new Route('index<?.html \.html?|\.php|>', ...);

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:
	router: 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[] = new Route('http://%host%/<presenter>/<action>', ...);

// Will generate an HTTPS address
$router[] = new Route('https://%host%/<presenter>/<action>', ...);

Routing Debugger

We will not hide from you that routing may seem a bit magical at first, and before you get into it, Routing Debugger will be a good helper. This is a panel displayed in the Tracy Bar, which provides a clear list of routes as well as parameters that the router obtained from the URL.

The green bar represents the route that matched the current URL, the blue bar indicates the routes that would also match the URL if green did not overtake them. We see the current presenter & action further.

Route Caching

If the router has no dependencies, such as a database, and its factory does not accept any arguments, we can serialize its built-in form directly into the DI container and thus slightly speed up the application.

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\Application\IRouter interface with two methods:

use Nette\Application\Request as AppRequest;
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\Url;

class MyRouter implements Nette\Application\IRouter
{
	public function match(HttpRequest $httpRequest)
	{
		// ...
	}

	public 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.


Related blog posts