初めに
友人がLaravelのプロジェクトに参画することが決まり、個人学習のレビューを依頼されているのですがルーティングのレビューが好評だったので記事にします。
Laravelが推奨する構成
LaravelではRESTfulアーキテクチャ、またリソース指向アーキテクチャ(ROA)の設計思想を取り入れています。
公式ではそのRESTやROAの考えに沿ったコントローラーアクションの命名やHTTPメソッド、URIが推奨されており、以下のようになっています。
| アクション名 | HTTPメソッド | URL | 説明 |
|---|---|---|---|
| index | GET | /tasks | 一覧の表示 |
| create | GET | /tasks/create | 新規作成フォームの表示 |
| store | POST | /tasks | 新規作成 |
| show | GET | /tasks/{task} | 詳細の表示 |
| edit | GET | /tasks/{task}/edit | 編集フォームの表示 |
| update | PUT/PATCH | /tasks/{task} | 編集 |
| destroy | DELETE | /tasks/{task} | 削除 |
基本的なルートの書き方
Route::get('/tasks/create', [TaskController::class, 'create'])->name('tasks.create');
このような書き方が基本的な書き方です。
ただ、この書き方では問題があります。
Laravelが推奨しているアクション全てをこのバージョンで作成しようとすると以下のようになります。
Route::get('/tasks', [TaskController::class, 'index'])->name('tasks.index');
Route::get('/tasks/create', [TaskController::class, 'create'])->name('tasks.create');
Route::post('/tasks', [TaskController::class, 'store'])->name('tasks.store');
Route::get('/tasks/{task}', [TaskController::class, 'show'])->name('tasks.show');
Route::get('/tasks/{task}/edit', [TaskController::class, 'edit'])->name('tasks.edit');
Route::patch('/tasks/{task}', [TaskController::class, 'update'])->name('tasks.update');
Route::delete('/tasks/{task}', [TaskController::class, 'destroy'])->name('tasks.destroy');
問題点は、全てにおいてTaskController::classと書いてたり、最初のパスは'/tasks'であったり、名前付きルートのプレフィックスがtasks.であったりと繰り返していることです。
解決策
重複していたそれぞれを省略、グループ化するための3つのメソッドがあります。それらを紹介します。
1. controllerメソッド
コントローラーのグループ化
Route::controller(TaskController::class)->group(function () {
// ここにルートを定義
Route::get('/tasks', 'index')->name('tasks.index');
Route::get('/tasks/create', 'create')->name('tasks.create');
Route::post('/tasks', 'store')->name('tasks.store');
Route::get('/tasks/{task}', 'show')->name('tasks.show');
Route::get('/tasks/{task}/edit', 'edit')->name('tasks.edit');
Route::patch('/tasks/{task}', 'update')->name('tasks.update');
Route::delete('/tasks/{task}', 'destroy')->name('tasks.destroy');
});
こうすることで、全てのルート内に配列で[TaskController::class, 'create']というような形でコントローラーとメソッド名を指定してたのが、メソッド名だけの指定で収まるようにできます。
2. prefixメソッド
URIのグループ化
Route::prefix('/tasks')->group(function () {
// ここにルートを定義
Route::get('/', [TaskController::class, 'index'])->name('tasks.index');
Route::get('/create', [TaskController::class, 'create'])->name('tasks.create');
Route::post('/', [TaskController::class, 'store'])->name('tasks.store');
Route::get('/{task}', [TaskController::class, 'show'])->name('tasks.show');
Route::get('/{task}/edit', [TaskController::class, 'edit'])->name('tasks.edit');
Route::patch('/{task}', [TaskController::class, 'update'])->name('tasks.update');
Route::delete('/{task}', [TaskController::class, 'destroy'])->name('tasks.destroy');
});
こう書くことでprefixメソッドがあるgroup内にある全てのルートのURIには/tasks/がつくようになります。なおかつ各ルートで/tasksというURIを指定しなくてよくなります。
3. asメソッド
名前付きルートのグループ化
Route::as('tasks.')->group(function () {
// ここにルートを定義
Route::get('/tasks', [TaskController::class, 'index'])->name('index');
Route::get('/tasks/create', [TaskController::class, 'create'])->name('create');
Route::post('/tasks', [TaskController::class, 'store'])->name('store');
Route::get('/tasks/{task}', [TaskController::class, 'show'])->name('show');
Route::get('/tasks/{task}/edit', [TaskController::class, 'edit'])->name('edit');
Route::patch('/tasks/{task}', [TaskController::class, 'update'])->name('update');
Route::delete('/tasks/{task}', [TaskController::class, 'destroy'])->name('destroy');
});
このasメソッドを入れることで、名前付きルートもグループ化できてグループ内の名前付きルート全てにはas()内に書いたtasks.が適用されます。
その為、name()内には各アクション名の指定だけで済ませることができます。
まとめ
前提としては、今まで書いた4つのパターンのルート定義は動作的にどれも同じです。
保守性の観点から、こうした方がいいよねというだけです。
個人的によく実務で使うパターンとしてはcontroller(), prefix(), as()をメソッドチェーンでグループ化するパターンです。
例として以下のような形になります。
Route::controller(TaskController::class)->prefix('/tasks')->as('tasks.')->group(function () {
Route::get('/', 'index')->name('index');
Route::get('/create', 'create')->name('create');
Route::post('/', 'store')->name('store');
Route::get('/{task}', 'show')->name('show');
Route::get('/{task}/edit', 'edit')->name('edit');
Route::patch('/{task}', 'update')->name('update');
Route::delete('/{task}', 'destroy')->name('destroy');
});
このように記述することで、それぞれのルートに対して個別でURIなどの設定が可能な柔軟性を持たせながら、コードをまとめることができます。
ただ実務ではプロジェクトリーダーの考えに従ってください。
参考になればな〜と思います。
補足
また、上記で紹介していないパターンが2つあります。
それらについても触れていきます。
1. resourceメソッド
Route::resource('tasks', TaskController::class);
第一引数がリソース名となり、URIのベースパスと名前付きルートの両方に使われ、第二引数がコントローラーになります。
この処理だけで第二引数に入れたコントローラーのLaravelが推奨するindex,create,store,show,edit,update,destroy全てのルートが自動的に定義されます。
これは大規模システムにおいて、ルーティングファイルの肥大化を防ぐために効果的です。
ただいくつかの制約があるので紹介します。
問題点1. カスタムルートと相性が悪い
Laravelが推奨している構成では表現できない処理などがある場合、独自でルートを定義する必要があります。それはこれまで紹介してきたパターン全てでも同じです。
その場合カスタムとして独自にルートを作って対応します。mypageみたいな。。。
Route::resource('users', UserController::class);
// カスタムルート
Route::get('/users/mypage', [UserController::class, 'myPage'])->name('users.mypage');
このような形でresourceの後に、users/mypageのカスタムルートを置くと予期しない挙動になります。
というのもshowアクションがusers/{user}というURIになっており、users/mypageのmypageの部分が{user}の部分に入ってしまうことでshowアクションへ流れます。showアクションではルートモデルバインディングが失敗するため例外も発生します。
カスタムルートをresourceメソッドよりも上の位置で記述すると解決できますが、一種の制約です。
// 上の方に記述する
Route::get('/users/mypage', [UserController::class, 'myPage'])->name('users.mypage');
Route::resource('users', UserController::class);
問題点2. URIの編集が柔軟にできない
resourceメソッドで作成されるURIはLaravel推奨のURIが自動的に定義されます。
そこで問題なのが、セキュリティの観点やSEOの観点からURIを変更しなければならない状況になった場合対応できません。
parametersメソッドやnamesメソッド、resourceVerbsメソッド等である程度変更を加えることも可能ですが、RESTful構造からの大幅な変更は難しいというのが現状です。
問題点3. 全体の把握ができない
resourceメソッドの後に->except(['show'])というように記述することで特定のアクションのルート定義を除外することができます。
また、必要なものが少数の場合は->only(['index'])のように生成するものを指定もできます。
resourceメソッドを使用するのであれば不必要なルートも生成されるので、実務で使うのであればexceptやonlyで除外されることが考えられます。
その問題点が全体の把握を難しくさせるという点です。
Route::resource('users', UserController::class)->except(['show']);
Route::resource('tasks', TaskController::class)->except(['show', 'edit', 'update']);
Route::resource('genre', GenreController::class)->only(['index', 'show']);
このようなルート定義がされていた場合、合計で何個ルートがあるのかというのが一瞬で掴めません。
サンプルの合計は12ルートです。たった3行でこれほど全体の把握を難しくしてしまいます。
結論としてresourceメソッドはルーティングの量を減らす際に効果的な側面があるものの一定の制約も生まれてしまいます。
これに関してはプロジェクトに合わせてください。
2. groupメソッドのオプション
groupメソッドの第一引数に配列でasやprefixを含めたオプションを指定することができます。
Route::controller(TaskController::class)->group(['prefix' => '/tasks', 'as' => 'tasks.'], function () {
Route::get('/', 'index')->name('index');
Route::get('/create', 'create')->name('create');
Route::post('/', 'store')->name('store');
Route::get('/{task}', 'show')->name('show');
Route::get('/{task}/edit', 'edit')->name('edit');
Route::patch('/{task}', 'update')->name('update');
Route::delete('/{task}', 'destroy')->name('destroy');
});
問題ないと言えばないのですが、これは古い書き方で公式も推奨していません。
またメソッドではなく配列のキーになることでタイポに気付きにくいデメリットがあります。