URL 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).

The recommended way to configure application to use SimpleRouter is to use configuration file (e.g. config.neon):

services:
    application.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>]', [
    '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 absolute path (starts with a single slash) or absolute URL with domain (starts with a double slash).

// relative path to application document root (www directory)
$route = new Route('<presenter>/<action>', ...);

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

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

// absolute URL
$route = new Route('https://<subdomain>.example.com/<presenter>/<action>', ...);

Absolute path mask may utilize the following variables:

  • %tld% = top level domain, e.g. com or org
  • %sld% = second level domain, e.g. at example.com return example
  • %domain% = second level domain, e.g. example.com
  • %basePath%
$route = new Route('//www.%domain%/%basePath%/<presenter>/<action>', ...);
$route = new Route('//www.%sld%.%tld/%basePath%/<presenter>/<action', ...);

Route Collection

Because we usually define more than one route, we wrap them into a RouteList. Nette does not require routes to be named. See example.

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>', [
    'presenter' => 'Homepage',
    'action' => 'default',
    'id' => null,
]);

// equals to the following complex notation
$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => [
        Route::VALUE => 'Homepage',
    ],
    'action' => [
        Route::VALUE => 'default',
    ],
    'id' => [
        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>]', [
    'presenter' => 'Homepage',
    'action' => 'default',
    'id' => [
        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 .+.

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>]]]');

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');

ONE_WAY flag

Unidirectional routers are especially used to preserve the functionality of old URLs by redirecting the URL in the application to a newer form. The ONE_WAY flag then marks rows that are no longer used to generate URLs.

// 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');

Additionally, automatic redirection to the new URL form will cause to do not index the search engines twice (see SEO and canonization).

HTTPS

We need configurate our server for use HTTPS protocol.

Forward all addresses for already well-established applications can be achieved by using .htaccess file in the root directory of our application using a permanent redirect with code 301. (Settings may vary by hosting.)

<IfModule mod_rewrite.c>
    RewriteEngine On
    ...
    RewriteCond %{HTTPS} off
    RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
    ...
</IfModule>

Routes generate URLs with the same protocol as the page was loaded, so nothing more is usually needed to set up.

However, if we need the individual routines to run under different protocols, we'll put it in route mask:

// Uses the same protocol, which the page was loaded
$route = new Route('//%host%/<presenter>/<action>','Homepage:default');

// Will generate an HTTP address
$route = new Route('http://%host%/<presenter>/<action>','Homepage:default');

// Will generate an HTTPS address
$route = new Route('https://%host%/<presenter>/<action>','Admin:default');

You must activate HTTPS for your hosting.

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>', [
    '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>', [
    'presenter' => [
        Route::VALUE => 'Homepage', // default value
        Route::FILTER_TABLE => [
            // translated string in URL => presenter
            'produkt' => 'Product',
            'einkaufswagen' => 'Cart',
            'katalog' => 'Catalog',
        ],
    ],
    'action' => [
        Route::VALUE => 'default',
        Route::FILTER_TABLE => [
            '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.

If we only want to only use a translation dictionary for a given parameter, let add FILTER_STRICT => true. This will ensure that only the values in dictionary well will be accepted.

In addition to the translation dictionary in the form of an array, it is possible to set own translation functions and filters:

$route = new Route('<presenter>/<action>/<id>', [
    'presenter' => [
        Route::VALUE => 'Homepage',
        Route::FILTER_IN => 'filterInFunc',
        Route::FILTER_OUT => 'filterOutFunc',
    ],
    'action' => 'default',
    'id' => null,
]);

Where 'filterInFunc` and filterOutFunc are functions or methods that convert between the parameter in the URL and the value that is passed to the presenter. Each of them provides a conversion in the opposite direction.

The default in-filter is rawurldecode and the out-filter is a function that escapes special characters (such as a slash or a space) for use in the URL.

There are situations where we want to change this behavior, for example, if we use the path parameter, which may include slashes. In order not to convert to %2F, cancel the filters:

// accepts http://files.example.com/path/to/my/file

$route = new Route('//files.example.com/<path .+>', [
    'presenter' => 'File',
    'action' => 'default',
    'path' => [
        Route::VALUE => null,
        Route::FILTER_IN => null,
        Route::FILTER_OUT => 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>', [
    'presenter' => 'Homepage',
    'action' => 'default',
    null => [
        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>.

Router Factory

The recommended way to configure the application router is to write a factory (e.g. class RouterFactory) and register it to system DI container in a configuration file:

<?php
namespace App;

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

class RouterFactory
{
    use Nette\StaticClass;

    /**
     * @return Nette\Application\IRouter
     */
    public static function createRouter()
    {
        $router = new RouteList;
        $router[] = new Route('article/<id>', 'Article:view');
        $router[] = new Route('rss.xml', 'Feed:rss');
        $router[] = new Route('<presenter>/<action>', 'Homepage:default');
        return $router;
    }
}

File config.neon:

services:
    router: App\RouterFactory::createRouter

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

Count of routes has effects the performance of application, especially when generating links. It is therefore worthwhile to simplify the routing table.

If a route is not found, the BadRequestException exception is thrown and the user will see it as the 404 Page Not Found.

Any dependencies, such as a database connection or a configuration flag, can be passed to the router's factory as parameters of the function:

class RouterFactory
{
    /**
     * @return Nette\Application\IRouter
     */
    public static function createRouter(Nette\Database\Connection $db, $debugMode = false)
    {
        $router = new RouteList;
        $router[] = ...
        return $router;
    }
}

Modules

In Nette we can split presenters into modules. Therefore we need to work with those modules in routes. We can use module parameter in Route class:

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

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 maps to presenter Forum:Homepage
        $router[] = new Route('//forum.example.com/<presenter>/<action>');
        return $router;
    }
}

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:

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.

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;
use Nette\Http\IRequest as HttpRequest;
use 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.