Cool urls and global route filters 13/12/2015

This article was taken and translated from (paveljanda.com)[https://paveljanda.com]. Behaviour in dev mode There may be a request to use custom looking urls in your project. Maybe it is a e-commerce project with urls like en/category/another-category/super-tuper-hello-kitty-mug or it could be a blog reflecting all tags in the url: blog/kitten/cute/fluffy/2015-12-13-my-fluffy-day. Either way, you probably noticed that there may be a variable count of segments in particular url. To route application like that, we may use global filter in the Nette\Application\Routers\Route class.

This article was taken and translated from (paveljanda.com)[https://paveljanda.com].

Behaviour in dev mode

There may be a request to use custom looking urls in your project. Maybe it is a e-commerce project with urls like en/category/another-category/super-tuper-hello-kitty-mug or it could be a blog reflecting all tags in the url: blog/kitten/cute/fluffy/2015-12-13-my-fluffy-day. Either way, you probably noticed that there may be a variable count of segments in particular url. To route application like that, we may use global filter in the Nette\Application\Routers\Route class. You can read more about global route filter on the documentation web.

Make sure you have your router registered in DIC:

services:
    - App\RouterFactory
    router: @App\RouterFactory::createRouter

Now, how should your App\RouterFactory class look like?

<?php

delcare(strict_types=1);

namespace App;

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

class RouterFactory
{

    private $urls = [
        'en/category/another-category/super-tuper-hello-kitty-mug' => [
            'presenter' => 'Products',
            'action' => 'detail',
            'id' => '1010',
            'lang' => 'en',
        ],
        'en/hello-kitty-pen' => [
            'presenter' => 'Products',
            'action' => 'detail',
            'id' => '1234',
            'lang' => 'en',
        ],
        'es/kitty-pen-in-spanish' => [
            'presenter' => 'Products',
            'action' => 'detail',
            'id' => '1234',
            'lang' => 'es',
        ],
    ];


    public function createRouter(): IRouter
    {
        $router = new RouteList;

        $router[] = new Route('[<url [a-z-0-9\/]+?>]', [
            null => [
                Route::FILTER_IN => [$this, 'urlIn'],
                Route::FILTER_OUT => [$this, 'urlOut']
            ]
        ]);

        return $router;
    }


    public function urlIn(array $params): ?array
    {
        $url = trim($params['url'], '/');

        if (isset($this->urls[$url])) {
            return $this->urls[$url];
        }

        return null;
    }


    public function urlOut(array $params): ?array
    {
        foreach ($this->urls as $url => $url_params) {
            if ($params == $url_params) {
                return ['url' => $url];
            }
        }

        return null;
    }
}

When we define route filter under the null key, it is called global filter and should receive/return all parameters including presenter and action. In FILTER_IN, we will receive an array with url and we shall return that presenter/action combination. The FILTER_OUT does the opposite.

But we should keep in mind couple of thing:

1) List of the urls with their parameters (presenter, action, id, lang) would be better to keep in database and then cache it to make the work with it faster.

2, Be carefoul with cutting url parameters in urlOut method. There could be more parameters that you think (your custom pagination params, native do parameter which leads to the presenter signal and so on).

3, When keeping in database old urls that will be redirected to the new ones, it may be more transparent to create a Nette\Application\Application::$onError event listener and redirect user requests here. You will recognize the “Page not found” exception by it’s code ($exception->getCode() === 404).