ルーティング

ルータはURLアドレスに関するすべてを担当するため、もはやそれについて考える必要はありません。以下に示します:

  • URLが期待通りになるようにルータを設定する方法
  • SEOとリダイレクトについて説明します
  • そして、独自のルータを作成する方法を示します

より人間的なURL(クールまたはプリティURLとも呼ばれます)は、より使いやすく、覚えやすく、SEOに積極的に貢献します。Netteはこれを考慮し、開発者の要望に完全に応えます。アプリケーション用に、まさにあなたが望むURLアドレス構造を設計できます。 コードやテンプレートへの介入なしに行えるため、アプリケーションがすでに完成している時点でも設計できます。それは、ルータ内の1つの場所でエレガントな方法で定義され、すべてのPresenterのアノテーションに散らばることはありません。

Netteのルータは、双方向であるという点で特別です。HTTPリクエスト内のURLをデコードするだけでなく、リンクを作成することもできます。したがって、Nette Applicationにおいて重要な役割を果たします。なぜなら、現在のリクエストを実行するPresenterとアクションを決定するだけでなく、テンプレートなどでURLを生成するためにも使用されるからです。

ただし、ルータはこの用途に限定されません。Presenterをまったく使用しないアプリケーション、REST APIなどで使用できます。詳細はsamostatné použitíセクションを参照してください。

ルートコレクション

アプリケーションのURLアドレスの形式を定義する最も快適な方法は、Nette\Application\Routers\RouteListクラスを使用することです。定義は、いわゆるルートのリスト、つまりURLアドレスのマスクと、それに関連付けられたPresenterおよびアクションで構成され、シンプルなAPIを使用して行われます。ルートに名前を付ける必要はありません。

$router = new Nette\Application\Routers\RouteList;
$router->addRoute('rss.xml', 'Feed:rss');
$router->addRoute('article/<id>', 'Article:view');
// ...

この例は、ブラウザでhttps://domain.com/rss.xmlを開くと、Presenter Feedとアクション rssが表示され、https://domain.com/article/12を開くと、Presenter 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');

ルートを正しく組み立てるには、ある程度のスキルが必要であることを隠すつもりはありません。それを習得するまでは、ルーティングパネルが便利なツールになります。

マスクとパラメータ

マスクは、Webサイトのルートディレクトリからの相対パスを記述します。最も単純なマスクは静的なURLです:

$router->addRoute('products', 'Products:default');

マスクには、いわゆるパラメータが含まれることがよくあります。これらは山括弧(例:<year>)で示され、ターゲットPresenterに渡されます。たとえば、メソッド renderShow(int $year) や永続パラメータ $year に渡されます:

$router->addRoute('chronicle/<year>', 'History:show');

この例は、ブラウザで https://example.com/chronicle/2020 を開くと、Presenter History とアクション show がパラメータ year: 2020 とともに表示されることを示しています。

パラメータには、マスク内で直接デフォルト値を指定でき、これによりオプションになります:

$router->addRoute('chronicle/<year=2020>', 'History:show');

これで、ルートはURL https://example.com/chronicle/ も受け入れ、これも History:show をパラメータ year: 2020 とともに表示します。

パラメータは、もちろんPresenter名やアクション名にもなり得ます。たとえば、このように:

$router->addRoute('<presenter>/<action>', 'Home:default');

指定されたルートは、たとえば /article/edit/catalog/list の形式のURLを受け入れ、それらをPresenterとアクション Article:edit および Catalog:list として解釈します。

同時に、パラメータ presenteraction にデフォルト値 Homedefault を与え、それらもオプションになります。したがって、ルートは /article の形式のURLも受け入れ、それを Article:default として解釈します。または逆に、Product:default へのリンクはパス /product を生成し、デフォルトの Home:default へのリンクはパス / を生成します。

マスクは、Webサイトのルートディレクトリからの相対パスだけでなく、スラッシュで始まる場合は絶対パス、または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>', /* ... */);

// 受け入れるパス:
//    /cs/download  => lang => cs, 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',
);

// 受け入れるパス:
// 	/cs/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>]]]', /* ... */);

ワイルドカード

絶対パスのマスクでは、次のワイルドカードを使用して、たとえば開発環境と本番環境で異なる可能性のあるドメインをマスクに書き込む必要性を回避できます:

  • %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の疑問符の後に指定されたクエリパラメータを使用しても変更できないことに注意することが重要です。

フィルタと翻訳

アプリケーションのソースコードは英語で記述しますが、Webサイトにチェコ語のURLが必要な場合は、次のような単純なルーティングでは:

$router->addRoute('<presenter>/<action>', 'Home:default');

/product/123/cart のような英語のURLが生成されます。URL内のPresenterとアクションをチェコ語の単語(例:/produkt/123/kosik)で表現したい場合は、翻訳辞書を使用できます。その記述には、2番目のパラメータのより「冗長な」バリアントが必要です:

use Nette\Routing\Route;

$router->addRoute('<presenter>/<action>', [
	'presenter' => [
		Route::Value => 'Home',
		Route::FilterTable => [
			// URL内の文字列 => presenter
			'produkt' => 'Product',
			'kosik' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'seznam' => 'list',
		],
	],
]);

翻訳辞書の複数のキーが同じPresenterにつながる可能性があります。これにより、異なるエイリアスが作成されます。正規のバリアント(つまり、生成された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内のパラメータとPresenterに渡される文字列の間で変換を行い、関数 FilterOut は逆方向の変換を保証します。

パラメータ presenteractionmodule には、PascalCaseまたはcamelCaseスタイルとURLで使用されるkebab-caseの間で変換を行う事前定義されたフィルタがすでにあります。パラメータのデフォルト値はすでに変換された形式で記述されるため、たとえばPresenterの場合は <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 { /* ... */ },
	],
]);

一般フィルタを使用すると、ルートの動作を完全に任意の方法で変更できます。たとえば、他のパラメータに基づいてパラメータを変更するために使用できます。たとえば、パラメータ <lang> の現在の値に基づいて <presenter><action> を翻訳するなどです。

パラメータに独自のフィルタが定義されており、同時に一般フィルタが存在する場合、独自の FilterIn が一般フィルタの前に実行され、逆に一般フィルタの FilterOut が独自のフィルタの前に実行されます。したがって、一般フィルタ内では、パラメータ presenter および action の値はPascalCaseおよびcamelCaseスタイルで記述されます。

一方向OneWay

一方向ルートは、アプリケーションがもはや生成しないが、まだ受け入れている古い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にアクセスすると、Presenterは自動的に新しいURLにリダイレクトするため、検索エンジンはこれらのページを2回インデックス付けしません(SEOとカノニカル化を参照)。

コールバックによる動的ルーティング

コールバックによる動的ルーティングを使用すると、ルートに直接関数(コールバック)を割り当てることができ、特定のパスが訪問されたときに実行されます。この柔軟な機能により、アプリケーションのさまざまなエンドポイントを迅速かつ効率的に作成できます:

$router->addRoute('test', function () {
	echo 'あなたは /test アドレスにいます';
});

マスクにパラメータを定義することもでき、それらは自動的にコールバックに渡されます:

$router->addRoute('<lang cs|en>', function (string $lang) {
	echo match ($lang) {
		'cs' => '私たちのウェブサイトのチェコ語版へようこそ!',
		'en' => 'Welcome to the English version of our website!',
	};
});

モジュール

共通のモジュールに属する複数のルートがある場合は、withModule() を使用します:

$router = new RouteList;
$router->withModule('Forum') // 以下のルートは Forum モジュールの一部です
	->addRoute('rss', 'Feed:rss') // presenter は Forum:Feed になります
	->addRoute('<presenter>/<action>')

	->withModule('Admin') // 以下のルートは Forum:Admin モジュールの一部です
		->addRoute('sign:in', 'Sign:in');

代替案は、パラメータ module を使用することです:

// URL manage/dashboard/default は Admin:Dashboard presenter にマッピングされます
$router->addRoute('manage/<presenter>/<action>', [
	'module' => 'Admin',
]);

サブドメイン

ルートコレクションをサブドメインごとに分割できます:

$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', 'Feed:rss')
	->addRoute('<presenter>/<action>');

ドメイン名にはzástupné znakyを使用することもできます:

$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の疑問符の後のパラメータ)も含めることができます。これらには検証式を定義できませんが、Presenterに渡される名前を変更できます:

// クエリパラメータ 'cat' をアプリケーションで 'categoryId' という名前で使用したい
$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */);

Fooパラメータ

さて、さらに深く掘り下げます。Fooパラメータは、基本的に名前のないパラメータであり、正規表現をマッチさせることができます。例としては、/index/index.html/index.htm/index.php を受け入れるルートがあります:

$router->addRoute('index<? \.html?|\.php|>', /* ... */);

URLを生成する際に使用される文字列を明示的に定義することもできます。文字列は疑問符の直後に配置する必要があります。次のルートは前のルートに似ていますが、文字列 .html が生成値として設定されているため、/index の代わりに /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

データベースなどへの依存関係は、autowiringを使用してファクトリメソッドにそのパラメータとして渡されます:

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/)にリダイレクトされるデフォルトのPresenterとアクションです。

// デフォルトのpresenterは '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でリダイレクトします。これにより、検索エンジンはページを2回インデックス付けせず、ページランクを希釈しません。

このプロセスはカノニカル化と呼ばれます。カノニカルURLは、ルータによって生成されるURL、つまりOneWayフラグのないコレクション内の最初の適合するルートです。したがって、コレクションではプライマリルートを最初にリストします。

カノニカル化はPresenterによって実行されます。詳細はカノニカル化の章を参照してください。

HTTPS

HTTPSプロトコルを使用するには、ホスティングで有効にし、サーバーを正しく設定する必要があります。

Webサイト全体を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を処理したであろうルートを示します。次に、現在のPresenterとアクションが表示されます。

同時に、カノニカル化による予期しないリダイレクトが発生した場合、redirectバーのパネルを見て、ルータが最初にURLをどのように理解し、なぜリダイレクトしたかを確認すると便利です。

ルータをデバッグする際には、ブラウザで開発者ツール(Ctrl+Shift+IまたはCmd+Option+I)を開き、ネットワークパネルでキャッシュを無効にして、リダイレクトがキャッシュされないようにすることをお勧めします。

パフォーマンス

ルートの数はルータの速度に影響します。その数は数十を超えるべきではありません。WebサイトのURL構造が複雑すぎる場合は、カスタムvlastní routerを作成できます。

ルータにデータベースなどの依存関係がなく、そのファクトリが引数を受け取らない場合は、そのコンパイル済み形式を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だけでなくヘッダーなども取得して、Presenter名とそのパラメータを含む配列に変換します。リクエストを処理できない場合は、nullを返します。 リクエストを処理する際には、少なくともPresenterとアクションを返す必要があります。Presenter名は完全であり、存在する可能性のあるモジュールも含まれます:

[
	'presenter' => 'Front:Home',
	'action' => 'default',
]

メソッド constructUrl は、逆にパラメータの配列から結果の絶対URLを構築します。そのために、パラメータ $refUrl(現在のURL)からの情報を使用できます。

add() を使用してルートコレクションに追加します:

$router = new Nette\Application\Routers\RouteList;
$router->add($myRouter);
$router->addRoute(/* ... */);
// ...

スタンドアロンでの使用

スタンドアロンでの使用とは、Nette ApplicationやPresenterを使用しないアプリケーションでルータの機能を利用することを意味します。この章で示したことのほとんどすべてが適用されますが、以下の違いがあります:

したがって、再びルータを構築するメソッドを作成します。例:

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());
バージョン: 4.0