Маршрутизиране
Маршрутизаторът ще се погрижи за всичко, свързано с URL адресите, така че повече няма да ви се налага да мислите за тях. Нека ви покажем:
- как да настроите маршрутизатора така, че URL адресите да са такива, каквито искате да бъдат
- Как да настроите SEO и пренасочвания.
- как да напишете собствен маршрутизатор
По-човешките URL адреси (или готините или красивите URL адреси) са по-използваеми, по-добре запомнящи се и имат положително въздействие върху SEO. Nette взема това предвид и напълно удовлетворява желанията на разработчиците. Можете да проектирате структурата на URL адресите на приложението си точно по желания от вас начин. Можете да го проектирате дори след като приложението е готово, тъй като това може да стане без промени в кода или шаблона. Тя е дефинирана елегантно на едно място, в маршрутизатора, а не е разпръсната под формата на анотации навсякъде из презентаторите.
Маршрутизаторът в Nette е специален, защото е двупосочен – той може да декодира URL адресите на HTTP заявките и да създава връзки. Ето защо той играе важна роля в приложението Nette, тъй като той решава кой главен потребител и действие ще изпълни текущата заявка, а също така се използва за генериране на URL адреси в шаблона и т.н.
Маршрутизаторът обаче не се ограничава само до това приложение, можете да го използвате в приложения, в които презентаторите изобщо не се използват, за REST API и т.н. За повече подробности вижте раздела за разделно използване.
Събиране на маршрути
Най-красивият начин за дефиниране на URL адреси в приложението е класът Nette\Application\Routers\RouteList. Дефиницията се състои от списък с така наречените маршрути, т.е. URL маски и свързани с тях преемници и действия, използващи прост API. Не е необходимо да назоваваме маршрутите.
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
В примера се казва, че ако отворим https://any-domain.com/rss.xml
с действие
rss
и т.н. Ако не бъде намерен подходящ маршрут, приложението Nette
отговаря с BadRequestException, което се
показва на потребителя като страница за грешка 404 Not Found.
Ред на маршрутите
Редът на изброяване на маршрутите е много важен, тъй като те се оценяват последователно отгоре надолу. Правилото е, че декларираме маршрути **от специфични към общи:
// ГРЕШКА: 'rss.xml' съвпада с първия маршрут и се третира неправилно като <slug>.
$router->addRoute('<slug>', 'Article:view');
$router->addRoute('rss.xml', 'Feed:rss');
// ПОВЕЧЕ
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('<slug>', 'Article:view');
Маршрутите също се оценяват отгоре надолу при генерирането на препратки:
// ГРЕШКА: генерира препратка към 'Feed:rss' като 'admin/feed/rss'
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
$router->addRoute('rss.xml', 'Feed:rss');
// ПОВЕЧЕ
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('admin/<presenter>/<action>', 'Admin:default');
Няма да крием от вас, че за правилното създаване на списък са необходими известни умения. Докато се научите, панелът за маршрутизиране ще бъде полезен инструмент.
Маска и параметри
Маската описва относителен път, базиран на корена на сайта. Най-простата маска е статичен URL адрес:
$router->addRoute('products', 'Products:default');
Маските често съдържат така наречените параметри. Те се поставят
в ъглови скоби (напр, <year>
) и се предават на целевия водещ,
например на метода renderShow(int $year)
или на постоянния параметър
$year
:
$router->addRoute('chronicle/<year>', 'History:show');
В примера се казва, че ако отворим https://any-domain.com/chronicle/2020
и
действието show
с параметър year: 2020
.
Можем да зададем стойност по подразбиране за параметрите директно в маската и по този начин тя става незадължителна:
$router->addRoute('chronicle/<year=2020>', 'History:show');
Маршрутът вече ще приема URL адреса https://any-domain.com/chronicle/
с
параметъра year
: 2020`.
Разбира се, името на водещия и действието също могат да бъдат параметри. Например:
$router->addRoute('<presenter>/<action>', 'Home:default');
Този маршрут приема например URL адреси под формата съответно на
/article/edit
и /catalog/list
и ги превръща в презентатори и действия
съответно Article:edit
и Catalog:list
.
Той също така дава на presenter
и action
стойности по
подразбиране за Home
и default
и следователно те не са
задължителни. Затова маршрутът също така взема URL адреса /article
и
го превежда като Article:default
. Или обратното, връзка към
Product:default
генерира пътя /product
, а връзка към стандартния
Home:default
генерира пътя /
.
Маската може да описва не само относителен път, базиран на корена на сайта, но и абсолютен път, ако започва с наклонена черта, или дори цял абсолютен URL адрес, ако започва с две наклонени черти:
// относителен път до корена на приложението
$router->addRoute('<presenter>/<action>', /* ... */);
// абсолютен път, относително към името на хоста на сървъра
$router->addRoute('/<presenter>/<action>', /* ... */);
// абсолютен URL адрес, включително името на хоста (относително към схемата)
$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */);
// абсолютен URL адрес, включително схемата
$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */);
Изрази за валидиране
За всеки параметър можете да зададете условие за валидиране, като
използвате регулярен израз.
Например, нека зададем id
само цифрово, като използваме \d+
regexp:
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
По подразбиране регулярният израз за всички параметри е
[^/]+
т.е. всичко освен наклонената черта. Ако параметърът трябва
да съответства и на наклонената черта, задаваме регулярния израз на
.+
.
// приема https://example.com/a/b/c, път - 'a/b/c'
$router->addRoute('<path .+>', /* ... */);
Незадължителни последователности
С квадратни скоби са обозначени допълнителните части на маската. Всяка част от маската може да бъде посочена като незадължителна, включително тези, които съдържат параметри:
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// Приети URL адреси: Параметри:
// /en/download lang => en, name => download
// /download lang => null, name => download
Разбира се, когато даден параметър е част от незадължителна последователност, той също става незадължителен. Ако няма стойност по подразбиране, тя ще бъде нула.
Незадължителните части също могат да бъдат в домейна:
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
Последователностите могат да бъдат свободно вложени и обединени:
$router->addRoute(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[page-<page=0>]',
'Home:default',
);
//получени URL адреси:
// /ru/hello
// /en-us/hello
// /hello
// /hello/page-12
Генераторът на URL адреси се опитва да направи URL адреса възможно
най-кратък, така че това, което може да се пропусне, се пропуска. Затова
например маршрутът index[.html]
генерира пътя /index
. Можете да
промените това поведение, като напишете възклицателен знак след
лявата квадратна скоба:
// взема /hello и /hello.html, генерира /hello
$router->addRoute('<name>[.html]', /* ... */);
// взема /hello и /hello.html, генерира /hello.html
$router->addRoute('<name>[!.html]', /* ... */);
Незадължителни параметри (т.е. параметри, които имат стойност по подразбиране) без квадратни скоби се държат така, сякаш са обвити по този начин:
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);
// е равно на:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
За да промените начина, по който се генерира най-дясната наклонена
черта, т.е. вместо /home/
да получите /home
, конфигурирайте
маршрута по този начин:
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
Символи за заместване
В маската на абсолютния път можем да използваме следните заместващи символи, за да избегнем например необходимостта да записваме домейн в маската, който може да е различен в средата за разработка и в производствената среда:
%tld%
= домейн от първо ниво, напримерcom
илиorg
%sld%
= домейн от второ ниво, напр,example
%domain%
= домейн без поддомейни, напр,example.com
%host%
= целият хост, напр,www.example.com
%basePath%
= път до главната директория
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */);
Разширена нотация
Целта на маршрута, обикновено записана под формата на
Presenter:action
, може да бъде изразена и с помощта на масив, който
определя отделните параметри и техните стойности по подразбиране:
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Home',
'action' => 'default',
]);
За по-подробна спецификация може да се използва още по-разширена
форма, в която освен стойностите по подразбиране могат да се задават и
други свойства на параметъра, например регулярен израз за валидиране
(вж. параметъра id
):
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::Value => 'Home',
],
'action' => [
Route::Value => 'default',
],
'id' => [
Route::Pattern => '\d+',
],
]);
Важно е да се отбележи, че ако параметрите, дефинирани в масива, не са включени в маската на пътя, техните стойности не могат да бъдат променени, дори и чрез използване на параметри на заявката, зададени след въпросителен знак в URL адреса.
Филтри и преводи
Добра практика е изходният код да бъде написан на английски език, но какво да правите, ако URL адресът на уебсайта ви трябва да бъде преведен на друг език?
$router->addRoute('<presenter>/<action>', 'Home:default');
ще генерира английски URL адреси като /product/123
или /cart
. Ако
искаме презентаторите и действията в URL адреса да бъдат преведени на
немски език (напр. /produkt/123
или /einkaufswagen
), можем да
използваме речник за превод. За да го добавим, вече се нуждаем от
„по-ясна“ версия на втория параметър:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterTable => [
// строка в URL => ведущий
'produkt' => 'Product',
'einkaufswagen' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::Value => 'default',
Route::FilterTable => [
'liste' => 'list',
],
],
]);
За един и същ водещ могат да се използват няколко речникови ключа. Те ще създадат различни псевдоними за него. Последният ключ се счита за каноничния вариант (т.е. този, който ще бъде в генерирания URL адрес).
По този начин към всеки параметър може да се приложи таблица за
превод. Ако обаче няма превод, се приема оригиналната стойност. Можем
да променим това поведение, като добавим Route::FilterStrict => true
, след
което маршрутът ще отхвърли URL адреса, ако стойността не е в речника.
В допълнение към речника за превод като масив можем да дефинираме собствени функции за превод:
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>/<id>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterIn => function (string $s): string { /* ... */ },
Route::FilterOut => function (string $s): string { /* ... */ },
],
'action' => 'default',
'id' => null,
]);
Функцията Route::FilterIn
извършва преобразуване между параметър в
URL адреса и низ, който след това се предава на презентатора, а функцията
FilterOut
осигурява преобразуване в обратна посока.
Параметрите presenter
, action
и module
вече имат
предварително дефинирани филтри, които преобразуват между PascalCase, camelCase
и kebab-case, използвани съответно в URL адреса. Стойността по подразбиране
на параметрите вече е записана в преобразуваната форма, така че
например в случая с презентатора пишем <presenter=ProductEdit>
вместо
<presenter=product-edit>
.
Общи филтри
В допълнение към филтрите за конкретни параметри можете да
дефинирате и общи филтри, които получават асоциативен масив от всички
параметри, които могат да променят по какъвто и да е начин, и след това
да върнат. Общите филтри се определят от ключа null
.
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Home',
'action' => 'default',
null => [
Route::FilterIn => function (array $params): array { /* ... */ },
Route::FilterOut => function (array $params): array { /* ... */ },
],
]);
Общите филтри ви дават възможност да персонализирате поведението на
даден маршрут по абсолютно всякакъв начин. Можем да ги използваме
например за промяна на настройки въз основа на други настройки.
Например, превод <presenter>
и <action>
въз основа на
текущата стойност на параметъра <lang>
.
Ако за даден параметър е дефиниран потребителски филтър и
едновременно с това съществува общ филтър, потребителският
FilterIn
се изпълнява преди общия, и обратно, общият FilterOut
се
изпълнява преди потребителския. По този начин вътре в общия филтър се
намират стойностите на параметрите presenter
и action
, записани
съответно с PascalCase и camelCase.
Еднопосочен флаг
Еднопосочните маршрути се използват за запазване на
функционалността на стари URL адреси, които приложението вече не
генерира, но все още приема. Маркираме ги с флага OneWay
:
// старый URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY);
// нов URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
При достъп до стария URL адрес презентаторът автоматично пренасочва към новия URL адрес, така че търсачките да не индексират тези страници два пъти (вж. SEO и канонизация).
Динамично маршрутизиране с обратни извиквания
Динамичното маршрутизиране с обратни повиквания ви позволява директно да присвоявате функции (обратни повиквания) към маршрутите, които ще се изпълняват при посещение на посочения път. Тази гъвкава функция ви позволява бързо и ефективно да създавате различни крайни точки за вашето приложение:
$router->addRoute('test', function () {
echo 'You are at the /test address';
});
Можете също така да дефинирате параметри в маската, които автоматично ще бъдат предадени на вашето обратно извикване:
$router->addRoute('<lang cs|en>', function (string $lang) {
echo match ($lang) {
'cs' => 'Welcome to the Czech version of our website!',
'en' => 'Welcome to the English version of our website!',
};
});
Модули
Ако имаме няколко маршрута, принадлежащи към един и същ модул, можем
да използваме withModule()
, за да ги групираме:
$router = new RouteList;
$router->withModule('Forum') // следните маршрутизатори са част от модула Forum
->addRoute('rss', 'Feed:rss') // водещият Форум:Feed
->addRoute('<presenter>/<action>')
->withModule('Admin') // следните маршрутизатори са част от модула Forum:Admin
->addRoute('sign:in', 'Sign:in');
Алтернативата е да се използва параметърът module
:
// URL адресът manage/dashboard/default се показва в главното меню на Admin:Dashboard
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin',
]);
Поддомейни
Колекциите от маршрути могат да бъдат групирани в поддомейни:
$router = new RouteList;
$router->withDomain('example.com')
->addRoute('rss', 'Feed:rss')
->addRoute('<presenter>/<action>');
Можете също така да използвате заместващи символи в името на домейна:
$router = new RouteList;
$router->withDomain('example.%tld%')
// ...
Префикс на пътя
Колекциите от маршрути могат да бъдат групирани по път в URL адреса:
$router = new RouteList;
$router->withPath('eshop')
->addRoute('rss', 'Feed:rss') // съответства на URL /eshop/rss
->addRoute('<presenter>/<action>'); // съответства на URL /eshop/<presenter>/<action>
Комбинации
Горните употреби могат да се комбинират:
$router = (new RouteList)
->withDomain('admin.example.com')
->withModule('Admin')
->addRoute(/* ... */)
->addRoute(/* ... */)
->end()
->withModule('Images')
->addRoute(/* ... */)
->end()
->end()
->withDomain('example.com')
->withPath('export')
->addRoute(/* ... */)
// ...
Опции за заявка
Маските могат да съдържат и параметри на заявката (параметри след въпросителния знак в URL адреса). Те не могат да дефинират израз за валидиране, но могат да променят името, чрез което се предават на водещия:
// използвайте параметъра на заявката 'cat' като 'categoryId' в приложението
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
Параметри на Foo
Сега се задълбочаваме. Параметрите Foo по същество са неназовани
параметри, които позволяват да се съпостави регулярен израз. Следният
маршрут съвпада с /index
, /index.html
, /index.htm
и
/index.php
:
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
Можете също така изрично да посочите низ, който да се използва за
генериране на URL адреса. Реда трябва да се намира непосредствено след
въпросителния знак. Следващият маршрут е подобен на предишния, но
генерира /index.html
вместо /index
, тъй като символният низ
.html
е зададен като „генерирана стойност“.
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
Интеграция
За да свържем маршрутизатора си с приложението, трябва да
информираме за това контейнера DI. Най-лесният начин е да се подготви
фабрика, която ще създаде обект маршрутизатор, и да се каже на
конфигурацията на контейнера да го използва. Да предположим, че
напишем метод за това, 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;
}
}
След това пишем в конфигурацията:
services:
- App\Core\RouterFactory::createRouter
Всички зависимости, като например връзки към бази данни и т.н., се предават на метода на фабриката като параметри, като се използва автоматично свързване:
public static function createRouter(Nette\Database\Connection $db): RouteList
{
// ...
}
SimpleRouter
Много по-прост маршрутизатор от колекция от маршрути е SimpleRouter. Той може да
се използва, когато няма нужда от конкретен формат на URL, когато не е
наличен mod_rewrite
(или алтернативи) или когато просто не искаме да
се занимаваме с удобни за потребителя URL адреси.
Той генерира адреси с форма, подобна на тази:
http://example.com/?presenter=Product&action=detail&id=123
Параметърът на конструктора SimpleRouter
е презентаторът и
действието по подразбиране, т.е. действието, което ще се изпълни, ако
отворим например http://example.com/
без никакви допълнителни
параметри.
// използвайте презентатора 'Home' и действието 'default'
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
Препоръчваме да дефинирате SimpleRouter директно в конфигурацията:
services:
- Nette\Application\Routers\SimpleRouter('Home:default')
SEO и канонизация
Рамката подобрява SEO оптимизацията, като предотвратява дублиращото
се съдържание на различни URL адреси. Ако няколко URL адреса сочат към
една и съща дестинация, например /index
и /index.html
, рамката
идентифицира първия като основен (каноничен) и пренасочва останалите
към него, като използва HTTP код 301. Това гарантира, че търсачките няма да
индексират страниците два пъти и няма да нарушат класирането им.
Този процес се нарича канонизиране. Каноничният URL адрес е URL адрес, генериран от маршрутизатор, т.е. първият съвпадащ маршрут в колекцията без флага OneWay. Ето защо в колекцията са изброени първите маршрути.
Канонизацията се извършва от водещия, за подробности вижте глава Канонизация.
HTTPS
За да използвате протокола HTTPS, трябва да го активирате в хостинг услугата си и да конфигурирате сървъра.
Пренасочването на целия сайт към HTTPS трябва да се извърши на ниво сървър, например с помощта на файл .htaccess в главната директория на нашето приложение с HTTP код 301. Настройките могат да варират в зависимост от хостинга и да изглеждат по следния начин:
<IfModule mod_rewrite.c>
RewriteEngine On
...
RewriteCond %{HTTPS} off
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
...
</IfModule>
Маршрутизаторът генерира URL адреса със същия протокол, с който е изтеглена страницата, така че не е необходимо да задавате нищо друго.
Въпреки това, ако искаме различни маршрути да работят с различни протоколи, ще поставим това в маската на маршрута:
// Генерира HTTP адрес
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);
// Генерира HTTPS адрес
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
Дебъгер за маршрутизиране
Лентата за маршрутизиране, показана в лентата на Tracy, е полезен инструмент, който показва списък с маршрути, както и параметрите, които маршрутизаторът е извлякъл от URL адреса.
Зелената лента със символ ✓ представлява маршрута, който съответства на текущия URL адрес, а сините ленти със символи ≈ показват маршрутите, които също биха съответствали на URL адреса, ако зеленият цвят не ги е изпреварил. След това виждаме текущия главен учител и действията.
В същото време, ако се появи неочаквано пренасочване поради канонизация, е полезно да погледнете в панела пренасочване, за да видите как маршрутизаторът първоначално е разбрал URL адреса и защо го е пренасочил.
При отстраняване на грешки в маршрутизатора се препоръчва да отворите Developer Tools (Инструменти за разработчици) в браузъра (Ctrl+Shift+I или Cmd+Option+I) и да деактивирате кеша в панела Network (Мрежа), така че пренасочванията да не се запазват в него.
Производителност
Броят на маршрутите влияе на скоростта на маршрутизатора. Броят им, разбира се, не трябва да надвишава няколко десетки. Ако сайтът ви има твърде сложна структура на URL адресите, можете да напишете потребителски маршрутизатор.
Ако маршрутизаторът няма зависимости, като например база данни, и фабриката му няма аргументи, можем да сериализираме компилираната му форма директно в контейнер DI и така да ускорим малко приложението.
routing:
cache: true
Потребителски рутер
Следващите редове са за много опитни потребители. Можете да създадете свой собствен маршрутизатор и, разбира се, да го добавите към колекцията от маршрути. Маршрутизаторът е реализация на интерфейса Router с два метода:
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
{
// ...
}
}
Методът match
обработва текущото $httpRequest, от което
може да се извлече не само URL адресът, но и заглавията и т.н., в масив,
съдържащ името на главния потребител и неговите параметри. Ако не може
да обработи заявката, тя връща null. Когато обработваме заявката, трябва
да върнем поне главния потребител и действието. Основното име е пълно и
включва всички модули:
[
'presenter' => 'Front:Home',
'action' => 'default',
]
Методът constructUrl
, от друга страна, генерира абсолютен URL адрес от
масив от параметри. Той може да използва информация от параметъра
$refUrl
, който е текущият URL адрес.
За да добавите потребителски маршрутизатор към колекцията от
маршрути, използвайте add()
:
$router = new Nette\Application\Routers\RouteList;
$router->add(new MyRouter);
$router->addRoute(/* ... */);
// ...
Отделна употреба
Под разделно използване се разбира използването на възможностите на маршрутизатора в приложение, което не използва приложението Nette и презентаторите. Почти всичко, което показахме в тази глава, се отнася за него със следните разлики:
- За събиране на маршрути използваме класа Nette\Routing\RouteList.
- като обикновен клас маршрутизатор Nette\Routing\SimpleRouter.
- тъй като няма двойка
Presenter:action
, използваме разширена нотация.
Затова отново ще добавим метод, който ще създаде например маршрутизатор:
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;
}
}
Ако използвате DI контейнер, както препоръчваме, добавете метода отново към конфигурацията и след това получете маршрутизатора заедно с HTTP заявката от контейнера:
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
Или ще създадем обектите директно:
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
Сега трябва да оставим маршрутизатора да свърши работата:
$params = $router->match($httpRequest);
if ($params === null) {
// не е намерен съответстващ маршрут, изпратете 404
exit;
}
// обработка на получените параметри
$controller = $params['controller'];
// ...
И обратното, ще използваме маршрутизатора, за да създадем връзката:
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());