ルーティング
******
<div class=perex>
ルーターは URL アドレスにまつわるすべてを引き受けるので、あなたが URL のことを考える必要はありません。ここでは次のことを扱います。
- URL を思いどおりの見た目にするためのルーターの設定
- SEO とリダイレクト
- そして独自のルーターの書き方
</div>
人にやさしい URL(かっこいい URL、きれいな URL とも呼ばれます)は使いやすく、覚えやすく、SEO にもよい影響を与えます。Nette はそれを心得ていて、開発者の求めるものにしっかり応えます。アプリケーションの URL の構造を、思いどおりに設計できます。しかもアプリケーションが完成したあとでも設計できます。コードやテンプレートの変更が要らないからです。それはすべてのプレゼンターにアノテーションとして散らばるのではなく、[ただ一か所 |#組み込み]、つまりルーターで優雅に定義されます。
Nette のルーターが並外れているのは、**双方向**だからです。HTTP のリクエストから URL を読み解くことも、リンクを作ることもできます。ですから [Nette Application |how-it-works#Nette Application]で欠かせない役割を果たします。現在のリクエストをどのプレゼンターとアクションが処理するかを決めるだけでなく、テンプレートなどで [URL を生成する |creating-links]のにも使われるからです。
とはいえルーターの用途はそれだけではありません。プレゼンターをまったく使わないアプリケーションや REST API などでも使えます。詳しくは [#単独での利用]の節をご覧ください。
ルートのコレクション
==========
アプリケーションの URL アドレスの構造を定義するもっとも心地よい方法は、[api:Nette\Application\Routers\RouteList]クラスが提供します。その定義は、いわゆるルートの一覧、つまり URL アドレスのマスクと、それに結びつくプレゼンターやアクションから成り、単純な API で書けます。ルートに名前を付ける必要はまったくありません。
```php
$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...
```
この例からわかるように、ブラウザで `https://domain.com/rss.xml` を開くと `Feed` プレゼンターの `rss` アクションが表示されます。`https://domain.com/article/12` なら `Article` プレゼンターの `view` アクションが表示される、といった具合です。ふさわしいルートが見つからなければ、Nette Application は [BadRequestException |api:Nette\Application\BadRequestException]を投げて応え、ユーザーには 404 Not Found のエラーページとして表示されます。
ルートの順序
------
個々のルートを並べる**順序**はきわめて**重要**です。上から下へ順に評価されるからです。原則は、ルートを**具体的なものから一般的なものへ**と宣言することです。
```php
// 誤り: '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');
```
リンクを生成するときも、ルートは上から下へ評価されます。
```php
// 誤り: '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 です。
```php
$router->addRoute('products', 'Products:default');
```
マスクには**パラメータ**が入ることがよくあります。これは山かっこで囲まれ(たとえば `<year>`)、対象のプレゼンターに、たとえば `renderShow(int $year)` メソッドや永続パラメータ `$year` に渡されます。
```php
$router->addRoute('chronicle/<year>', 'History:show');
```
この例からわかるように、ブラウザで `https://example.com/chronicle/2020` を開くと、`History` プレゼンターの `show` アクションがパラメータ `year: 2020` とともに表示されます。
パラメータの既定値はマスクの中で直接指定でき、そうするとそのパラメータは省略できるようになります。
```php
$router->addRoute('chronicle/<year=2020>', 'History:show');
```
これでこのルートは `https://example.com/chronicle/` という URL も受け付け、やはり `History:show` をパラメータ `year: 2020` とともに表示します。
もちろんプレゼンターやアクションの名前もパラメータにできます。たとえば次のようにです。
```php
$router->addRoute('<presenter>/<action>', 'Home:default');
```
このルートはたとえば `/article/edit` や `/catalog/list` の形の URL を受け付け、それぞれプレゼンターとアクション `Article:edit`、`Catalog:list` と解釈します。
同時に `presenter` と `action` のパラメータに既定値 `Home` と `default` を与えるので、これらも省略できるようになります。ですからこのルートは `/article` という URL も受け付け、`Article:default` と解釈します。逆に `Product:default` へのリンクはパス `/product` を生成し、既定の `Home:default` へのリンクはパス `/` を生成します。
マスクはウェブサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始めれば絶対パスを、スラッシュ 2 つで始めれば絶対 URL 全体を表せます。
```php
// ドキュメントルートからの相対
$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>', /* ... */);
```
検証の正規表現
-------
パラメータごとに [正規表現|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]で検証の条件を指定できます。たとえば `id` パラメータには、正規表現 `\d+` で数字しか入れられないと指定します。
```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
```
すべてのパラメータの既定の正規表現は `[^/]+`、つまりスラッシュ以外のすべてです。パラメータにスラッシュも受け付けさせたいなら、正規表現を `.+` にします。
```php
// https://example.com/a/b/c を受け付け、path は 'a/b/c' になります
$router->addRoute('<path .+>', /* ... */);
```
省略できる部分
-------
マスクの中では、角かっこで省略できる部分に印を付けられます。マスクのどの部分も省略できるようにでき、その中にパラメータを入れられます。
```php
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// 次のパスを受け付けます:
// /en/download => lang => en, name => download
// /download => lang => null, name => download
```
パラメータが省略できる部分の中にあると、当然そのパラメータも省略できるようになります。既定値が指定されていなければ null になります。
省略できる部分はドメインにも置けます。
```php
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
```
省略できる部分は自由に入れ子にしたり組み合わせたりできます。
```php
$router->addRoute(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
'Home:default',
);
// 次のパスを受け付けます:
// /en/hello
// /en-us/hello
// /hello
// /hello/page-12
```
URL を生成するときは、もっとも短い形が優先されるので、省ける部分はすべて省かれます。ですからたとえばルート `index[.html]` はパス `/index` を生成します。この振る舞いは、開き角かっこのうしろに感嘆符を置くと逆にできます。
```php
// /hello と /hello.html を受け付け、/hello を生成します
$router->addRoute('<name>[.html]', /* ... */);
// /hello と /hello.html を受け付け、/hello.html を生成します
$router->addRoute('<name>[!.html]', /* ... */);
```
角かっこのない、省略できるパラメータ(つまり既定値を持つパラメータ)は、実質的に次のように囲まれているかのように振る舞います。
```php
$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */);
// これは次と同じです:
$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */);
```
末尾のスラッシュの振る舞いに手を入れて、たとえば `/home/` の代わりに `/home` を生成させたいなら、次のようにできます。
```php
$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */);
```
ワイルドカード
-------
絶対 URL のマスクでは次のワイルドカードを使えます。たとえば開発環境と本番環境で違うかもしれないドメインを、マスクに書かずに済ませられます。
- `%tld%` = トップレベルドメイン、たとえば `com` や `org`
- `%sld%` = セカンドレベルドメイン、たとえば `example`
- `%domain%` = サブドメインを除いたドメイン、たとえば `example.com`
- `%host%` = ホスト全体、たとえば `www.example.com`
- `%basePath%` = ルートディレクトリへのパス
```php
$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action>', /* ... */);
```
詳しい書き方
------
ふつう `Presenter:action` の形で書くルートの行き先は、個々のパラメータとその既定値を定義する配列でも書けます。
```php
$router->addRoute('<presenter>/<action>[/<id \d+>]', [
'presenter' => 'Home',
'action' => 'default',
]);
```
より細かく指定するには、さらに広げた形も使えます。そこでは既定値のほかに、検証の正規表現などパラメータのほかの性質も設定できます(`id` パラメータをご覧ください)。
```php
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>[/<id>]', [
'presenter' => [
Route::Value => 'Home',
],
'action' => [
Route::Value => 'default',
],
'id' => [
Route::Pattern => '\d+',
],
]);
```
大事なのは、配列で定義したパラメータがパスのマスクに現れていない場合、その値は変えられないという点です。URL の疑問符のうしろに書くクエリパラメータでも変えられません。
これは**固定のパラメータ**、つまりあるページに短く覚えやすい URL を与えるのに役立ちます。たとえば `/tos` がいつも `Article:view` を `id: 123` で開くようにするには次のようにします。
```php
$router->addRoute('tos', [
'presenter' => 'Article',
'action' => 'view',
'id' => 123,
]);
```
フィルタと変換
-------
アプリケーションのソースコードは英語で書きますが、ウェブサイトの URL をチェコ語にしたい場合、次のような単純なルーティングでは、
```php
$router->addRoute('<presenter>/<action>', 'Home:default');
```
`/product/123` や `/cart` のような英語の URL が生成されます。URL の中のプレゼンターとアクションをチェコ語(たとえば `/produkt/123` や `/kosik`)で表したいなら、変換の辞書を使えます。それを書くには、第 2 パラメータの「詳しい」書き方がもう必要です。
```php
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterTable => [
// URL の中の文字列 => プレゼンター
'produkt' => 'Product',
'kosik' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::Value => 'default',
Route::FilterTable => [
'seznam' => 'list',
],
],
]);
```
変換の辞書では、複数のキーが同じプレゼンターを指しても構いません。そうするとそのプレゼンターにいろいろな別名ができます。最後のキーが正式な形(つまり生成される URL に入る形)と見なされます。
変換の表はこのやり方でどのパラメータにも使えます。変換が存在しなければ、もとの値がそのまま使われます。この振る舞いは `Route::FilterStrict => true` を足すと変えられ、値が辞書にない場合、そのルートは URL を受け付けなくなります。
配列の形の変換の辞書のほかに、独自の変換の関数も使えます。
```php
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 の書き方と、URL で使われる kebab-case とのあいだを変換するフィルタがあらかじめ用意されています。パラメータの既定値は、アプリケーションに渡される形(プレゼンターとモジュールは PascalCase、アクションは camelCase)で書くので、たとえばプレゼンターなら `<presenter=ProductEdit>` と書き、`<presenter=product-edit>` とは書きません。
一般のフィルタ
-------
特定のパラメータ向けのフィルタのほかに、一般のフィルタも定義できます。これはすべてのパラメータの連想配列を受け取り、好きなように変えて返せます。一般のフィルタは空のキーの下に定義します。
```php
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Home',
'action' => 'default',
'' => [
Route::FilterIn => function (array $params): array { /* ... */ },
Route::FilterOut => function (array $params): array { /* ... */ },
],
]);
```
一般のフィルタを使えば、ルートの振る舞いを本当に好きなように変えられます。たとえばほかのパラメータをもとにパラメータを変えるのに使えます。たとえば `<lang>` パラメータの今の値をもとに `<presenter>` と `<action>` を変換する、といった具合です。
パラメータに独自のフィルタが定義されていて、一般のフィルタもある場合、独自の `FilterIn` が一般のものより先に実行され、逆に一般の `FilterOut` が独自のものより先に実行されます。ですから一般のフィルタの中では、`presenter` と `action` のパラメータの値はそれぞれ PascalCase と camelCase の書き方になっています。
これらのフィルタの実用的な使い方は [スラッグ付きのきれいな URL |best-practices:pretty-urls]をご覧ください。テンプレートを一切変えずに `/article/123-how-to-bake-bread` のような SEO にやさしい URL を生成します。
OneWay フラグ
----------
一方通行のルートは、アプリケーションがもう生成しないけれど受け付けはする古い URL を生かしておくのに使います。`OneWay` フラグで印を付けます。
```php
// 古い URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// 新しい URL /product/123
$router->addRoute('product/<id>', 'Product:detail');
```
古い URL にアクセスすると、プレゼンターが自動的に新しい URL へリダイレクトするので、検索エンジンがこれらのページを二重に登録することはありません([#SEO と正規化]をご覧ください)。
コールバックによる動的なルーティング
------------------
コールバックによる動的なルーティングを使うと、ルートに関数(コールバック)を直接割り当てられ、そのパスが訪れられたときに実行されます。この柔軟な機能のおかげで、アプリケーションのさまざまなエンドポイントを手早く効率よく作れます。
```php
$router->addRoute('test', function () {
echo 'You are at the /test address';
});
```
マスクの中にパラメータを定義することもでき、それは自動的にコールバックに渡されます。
```php
$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!',
};
});
```
マスクからのパラメータのほかに、コールバックは DI コンテナのサービスも受け取れます。それはパラメータの型をもとに渡されます。さらに `$presenter` パラメータには、そのルートを処理する [MicroPresenter |api:NetteModule\MicroPresenter]のインスタンスが渡されます。
```php
$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
// ...
});
```
モジュール
-----
共通の[モジュール |directory-structure#プレゼンターとテンプレート]に属するルートが複数あるなら、`withModule()` を使います。指定したモジュールは、そのグループのすべてのルートのプレゼンターに自動的に前置され、URL からはすっかり消えます。
```php
$router = new RouteList;
$router->withModule('Forum') // 以下のルートは Forum モジュールの一部です
->addRoute('rss', 'Feed:rss') // プレゼンターは Forum:Feed になります
->addRoute('<presenter>/<action>')
->withModule('Admin') // 以下のルートは Forum:Admin モジュールの一部です
->addRoute('sign:in', 'Sign:in');
```
代わりに `module` パラメータも使えます。これも同じように固定のモジュールを設定し、URL には現れないようにします。
```php
// URL manage/dashboard/default はプレゼンター Admin:Dashboard に対応します
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin',
]);
```
プレゼンターの名前は、そのモジュールと合わせてはじめて完全になります。たとえば `Front:Admin:ProductList` です。こうした完全な名前が URL のパラメータに入るとき、ルーターは 2 つの単純な規則でそれを符号化します。コロン `:`(モジュールの区切り)はすべて**ドット**になり、PascalCase の名前の語の切れ目はすべて**ハイフン**になります。ですから `Front:Admin:ProductList` は URL の中で `front.admin.product-list` として現れ、同じやり方で読み戻されます。モジュールに分かれたアプリケーションが、上のような道具を使わないとドットだらけの URL を作るのは、これが理由です。
`withModule()` も `module` パラメータも、まさにこれを避けます。プレゼンターの名前が URL に届く前に、分かっているモジュールの接頭辞を取り除くからです。モジュールが決まりきったものである以上、そもそも符号化する必要はありません。
ときにはモジュール自体を変えられるようにして、URL に現れてほしいこともあります。そこでマスクの中で `<module>` を直接使います。ただし大事な点にご注意ください。**`<module>` はモジュールのパス全体**、つまりプレゼンターの名前の最後のコロンまでを取り込みます。プレゼンター `Shop:Admin:Product` ならモジュールは `Shop:Admin`、プレゼンターは `Product` ということになり、コロンはドットになるので、次のようになります。
サブドメイン
------
ルートのコレクションはサブドメインごとに分けられます。
```php
$router = new RouteList;
$router->withDomain('example.com')
->addRoute('rss', 'Feed:rss')
->addRoute('<presenter>/<action>');
```
ドメイン名には [#ワイルドカード]も使えます。
```php
$router = new RouteList;
$router->withDomain('example.%tld%')
// ...
```
パスの接頭辞
------
ルートのコレクションは URL のパスごとに分けられます。
```php
$router = new RouteList;
$router->withPath('eshop')
->addRoute('rss', 'Feed:rss') // URL /eshop/rss に一致します
->addRoute('<presenter>/<action>'); // URL /eshop/<presenter>/<action> に一致します
```
組み合わせ
-----
上のグループ分けは互いに組み合わせられます。
```php
$router = (new RouteList)
->withDomain('admin.example.com')
->withModule('Admin')
->addRoute(/* ... */)
->addRoute(/* ... */)
->end()
->withModule('Images')
->addRoute(/* ... */)
->end()
->end()
->withDomain('example.com')
->withPath('export')
->addRoute(/* ... */)
// ...
```
クエリパラメータ
--------
マスクにはクエリパラメータ(URL の疑問符のうしろのパラメータ)も入れられます。これには検証の正規表現を定義できませんが、プレゼンターに渡される名前は変えられます。
```php
// クエリパラメータ 'cat' をアプリケーションでは 'categoryId' という名前で使いたい
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);
```
Foo パラメータ
---------
さらに深いところへ進みます。Foo パラメータは要するに名前のないパラメータで、正規表現に一致させられます。例として `/index`、`/index.html`、`/index.htm`、`/index.php` を受け付けるルートを挙げます。
```php
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
```
URL を生成するときに使われる文字列を、はっきり定義することもできます。その文字列は疑問符のすぐうしろに置かなければなりません。次のルートは前のものと似ていますが、`/index` の代わりに `/index.html` を生成します。生成に使う値として文字列 `.html` が設定されているからです。
```php
$router->addRoute('index<?.html \.html?|\.php|>', /* ... */);
```
組み込み
====
作ったルーターをアプリケーションに組み込むには、DI コンテナにそれを教える必要があります。もっとも簡単なのは、ルーターのオブジェクトを作るファクトリを用意して、設定でそれを使うようにコンテナに伝えることです。そのために `App\Core\RouterFactory::createRouter()` メソッドを書くとしましょう。
```php
namespace App\Core;
use Nette\Application\Routers\RouteList;
class RouterFactory
{
public static function createRouter(): RouteList
{
$router = new RouteList;
$router->addRoute(/* ... */);
return $router;
}
}
```
そして[設定 |dependency-injection:services]に次のように書きます。
```neon
services:
- App\Core\RouterFactory::createRouter
```
データベースなどへの依存関係があれば、[オートワイヤリング|dependency-injection:autowiring]によってファクトリメソッドのパラメータとして渡されます。
```php
public static function createRouter(Nette\Database\Connection $db): RouteList
{
// ...
}
```
SimpleRouter
============
ルートのコレクションよりずっと単純なルーターが [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]です。URL の形に特別な要求がないとき、`mod_rewrite`(やその代わりになるもの)が使えないとき、あるいはまだきれいな URL に手を出したくないときに使います。
だいたい次のような形のアドレスを生成します。
```
http://example.com/?presenter=Product&action=detail&id=123
```
`SimpleRouter` のコンストラクタのパラメータは既定のプレゼンターとアクション、つまり `http://example.com/` をほかのパラメータなしで開いたときに実行されるアクションです。
```php
// 既定のプレゼンターは 'Home'、アクションは 'default' になります
$router = new Nette\Application\Routers\SimpleRouter('Home:default');
```
SimpleRouter は[設定 |dependency-injection:services]で直接定義することをおすすめします。
```neon
services:
- Nette\Application\Routers\SimpleRouter('Home:default')
```
SEO と正規化
========
フレームワークは、異なる URL に同じ内容が現れるのを防ぐことで SEO(検索エンジン最適化)に貢献します。ある行き先に複数のアドレス、たとえば `/index` と `/index.html` が通じている場合、フレームワークは最初のものを主要なもの(正式なもの)と定め、ほかはそこへ HTTP コード 301 でリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがありません。
この処理を正規化と呼びます。正式な URL は、ルーターが生成するもの、つまりコレクションの中で最初に一致した OneWay フラグのないルートが生成するものです。ですからコレクションでは**主要なルートを先に**並べます。
正規化はプレゼンターが行います。詳しくは [正規化 |presenters#正規化]の章をご覧ください。
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 を生成するので、これ以上の設定は要りません。
とはいえ、例外的にルートごとに違うプロトコルで動かす必要があるなら、ルートのマスクで指定します。
```php
// HTTP のアドレスを生成します
$router->addRoute('http://%host%/<presenter>/<action>', /* ... */);
// HTTPS のアドレスを生成します
$router->addRoute('https://%host%/<presenter>/<action>', /* ... */);
```
ルーターのデバッグ
=========
[Tracy Bar |tracy:]に表示されるルーティングのパネルは役に立つ助っ人で、ルートの一覧と、ルーターが URL から取り出したパラメータを見せてくれます。
✓ の印が付いた緑の帯は、現在の URL を処理したルートを表します。青色と ≈ の印は、緑のルートに先を越されなければやはりその URL を処理したであろうルートを示します。さらに現在のプレゼンターとアクションも見られます。
[* routing-debugger.webp *]
同時に、[正規化 |#SEO と正規化]によって思いがけないリダイレクトが起きたときは、パネルの *redirect* の帯を見るとよいでしょう。そこでルーターがもともとその URL をどう解釈し、なぜリダイレクトしたのかがわかります。
.[note]
ルーターをデバッグするときは、ブラウザで開発者ツールを開き(Ctrl+Shift+I または Cmd+Option+I)、Network のパネルでキャッシュを無効にして、リダイレクトが保存されないようにすることをおすすめします。
性能
===
ルートの数はルーターの速さに影響します。その数は数十を超えないようにすべきです。ウェブサイトの URL の構造が複雑すぎるなら、独自の[ルーター |#独自のルーター]を書けます。
ルーターがデータベースなどへの依存関係を持たず、そのファクトリが引数を取らないなら、その組み立て済みの形を DI コンテナに直接シリアライズして、アプリケーションをわずかに速くできます。
```neon
routing:
cache: true
```
独自のルーター
=======
以下の記述はとても進んだ利用者向けです。独自のルーターを作って、ルートのコレクションに自然に組み込めます。ルーターは 2 つのメソッドを持つ [api:Nette\Routing\Router]インターフェースの実装です。
```php
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 |http:request](そこからは URL だけでなくヘッダーなども取り出せます)を、プレゼンターの名前とそのパラメータを含む配列に変えます。リクエストを処理できなければ null を返します。リクエストを処理するときは、少なくともプレゼンターを返さなければなりません。アクションは省略でき、指定がなければ `default` になります。プレゼンターの名前は完全で、モジュールも含みます。
```php
[
'presenter' => 'Front:Home',
'action' => 'default',
]
```
一方 `constructUrl` メソッドは、パラメータの配列から結果となる絶対 URL を組み立てます。そのとき現在の URL である [`$refUrl`|api:Nette\Http\UrlScript]パラメータの情報を使えます。
`add()` でルートのコレクションに足します。
```php
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
```
単独での利用
======
単独での利用とは、Nette Application とプレゼンターを使わないアプリケーションでルーターの機能を活かすことです。この章で見てきたことのほとんどはそこでも当てはまり、違いは次の点だけです。
- ルートのコレクションには [api:Nette\Routing\RouteList]クラスを使います
- 単純なルーターには [api:Nette\Routing\SimpleRouter]クラスを使います
- `Presenter:action` の組が存在しないので、[#詳しい書き方]を使います
ですからここでもルーターを組み立てるメソッドを作ります。たとえば次のようにです。
```php
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 のリクエストとともに受け取ります。
```php
$router = $container->getByType(Nette\Routing\Router::class);
$httpRequest = $container->getByType(Nette\Http\IRequest::class);
```
あるいはオブジェクトを直接作ります。
```php
$router = App\Core\RouterFactory::createRouter();
$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals();
```
あとはルーターに仕事をさせるだけです。
```php
$params = $router->match($httpRequest);
if ($params === null) {
// 一致するルートが見つからなかったので 404 のエラーを送ります
exit;
}
// 得られたパラメータを処理します
$controller = $params['controller'];
// ...
```
逆にルーターを使ってリンクを組み立てます。
```php
$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());
```
{{composer: nette/routing}}
{{repo: nette/routing}}
{{api: https://api.nette.org/routing/}}
ルーティング
ルーターは URL アドレスにまつわるすべてを引き受けるので、あなたが URL
のことを考える必要はありません。ここでは次のことを扱います。
URL を思いどおりの見た目にするためのルーターの設定
SEO とリダイレクト
そして独自のルーターの書き方
人にやさしい URL(かっこいい URL、きれいな URL
とも呼ばれます)は使いやすく、覚えやすく、SEO にもよい影響を与えます。Nette
はそれを心得ていて、開発者の求めるものにしっかり応えます。アプリケーションの URL
の構造を、思いどおりに設計できます。しかもアプリケーションが完成したあとでも設計できます。コードやテンプレートの変更が要らないからです。それはすべてのプレゼンターにアノテーションとして散らばるのではなく、ただ一か所 、つまりルーターで優雅に定義されます。
Nette のルーターが並外れているのは、双方向 だからです。HTTP のリクエストから URL
を読み解くことも、リンクを作ることもできます。ですから Nette
Application で欠かせない役割を果たします。現在のリクエストをどのプレゼンターとアクションが処理するかを決めるだけでなく、テンプレートなどで
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://domain.com/rss.xml を開くと Feed
プレゼンターの rss アクションが表示されます。https://domain.com/article/12 なら
Article プレゼンターの view
アクションが表示される、といった具合です。ふさわしいルートが見つからなければ、Nette
Application は 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://example.com/chronicle/2020
を開くと、History プレゼンターの show アクションがパラメータ
year: 2020 とともに表示されます。
パラメータの既定値はマスクの中で直接指定でき、そうするとそのパラメータは省略できるようになります。
$router->addRoute('chronicle/<year=2020>', 'History:show');
これでこのルートは https://example.com/chronicle/ という URL も受け付け、やはり
History:show をパラメータ year: 2020 とともに表示します。
もちろんプレゼンターやアクションの名前もパラメータにできます。たとえば次のようにです。
$router->addRoute('<presenter>/<action>', 'Home:default');
このルートはたとえば /article/edit や /catalog/list の形の URL
を受け付け、それぞれプレゼンターとアクション Article:edit、Catalog:list
と解釈します。
同時に presenter と action のパラメータに既定値 Home と
default を与えるので、これらも省略できるようになります。ですからこのルートは
/article という URL も受け付け、Article:default と解釈します。逆に
Product:default へのリンクはパス /product を生成し、既定の Home:default
へのリンクはパス / を生成します。
マスクはウェブサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始めれば絶対パスを、スラッシュ
2 つで始めれば絶対 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+ で数字しか入れられないと指定します。
$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */);
すべてのパラメータの既定の正規表現は
[^/]+、つまりスラッシュ以外のすべてです。パラメータにスラッシュも受け付けさせたいなら、正規表現を
.+ にします。
// https://example.com/a/b/c を受け付け、path は 'a/b/c' になります
$router->addRoute('<path .+>', /* ... */);
省略できる部分
マスクの中では、角かっこで省略できる部分に印を付けられます。マスクのどの部分も省略できるようにでき、その中にパラメータを入れられます。
$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */);
// 次のパスを受け付けます:
// /en/download => lang => en, name => download
// /download => lang => null, name => download
パラメータが省略できる部分の中にあると、当然そのパラメータも省略できるようになります。既定値が指定されていなければ
null になります。
省略できる部分はドメインにも置けます。
$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */);
省略できる部分は自由に入れ子にしたり組み合わせたりできます。
$router->addRoute(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
'Home:default',
);
// 次のパスを受け付けます:
// /en/hello
// /en-us/hello
// /hello
// /hello/page-12
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>]]]', /* ... */);
ワイルドカード
絶対 URL
のマスクでは次のワイルドカードを使えます。たとえば開発環境と本番環境で違うかもしれないドメインを、マスクに書かずに済ませられます。
%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
を与えるのに役立ちます。たとえば /tos がいつも Article:view を id: 123
で開くようにするには次のようにします。
$router->addRoute('tos', [
'presenter' => 'Article',
'action' => 'view',
'id' => 123,
]);
フィルタと変換
アプリケーションのソースコードは英語で書きますが、ウェブサイトの URL
をチェコ語にしたい場合、次のような単純なルーティングでは、
$router->addRoute('<presenter>/<action>', 'Home:default');
/product/123 や /cart のような英語の URL が生成されます。URL
の中のプレゼンターとアクションをチェコ語(たとえば /produkt/123 や
/kosik)で表したいなら、変換の辞書を使えます。それを書くには、第 2
パラメータの「詳しい」書き方がもう必要です。
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => [
Route::Value => 'Home',
Route::FilterTable => [
// URL の中の文字列 => プレゼンター
'produkt' => 'Product',
'kosik' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::Value => 'default',
Route::FilterTable => [
'seznam' => '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
の書き方と、URL で使われる kebab-case
とのあいだを変換するフィルタがあらかじめ用意されています。パラメータの既定値は、アプリケーションに渡される形(プレゼンターとモジュールは
PascalCase、アクションは camelCase)で書くので、たとえばプレゼンターなら
<presenter=ProductEdit> と書き、<presenter=product-edit> とは書きません。
一般のフィルタ
特定のパラメータ向けのフィルタのほかに、一般のフィルタも定義できます。これはすべてのパラメータの連想配列を受け取り、好きなように変えて返せます。一般のフィルタは空のキーの下に定義します。
use Nette\Routing\Route;
$router->addRoute('<presenter>/<action>', [
'presenter' => 'Home',
'action' => 'default',
'' => [
Route::FilterIn => function (array $params): array { /* ... */ },
Route::FilterOut => function (array $params): array { /* ... */ },
],
]);
一般のフィルタを使えば、ルートの振る舞いを本当に好きなように変えられます。たとえばほかのパラメータをもとにパラメータを変えるのに使えます。たとえば
<lang> パラメータの今の値をもとに <presenter> と <action>
を変換する、といった具合です。
パラメータに独自のフィルタが定義されていて、一般のフィルタもある場合、独自の
FilterIn が一般のものより先に実行され、逆に一般の FilterOut
が独自のものより先に実行されます。ですから一般のフィルタの中では、presenter と
action のパラメータの値はそれぞれ PascalCase と camelCase の書き方になっています。
これらのフィルタの実用的な使い方は スラッグ付きのきれいな
URL をご覧ください。テンプレートを一切変えずに /article/123-how-to-bake-bread のような
SEO にやさしい URL を生成します。
OneWay フラグ
一方通行のルートは、アプリケーションがもう生成しないけれど受け付けはする古い URL
を生かしておくのに使います。OneWay フラグで印を付けます。
// 古い URL /product-info?id=123
$router->addRoute('product-info', 'Product:detail', oneWay: true);
// 新しい 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!',
};
});
マスクからのパラメータのほかに、コールバックは DI
コンテナのサービスも受け取れます。それはパラメータの型をもとに渡されます。さらに
$presenter パラメータには、そのルートを処理する MicroPresenter のインスタンスが渡されます。
$router->addRoute('<lang cs|en>', function (string $lang, Nette\Http\Request $httpRequest, NetteModule\MicroPresenter $presenter) {
// ...
});
モジュール
共通のモジュール に属するルートが複数あるなら、withModule()
を使います。指定したモジュールは、そのグループのすべてのルートのプレゼンターに自動的に前置され、URL
からはすっかり消えます。
$router = new RouteList;
$router->withModule('Forum') // 以下のルートは Forum モジュールの一部です
->addRoute('rss', 'Feed:rss') // プレゼンターは Forum:Feed になります
->addRoute('<presenter>/<action>')
->withModule('Admin') // 以下のルートは Forum:Admin モジュールの一部です
->addRoute('sign:in', 'Sign:in');
代わりに module
パラメータも使えます。これも同じように固定のモジュールを設定し、URL
には現れないようにします。
// URL manage/dashboard/default はプレゼンター Admin:Dashboard に対応します
$router->addRoute('manage/<presenter>/<action>', [
'module' => 'Admin',
]);
プレゼンターの名前は、そのモジュールと合わせてはじめて完全になります。たとえば
Front:Admin:ProductList です。こうした完全な名前が URL
のパラメータに入るとき、ルーターは 2 つの単純な規則でそれを符号化します。コロン
:(モジュールの区切り)はすべてドット になり、PascalCase
の名前の語の切れ目はすべてハイフン になります。ですから Front:Admin:ProductList は
URL の中で front.admin.product-list
として現れ、同じやり方で読み戻されます。モジュールに分かれたアプリケーションが、上のような道具を使わないとドットだらけの
URL を作るのは、これが理由です。
withModule() も module
パラメータも、まさにこれを避けます。プレゼンターの名前が URL
に届く前に、分かっているモジュールの接頭辞を取り除くからです。モジュールが決まりきったものである以上、そもそも符号化する必要はありません。
ときにはモジュール自体を変えられるようにして、URL
に現れてほしいこともあります。そこでマスクの中で <module>
を直接使います。ただし大事な点にご注意ください。<module>
はモジュールのパス全体 、つまりプレゼンターの名前の最後のコロンまでを取り込みます。プレゼンター
Shop:Admin:Product ならモジュールは Shop:Admin、プレゼンターは Product
ということになり、コロンはドットになるので、次のようになります。
サブドメイン
ルートのコレクションはサブドメインごとに分けられます。
$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
パラメータは要するに名前のないパラメータで、正規表現に一致させられます。例として
/index、/index.html、/index.htm、/index.php
を受け付けるルートを挙げます。
$router->addRoute('index<? \.html?|\.php|>', /* ... */);
URL
を生成するときに使われる文字列を、はっきり定義することもできます。その文字列は疑問符のすぐうしろに置かなければなりません。次のルートは前のものと似ていますが、/index
の代わりに /index.html を生成します。生成に使う値として文字列 .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 と正規化
フレームワークは、異なる URL に同じ内容が現れるのを防ぐことで
SEO(検索エンジン最適化)に貢献します。ある行き先に複数のアドレス、たとえば
/index と /index.html
が通じている場合、フレームワークは最初のものを主要なもの(正式なもの)と定め、ほかはそこへ
HTTP コード 301
でリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがありません。
この処理を正規化と呼びます。正式な 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
Bar に表示されるルーティングのパネルは役に立つ助っ人で、ルートの一覧と、ルーターが URL
から取り出したパラメータを見せてくれます。
✓ の印が付いた緑の帯は、現在の URL を処理したルートを表します。青色と ≈
の印は、緑のルートに先を越されなければやはりその URL
を処理したであろうルートを示します。さらに現在のプレゼンターとアクションも見られます。
同時に、正規化 によって思いがけないリダイレクトが起きたときは、パネルの
redirect の帯を見るとよいでしょう。そこでルーターがもともとその URL
をどう解釈し、なぜリダイレクトしたのかがわかります。
ルーターをデバッグするときは、ブラウザで開発者ツールを開き(Ctrl+Shift+I または
Cmd+Option+I)、Network
のパネルでキャッシュを無効にして、リダイレクトが保存されないようにすることをおすすめします。
性能
ルートの数はルーターの速さに影響します。その数は数十を超えないようにすべきです。ウェブサイトの
URL の構造が複雑すぎるなら、独自のルーター を書けます。
ルーターがデータベースなどへの依存関係を持たず、そのファクトリが引数を取らないなら、その組み立て済みの形を
DI コンテナに直接シリアライズして、アプリケーションをわずかに速くできます。
routing:
cache: true
独自のルーター
以下の記述はとても進んだ利用者向けです。独自のルーターを作って、ルートのコレクションに自然に組み込めます。ルーターは
2 つのメソッドを持つ Nette\Routing\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
を返します。リクエストを処理するときは、少なくともプレゼンターを返さなければなりません。アクションは省略でき、指定がなければ
default になります。プレゼンターの名前は完全で、モジュールも含みます。
[
'presenter' => 'Front:Home',
'action' => 'default',
]
一方 constructUrl メソッドは、パラメータの配列から結果となる絶対 URL
を組み立てます。そのとき現在の URL である $refUrl パラメータの情報を使えます。
add() でルートのコレクションに足します。
$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...
単独での利用
単独での利用とは、Nette Application
とプレゼンターを使わないアプリケーションでルーターの機能を活かすことです。この章で見てきたことのほとんどはそこでも当てはまり、違いは次の点だけです。
ですからここでもルーターを組み立てるメソッドを作ります。たとえば次のようにです。
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());