Laravelのフォームリクエストのジレンマ

Laravelにはフォームリクエストという便利な機能がある。コントローラに適切なフォームリクエストをインジェクションさせると、リクエストがフォームリクエスト内のルールでバリデーションされた上でインジェクションされる。

<?php

namespace App\Http\Requests;

class UserRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules()
    {
        return [
            'name'  => ['string'],
            'email' => ['email'],
        ];
    }
}
<?php

namespace App\Http\Controllers;

class UserController extends Controller
{
    public function store(\App\Http\Requests\UserRequest $request)
    {
        dd($request->validated()); # バリデーション済みのデータが取得できる
    }
}

上記のコードは、コントローラ側で以下のように実装しても同等だ。

<?php

namespace App\Http\Controllers;

class UserControllerWithValidation extends Controller
{
    public function store(\Illuminate\Http\Request $request)
    {
        dd($request->validate([
            'name'  => ['string'],
            'email' => ['email'],
        ]));
    }
}

フォームリクエストを使わない方がコードは短くなる。ではなぜフォームリクエストを使うのか? それはフォームリクエストを使うことでコントローラとバリデーションの双方向の依存を、コントローラからバリデーションだけの一方向の依存にできるからだ。

ではすべてのバリデーションをフォームリクエストで行おう、となりそうだが、じつはフォームリクエストは以下のようなルールで破綻する。

<?php

namespace App\Http\Controllers;

class UserControllerWithValidation extends Controller
{
    public function update(\Illuminate\Http\Request $request, \App\User $user)
    {
        dd($request->validate([
            'name'  => ['string'],
            'email' => ['email', \Illumination\Validation\Rule::unique('users')->ignore($user->id)],
        ]));
    }
}

バリデーションルールがルートと関連したモデルに依存してしまっている。このような、よくある、ちょっと複雑な例にフォームリクエストを使うことは意味がない。

フォームリクエストの利点はコントローラに依存しなくなることだが、ユーザモデルのインスタンスを取得するためにはコントローラに依存するか、もっと酷いことにルートに直接依存することになる。

フォームリクエストは Illuminate\Http\Requests を拡張していて、同様に routeメソッド経由でルートにアクセスでき、そこからユーザモデルのインスタンスを取得できるが、コントローラ側でDIで間接的にルートから取得する方がまだマシだ。

ということで、フォームリクエストはコントローラへの依存を切り離せるので便利だが、依存を切り離すとコントローラに依存しないと用意できない複雑なルールに対応できなくなるので、じっさいにはほとんど使い所はない。

<?php

namespace App\Http\Requests;

class UserRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules()
    {
        $user = $this->route()->parameter('user'); # 持って来ること自体はできるのだが

        return [
            'name'  => ['string'],
            'email' => ['email', \Illumination\Validation\Rule::unique('users')->ignore($user->id)],
        ];
    }
}

バリデーションルールは、素直にコントローラ内なりに書くのがよさそうだ。