九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、今も進化を続けるLaravel
ですが、今回は少し視点を変えて直接目に見える部分ではなく、より根本的な「ルーティング(Routing)」についてまとめてみたいと思います。
ルーティングとは、例えば「http://*****.com/top」というURLでページを表示したい場合、ルーティング・ファイル(基本的には、/routes/web.php)で色々な条件をつけて指定ができるというものです。
ルーティングの便利な書き方を知っていると作業がより効率化できると思うのでぜひ活用してくださいね。
開発環境: 〜 Laravel 7.x
【追記:2020.3.12】Laravel 7.xの新しい機能を2つ追加しました。
目次
はじめに
まず前提として、ルーティングは「/routes/web.php」に記述します。条件によってその他、api.phpなどに変更してください。
また、今回はページを表示するルーティングに特化しているのでリダイレクトなどは割愛しています。リダイレクトについては、Laravelリダイレクト実例をご覧ください。
HTTPメソッドを指定するルーティング
Laravelでは以下6つのHTTPメソッドをルーティングに指定することができます。
- GET ・・・ (データを取得する基本的なもの)
- POST ・・・ (データの追加に使用)
- PUT ・・・ (データの更新に使用)
- PATCH ・・・ (ほぼPUTと同じですが、ごく一部を更新)
- DELETE ・・・ (データの削除に使用)
- OPTIONS ・・・ (使えるメソッド一覧を表示)
具体的には以下のようになります。「http://*****.com/top」というURLに各種HTTPメソッドを使ってアクセスすると「OKです!」と表示されます。
Route::get('top', function(){ return 'OKです!'; });
Route::post('top', function(){ return 'OKです!'; });
Route::put('top', function(){ return 'OKです!'; });
Route::patch('top', function(){ return 'OKです!'; });
Route::delete('top', function(){ return 'OKです!'; });
Route::options('top', function(){ return 'OKです!'; });
※が、正直なところ個人的には「patch」と「options」は使ったことがありません。
また、複数のHTTPメソッドを指定したい場合は以下のように「match」で許可したいHTTPメソッドを指定してください。
Route::match(['get', 'post', 'put'], 'top', function(){ return 'OKです!'; });
そして、逆に全メソッドでアクセスを許可したい場合は「any」です。
Route::any('top', function(){ return 'OKです!'; });
※この場合は、GETであろうがPOSTであろうが、どんなアクセス方法でも「http://*****.com/top」にアクセスできるようになります。ただし、セキュリティ上必要のない窓を開けておくことはあまり褒められたことではないので、特別な場合以外は使わない方がいいかもしれません。
注意!
ちなみに、「PUT」「PATCH」「DELETE」「OPTIONS」の場合は注意が必要で、これらは実際のところ「POST」送信となっています。というのも、HTMLの<form></form>タグがこれらの形式をサポートしていないからなんですね。
では、Laravelではどうやって送信形式を判断しているかというと、POST送信内に「_method」という専用データを追加して判断しています。
なので、もしフォームで「PUT」「PATCH」「DELETE」「OPTIONS」でデータ送信をしたい場合は、以下のようにhidden入力を使ってメソッド名を送信する必要があります。
<form action="/top" method="POST"> <input type="hidden" name="_method" value="PUT"> </form>
もしくは、Baldeのディレクティブを使えばよりシンプルに実行できます。
<form action="/top" method="POST"> @method('put') </form>
また、Laravelは初期状態でCSRF(クロスサイトリクエストフォージェリ)対策が施されているので、以下のようにしてトークンを追加することも忘れないでください。
<form action="/top" method="POST"> @csrf </form>
※ちなみにCSRF攻撃というのは、簡単に言うと「ログインしたまま他のサイト行ったら、そのログイン状態を悪用するよ😁」攻撃です。
※また、(おすすめしませんが)もしCSRF
を無効にしたい場合は、「\App\Http\Middleware\VerifyCsrfToken::class」をコメントアウトすればOKです。
/app/Http/Kernel.php
'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, // 👈ここ \Illuminate\Routing\Middleware\SubstituteBindings::class, ],
もしくは、個別に無効にする場合はミドルウェアVerifyCsrfToken
の$except
で指定します。
/app/Http/Middleware/VerifyCsrfToken.php
<?php namespace App\Http\Middleware; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** * The URIs that should be excluded from CSRF verification. * * @var array */ protected $except = [ // 👈ここ 'stripe/*', ]; }
パラメータつきルーティング
例えば、「http://*****.com/item/100」というURLにアクセスしたとき、URL内にある「100」を取得したい場合です。
実際の例はこうなります。
Route::get('item/{id}', function($id){ echo 'IDは、'. $id .'です。'; });
Laravelのルーティングでは、中括弧{}で囲まれた部分は可変のパラメータとして処理され、自動的にfunction()内の引数にしてくれます。
つまり、この例で「http://*****.com/item/100」にアクセスすると、
と表示されることになります。
ただ、この場合だと必ずIDを指定しないといけないため、もし「http://*****.com/item/」にアクセスするとエラーが発生することになります。
そこで、使えるのが省略可能な記述です。
Route::get('item/{id?}', function($id = -1){ echo 'IDは、'. $id .'です。'; });
さっきと似てますけど、中括弧の最後に「?」クエスチョンマークがついていて、さらにfunction()内の引数にはデフォルト値を追加しています。
これで、エラーは発生しなくなり、アクセスすると、
と、表示されるようになります。
では次に、パラメータに制限をかけたい場合を考えてみましょう。
さっきの例だと、ID番号をパラメータとして取得しました。
でも、例えばJavaScriptなどで変数が定義されていない場合「http://*****.com/item/undefined」だとか、もしくは初期値がNullの場合は「http://*****.com/item/null」という風なURLにアクセスすることになります。
これでは、データベースにアクセスした場合にエラーを引き起こしかねません。
そこで、このような場合に使えるのが「where()」です。
実際の例を見てみましょう。
Route::get('item/{id}', function($id){ echo 'IDは、'. $id .'です。'; })->where('id', '[0-9]+');
where()の第2引数は正規表現になっていて、この例では「最低でも1文字以上の数字」が入ってくる必要があります。そのため、「http://*****.com/item/undefined」も「http://*****.com/item/null」もアクセスできません。
もちろん正規表現を変更すれば色々なことができて、例えば「ある文字列だけで許可」したい場合は、以下のようにすればいいでしょう。
Route::get('item/{category}/list', function($category){ echo 'カテゴリは、'. $category .'です。'; })->where('category', '(dvd|game|audio)');
この例では、
- http://*****.com/item/dvd/list
- http://*****.com/item/game/list
- http://*****.com/item/audio/list
の3パターンだけアクセスできます。
モデルを自動で呼び出すバインディング
バインディングは、前項目のパラメータつきルーティングに似てますが、自動でデータベースから該当するデータを見つけてきて、さらにインスタンス化してくれる便利機能です。
例えば、「http://*****.com/user/100」にアクセスすれば、データベースから自動でIDが100のデータを探し出してきて、function()内の変数にそのデータのインスタンスを用意してくれます。
では、Userモデルを使った実際の例で見てみましょう。
Route::get('user/{user}', function(\App\User $user){ echo '名前は、'. $user->name .'です。'; });
まず中括弧{}の中身は、モデルの名前(小文字、単数形)。ここでは「User」モデルを呼び出すので {user} となります。
そして、function()内は、呼び出したいモデルをタイプヒンティング(型の宣言)で指定しましょう。
これで取得できる$userは、該当するIDのインスタンスなので、いちいち以下のようにデータベースから呼び出してくるコードを書く必要はありません。
$user = \App\User::find($id);
とても便利ですね。
コントローラーと連携するルーティング
ここまでのコードは全てweb.phpのルーティング・ファイル内で完結してましたが、実際には全てのコードをこのファイルに記述するのは保守作業的にも得策ではありません。
そこでよく利用されるのが、コントローラーと連携した書き方です。
Route::get('route', 'HomeController@route');
この例は、「http://*****.com/route」にアクセスすると「/app/Http/Controllers/HomeController.php」の中にある「route()」というメソッドが呼ばれるものです。
つまり、ルーティングではコントローラーとアクション(メソッド)を指定しておき、コードはコントローラーの中に記述する。つまり、特定の役割で分割するということですね。
ちなみにもし階層が下のフォルダ内に目的のコントローラーがある場合は、以下のようにネームスペース付きで指定してあげるとOKです。
Route::get('route', 'User\HomeController@route');
もちろん、コントローラーを呼び出す形でもパラメータつきルーティングやバインディングも同様に利用できます!
なお、独自のバインディングを作成することもできて、例えばIDとは別に商品コードなどを使ってデータを探したい場合です。
この場合、まず「/app/Providers/AppServiceProvider.php」のboot()内に以下のような独自ルールを追加してあげましょう。
\Route::bind('item_code', function ($value) { return \App\Item::where('product_code', $value)->first() ?? abort(404); });
こうすることで、以下のように {item_code} を使ってバインディングを実現することができます。
Route::get('item/{item_code}', function(\App\Item $item){ echo $item->name; });
グループ化したルーティング
例えば、ユーザーの種類が「管理者」と「会員」の2つだとして、それぞれに専用のURLを作る場合を考えてみましょう。
この場合おそらく、
- http://*****.com/user/top
- http://*****.com/user/settings
- http://*****.com/user/logout
や、
- http://*****.com/admin/main
- http://*****.com/admin/user/create
- http://*****.com/admin/settings
などのURLが必要になってくると思います。
もちろん、ひとつひとつ
Route::get('user/top', function(){}); Route::get('user/settings', function(){}); Route::get('user/logout', function(){});
と書いても問題ありませんが、できれば同じ目的のページはグループ化しておきたいものです。
そこで、活躍するのがgroup()です。
グループ化の種類は
- middleware ・・・ ミドルウェアでグループ化し適用する
- namespace ・・・ ネームスペースでグループ化
- domain ・・・ サブドメインをグループ化
- prefix ・・・ URLが始まる文字列でグループ化
- name ・・・ ルーティング名でグループ化
の5つ。
実際の例を見てみましょう。
middleware
Route::middleware('auth')->group(function(){ Route::get('user/top', 'HomeController@route'); });
中に記述されたルーティングにミドルウェアを適用する書き方です。この場合の「auth」は、「/app/Http/Kernel.php」内で設定されているものです。
namespace
Route::namespace('User')->group(function(){ Route::get('user/top', 'HomeController@index'); });
ネームスペースに「User」を指定しているので、以下のようなネームスペースを持ったコントローラーが呼び出されることになります。
<?php namespace App\Http\Controllers\User;
つまり、先ほどの例だとルーティングに「HomeController@index」と指定されていますが、実際にはネームスペースでグループ化しているので、「User\HomeController@index」が呼び出されます。
domain
サブドメインでルーティングをグループ化します。
実際の例を見てみましょう。
Route::domain('{account}.laravel.test')->group(function(){ Route::get('user/top', function($account){ echo $account; }); });
この場合、「http://laravel.test/user/top」にアクセスしてもエラーが発生しますが、例えば「http://www.laravel.test/user/top」とサブドメイン付きならうまくいきます。
さらに、$accountには「www」などの該当するサブドメインが格納されているのでこれらを使って分岐をすることもできるでしょう。
prefix
個人的によく使うのがprefix機能です。
ルーティングの数が増えてくると少しでも記述コードを少なくしたいところですので、重宝します。
例えば、以下のような書き方になります。
Route::prefix('user')->group(function(){ Route::get('top', 'HomeController@route'); Route::get('settings', function(){}); Route::get('logout', function(){}); });
この場合、有効になるURLは
- http://*****.com/user/top
- http://*****.com/user/settings
- http://*****.com/user/logout
となります。つまり、URLの最初の文字列を一括で指定できるわけですね。
もちろん、
Route::prefix('user/xxx')->group(function(){ // });
という風に多階層で指定することもできます。
name
ルーティングの名前を一括で指定する書き方です。
ルーティングにはname()で特定の名前をつけておくと、以下のように楽にURLを取得できる機能があります。
echo route('user.top'); // URLが取得できる
もし、この場合記述すべきコードは以下になります。
Route::name('user.')->group(function(){ Route::get('user/top', function(){ echo 'OKです!'; })->name('top'); });
つまり、prefix()などと同様に「user.」を一括で指定しておいて、中身は「top」だけを名前付けするという書き方ですね。たくさんの名前付けをする場合には有効かと思います。
複数のグループ化を一括でやりたい
複数のグループ化を指定したい場合は以下のように「->」アローで繋げば可能です。
Route::prefix('user')->middleware('auth')->group(function(){ // });
もしくは、Laravel5.6の公式ページには書かれてませんが、以下のようにgroup()内に配列で指定してやることで複数タイプのグループ化を指定することもできます。
Route::group(['prefix' => 'user', 'middleware' => 'auth'], function(){ // });
RESTfulなルーティング
RESTful化したルーティングを簡単に実現したい場合はresource()を使いましょう。
以下のようにプレフィックスとコントローラーを指定するだけで、次のURLが有効になります。
Route::resource('users', 'UserController');
- (GET) http://*****.com/users ・・・ 一覧表示。index()
- (GET) http://*****.com/users/create ・・・ 追加ページ。create()
- (POST) http://*****.com/users ・・・ 追加。store()
- (GET) http://*****.com/users/1 ・・・ 該当データ表示。show()
- (GET) http://*****.com/users/1/edit ・・・ 更新ページ。edit()
- (PUT) http://*****.com/users/1 ・・・ 更新。update()
- (DELETE) http://*****.com/users/1 ・・・ 削除。destroy()
もし、特定のページだけ有効にしたい場合はonly()、特定のページを除外したい場合はexcept()を使って指定しましょう。
Route::resource('user', 'UserController')->only([ 'index', 'show' // このメソッドは有効 ]);
Route::resource('user', 'UserController')->except([ 'store', 'update' // このメソッド以外が有効 ]);
ちなみに、RESTful用のコントローラーを作成するには、以下のようにartisanコマンドに「–resouce」をつければ一気にindex()以下のメソッドが書かれたコントローラーを自動作成することができます。
php artisan make:controller ItemController --resource
ID以外でモデル・バインディングする
Laravel 7.xの新ルート機能 > ID以外でモデル・バインディングする方法をご覧ください。
Laravel 7.x 以降
データの整合性を考慮したバインディング
Laravel 7.xの新ルート機能 >データの整合性を考慮したバインディングをご覧ください。
Laravel 7.x 以降
おわりに
ということで今回はLaravelのルーティングについての実例をまとめてみました。
常に新しい書き方が追加になっているので、もし知らない方法を発見してもらえたら嬉しいです。
ぜひ作業の効率化を進めてくださいね。
ではでは〜。