バックエンド開発において、ルーティングは避けて通れない概念です。ルーティングとは、言うなればバックエンドの要。サーバーに到達する全てのリクエストが、ルーティングという仕組みを経て、コントローラにリダイレクトされます。

Laravelでは、複雑な実装の細部が簡略化されており、さまざまな糖衣構文を使用することができます。開発の経験値を問わず、ウェブアプリケーションの構築に役立つフレームワークです。

この記事では、Laravelのルーティングについて詳しくご紹介していきます。

バックエンドルーティングとXSS

サーバー上には、パブリックとプライベートのルート(経路)が存在し、前者はクロスサイトスクリプティング(XSS)の対象になることがあります。これは、インジェクション攻撃の一種で、悪意ある行為にさらされる可能性があるため、懸念が残ります。

問題となるのは、セッショントークンを必要としない経路から、セッショントークンが必要な経路にリダイレクトされ、結果としてトークンなしでアクセスできてしまうという点です。

これに対処する1番簡単な方法は、HTTPヘッダーの強制により経路にリファラ(referrer)を加えることです。

'main' => [
  'path' => '/main',
  'referrer' => 'required,refresh-empty',
  'target' => ControllerDashboardController::class . '::mainAction'
]

Laravelの基本的なルーティング

Laravelでは、ルートを使うことで、リクエストを目的のコントローラにルーティングすることができます。最も基本的なルートは、URI(ルートパス)とクロージャ(無名関数)を引数に取ります。

Laravelには、デフォルトでウェブ用とAPI用の2つのルートがあり、これの設定には、それぞれweb.phpapi.phpファイルを使用します。

どちらもroutes/フォルダに格納されるものの、Providers/RouteServiceProvider.phpで読み込まれます。

Providers/RouteServiceProvider.phpのデフォルトの状態
Providers/RouteServiceProvider.phpのデフォルトの状態

そのため、routes/フォルダを無視して、RouteServiceProvider.php内で直接ルートを読み込むことができます。

ルートをRouteServiceProvider.phpで読み込む
ルートをRouteServiceProvider.phpで読み込む

リダイレクト

ルートの定義は通常、アクセスするユーザーをリダイレクトするのに使用されます。これには、非推奨のルートを背景としてバックエンドやサーバーに変更を加えたため、または二要素認証(2FA)導入のためなど、さまざまな理由が考えられます。

Laravelでは簡単にリダイレクトを設定できます。このフレームワークならではのシンプルさにより、Routeファサードでredirectメソッドを使用可能です。引数は、起点となる、そしてリダイレクト先のルートを取ります。

また、3番目のパラメータとして、任意でリダイレクトのステータスコードを渡すことができます。permanentRedirectメソッドはredirectメソッドと同じ動作をしますが、ステータスコードは常に301が返されます。

// 簡易リダイレクト
Route::redirect("/class", "/myClass");

// 独自のステータスでリダイレクト
Route::redirect("/home", "/office", 305);

// ステータスコード301でルートリダイレクト
Route::permanentRedirect("/home", "office");

リダイレクトルート内では、Laravelがdestination、statusキーワードを保持しているため、パラメータとして使用することはできません。

// 使用禁止
Route::redirect("/home", "/office/{status}");

ビュー

ビューは、Laravelアプリケーションのフロントエンドをレンダリングするために使用する、.blade.phpファイルです。テンプレートエンジン「Blade」を使用しており、Laravelのみでフルスタックアプリケーションを構築するデフォルトの手法となっています。

ルートがビューを返すようにするには、Routeファサードでviewメソッドを使うだけでOKです。ルートパラメータ、ビュー名、ビューに渡される値といった配列を受け取ります。

// my-domain.com/homepageにアクセスすると
// homepage.blade.phpファイルがレンダリングされる
Route::view("/homepage", "homepage");

例えば、任意の配列をパラメータとして渡して「Hello, {name}」というビューを作成するには、以下のようなコードが利用できます(ビューのパラメータが十分ではないと、失敗しエラーが返されます)。

Route::view('/homepage', 'homepage', ['name' => "Kinsta"]);

ルートリスト

アプリケーションの規模が大きければ大きいほど、ルーティングが必要なリクエスト数も増えていきます。そしてリクエスト数が増えれば、混乱が生じるもの。

そこで役立つのが、artisan route:list commandです。アプリケーションで定義されているすべてのルート、ミドルウェア、コントローラの概要を確認することができます。

php artisan route:list

上記では、ミドルウェア以外のすべてのルートが一覧で表示されます。ミドルウェアを確認するには、-vを使用します。

php artisan route:list -v

ドメイン駆動型設計で、経路に特定の名前がある場合は、このコマンドのフィルタリング機能を以下のように利用できます。

php artisan route:list –path=api/account

これにより、api/accountで始まるルートのみ表示されます。

–except-vendorまたは–only-vendorを使うと、サードパーティで定義されたルートを除外または含めるように指定することができます。

ルートパラメータ

時には、ユーザーIDやトークンなど、URIのセグメントをルートで取得したいこともあるはずです。これは、ルートパラメータを定義することで実現可能です。ルートパラメータは常に中括弧({})で囲み、アルファベット文字のみで構成します。

ルートがコールバック内に依存性を持つ場合は、サービスコンテナによって自動で注入(依存性の注入)されます。

use IlluminateHttpRequest;
use Controllers/DashboardController;
Route::post('/dashboard/{id}, function (Request $request, string $id) {
  return 'User:' . $id;
}
Route::get('/dashboard/{id}, DashboardController.php);

必須のパラメータ

Laravelにおける必須パラメータとは、呼び出し時に無視することが許されないルートパラメータです。これを無視するとエラーが返されます。

Route::post("/gdpr/{userId}", GetGdprDataController.php");

これで、GetGdprDataController.php内で、 $userIdパラメータに直接アクセスできます。

public function __invoke(int $userId) {
  // 受け取ったuserIdを使用
}

ルートが受け取るパラメータの数に制限はありません。パラメータは、記述された順にルートコールバック/コントローラに注入されます。

 // api.php
Route::post('/gdpr/{userId}/{userName}/{userAge}', GetGdprDataController.php);
// GetGdprDataController.php
public function __invoke(int $userId, string $userName, int $userAge) {
  // パラメータを使用
}

任意のパラメータ

パラメータが1つしかない状態で、アプリケーション全体に影響を与えることなく、ルート上で何かを行いたい場合、任意のパラメータを使用することができます。以下のように?を付加して示します。

 Route::get('/user/{age?}', function (int $age = null) {
  if (!$age) Log::info("User doesn't have age set");
  else Log::info("User's age is " . $age);
}
Route::get('/user/{name?}', function (int $name = "John Doe") {
  Log::info("User's name is " . $name);
}

ワイルドカード

Laravelでは、パラメータ(必須/任意どちらであっても)の見え方をフィルタリングすることができます。

例えば、ユーザーIDの文字列が必要であれば、whereメソッドを使ってルートレベルで検証することができます。

whereメソッドは、パラメータの名前と、バリデーションに適用される正規表現ルールを受け取ります。デフォルトでは、一つ目のパラメータを受け取りますが、複数のパラメータがある場合、該当のパラメータ名をキー、ルールを値とする配列を渡せば解析可能です。

Route::get('/user/{age}', function (int $age) {
  //
}->where('age', '[0-9]+');
Route::get('/user/{age}', function (int $age) {
  //
}->where('[0-9]+');
Route::get('/user/{age}/{name}', function (int $age, string $name) {
  //
}->where(['age' => '[0-9]+', 'name' => '[a-z][A-z]+');

さらに発展して、アプリのすべてのルートにバリデーションを適用するには、Routeファサードでpatternメソッドを使用します。

 Route::pattern('id', '[0-9]+');

これで、idのすべてのパラメータがこの正規表現で検証されます。一度定義すると、そのパラメータ名を使ったすべてのルートに自動で適用されます。

また、すでにお気づきかと思いますが、Laravelでは/をパスのセパレータとして使用します。これをパスで使うには、whereメソッドを使って、明示的にプレースホルダの一部として許可する必要があります。

 Route::get('/find/{query}', function ($query) {
  //
})->where('query', , '.*');

ただし、唯一の欠点として、これは最後のルートセグメントでのみ有効になります。

名前付きルート

その名の通り、ルートに名前を付けることができます。特定のルートに対するURLやリダイレクトの生成に有用です。

名前付きルートの作成方法

簡単なのは、Routeファサードでnameメソッドをチェーンする方法です。なお、各ルートにはそれぞれ独自の名前を付けてください。

 Route::get('/', function () {
})->name("homepage");

ルートグループ

ルートをグループ化することで、ミドルウェアのようなルート属性をルートごとに再定義することなく、多数のルートで共有することができます。

ミドルウェア

既存の全ルートにミドルウェアを割り当てることで、グループとしてまとめることができます。まずは、groupメソッドを使用します。注意点として、ミドルウェアはグループに適用した順に実行されます。

 Route:middleware(['AuthMiddleware', 'SessionMiddleware'])->group(function () {
  Route::get('/', function() {} );
  Route::post('/upload-picture', function () {} );
});

コントローラ

グループで同じコントローラを使用するには、controllerメソッドで、グループ内のすべてのルートに共通のコントローラを定義することができます。ルートが呼び出すメソッドを指定するようにしましょう。

 Route::controller(UserController::class)->group(function () {
  Route::get('/orders/{userId}', 'getOrders');
  Route::post('/order/{id}', 'postOrder');
});

サブドメインルーティング

サブドメインは、サイトのドメインの先頭に付く情報です。これによって、オンラインストア、ブログ、プレゼンテーションなど、特定の機能を持つコンテンツを他と分離して整理することができます。

以下のルートは、サブドメインのルーティング処理に使用します。コントローラとルートで使用するために、ドメインとサブドメインを取得し、Routeファサードのdomain メソッドを活用して、ルートを1つのドメイン下にグループ化することができます。

 Route::domain('{store}.enterprise.com')->group(function() {
  Route::get('order/{id}', function (Account $account, string $id) {
    // コードをここに記述
  }
});

接頭辞(プレフィックス)

ルートグループがある場合、Laravelの補助的な機能であるRouteファサードのprefixnameを使用すると、1つずつ修正する手間を省くことができます。

prefixメソッドは、グループ内の各ルートにURIを用いて接頭辞を付けるもので、nameメソッドは、各ルート名に文字列を用いて接頭辞を付けることができます。

これによって、admin(管理者)用ルートを新しく作成する際などに、識別するための名前や接頭辞をいちいち変更する必要がなくなります。

 Route::name('admin.")->group(function() {
  Route::prefix("admin")->group(function() {
    Route::get('/get')->name('get');
    Route::put('/put')->name(put');
    Route::post('/post')->name('post');
  });
});

上記ルートのURIは、admin/getadmin/putadmin/post、名前はそれぞれadmin.getadmin.putadmin.postとなります。

ルートキャッシュ

熟練したLaravel開発者ともなると、アプリケーションを本番サーバーにデプロイする際、Laravelのルートキャッシュという選択肢も検討したいところです。

ルートキャッシュとは

ルートキャッシュを利用して、すべてのアプリケーションルートを登録するのにかかる時間を短縮することができます。

php artisan route:cacheを実行すると、Illuminate/Routing/RouteCollectionのインスタンスが生成され、エンコード後にシリアライズ処理の施された出力がbootstrap/cache.routes.phpに書き込まれます。

これによって、その他のリクエストで(存在すれば)このキャッシュファイルを読み込むようになります。つまり、ルートファイルのエントリを解析し、Illuminate/Routing/RouteCollectionIlluminate/Routing/Routeオブジェクトに変換する必要がなくなるというわけです。

ルートキャッシュの必要性

Laravelのルートキャッシュ機能を利用しなければ、アプリケーションの動作が遅くなり、ひいては売上、顧客維持、ブランドの信頼性に悪影響を与える恐れがあります。

プロジェクトの規模やルート数にもよりますが、シンプルなルートキャッシュコマンドを実行するだけで、アプリケーションを130〜500%高速化することができます。

まとめ

ルーティングは、バックエンド開発の要と言っても過言ではありません。そして、ルートの定義と管理における冗長性を確保できるため、Laravelはこの点で優れています。

Laravelを使えば、無理なくアプリケーションを開発でき、さらにアプリを高速化することも可能です。

Laravelのルーティングについて、他にも役立つ知識やヒントをご存知ですか?以下のコメント欄でぜひお聞かせください。

Coman Cosmin

3年以上の経験を持つテクニカルライター、開発者。Kinstaでの執筆以外には、核物理学施設や大学での研究を支援。技術に精通し、コミュニティで活発に活動しており、常に革新的なソリューションを考案している。