1つのコントローラの中で、バリデーションに使用するルールをメソッド間で共有したい、とは誰しも思うこと。私もLaravelを習得して以来、管理性が高くしかもすっきりとした方法を求め続けていました。前回で紹介したカスタムルールの登場で、これは使えるんじゃない、という方法を見つけましたので、ここにて披露です。

欲しいもの

いくつか条件あります。

1.コントローラー内でルールを指定したい

これはどちらかというと実用性より好みの問題かもしれませんが、Form Requestのように違うファイルとして定義するのではなく、コントローラー内で定義することにより、あちこちのファイルを見に行くのではなくコントローラーのファイル内で使用されるルールを見たいためです。たいていのコントローラーは、そうたいした数のルールがあるわけでもないので、Form Requestを使用する必要もないです。

2.コントローラーのメソッド内でルールを指定したくない

これは管理性大いにあります。以下のようにそれぞれのメソッド内で重複してルールを定義するのは間違いが起こるもとです。もちろん、これが今回のルールの共有の大きな要因でもあります。

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

use App\User;

class UserController extends Controller
{
...
    public function store(Request $request)
    {
        $request->validate([
            'name'        => 'required',
            'name_kana'   => 'required|kana',
            'mailcode'    => 'required|mailcode',
            'prefecture'  => 'required',
            'city'        => 'required',
            'address1'    => 'required',
            'phone'       => 'required|phone_with_dash',
            'email'       => 'required|email|unique:users',
            'password'    => 'required|min:8|max:20|confirmed',
        ]);
        ...
    }
...
    public function update(Request $request, User $user)
    {
        $request->validate([
            'name'        => 'required',
            'name_kana'   => 'required|kana',
            'mailcode'    => 'required|mailcode',
            'prefecture'  => 'required',
            'city'        => 'required',
            'address1'    => 'required',
            'phone'       => 'required|phone_with_dash',
          'email'       => [
                'required',
                'email',
                Rule::unique('users')->ignore($user->id), //現在のレコード以外のレコードで重複がないかチェック
             ],
        ]);
        ...
    }
...
}

解決方法 その1

上の条件にかなうものとしては、コントローラー内の一箇所にルールーを指定することになります。まず、考えたのは、インスタンス変数を使用すること、

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

use App\User;

class UserController extends Controller
{
    protected  $rules = [
        'shared' => [ // kana, mailcode, phone_with_dashはカスタムバリデーター
            'name'        => 'required',
            'name_kana'   => 'required|kana',
            'mailcode'    => 'required|mailcode',
            'prefecture'  => 'required',
            'city'        => 'required',
            'address1'    => 'required',
            'phone'       => 'required|phone_with_dash',
        ],

        'store' => [
            'email'       => 'required|email|unique:users',
            'password'    => 'required|min:8|max:20|confirmed',
       ],

       'update' => [
        'email'       => [
                'required',
                'email',
                Rule::unique('users')->ignore($user->id), //現在のレコード以外のレコードで重複がないかチェック
             ],
       ]
    ];
...
    public function store(Request $request)
    {
        $request->validate($this->rules['shared'] + $this->rules['store']);
        ...
    }
...
    public function update(Request $request, User $user)
    {
        $request->validate($this->rules['shared'] + $this->rules['update']);
        ...
    }
...
}

いい感じ?

しかし問題あります。

Rule::unique('users')->ignore($user->id)

これ、$rulesの変数の宣言時に、このような初期化はできないし、$userを渡すこともできません。update()のメソッド内でその初期化をする必要あります。

...
    public function update(Request $request, User $user)
    {
        $thus->rules['update']['email'][] = Rule::unique('users')->ignore($user->id);
        $request->validate($this->rules['shared'] + $this->rules['update']);
        ...
    }
...

せっかくコントローラーの先頭で一箇所で定義しようと思ったのに、これは良くないですね。

欲しかった解決方法

初期化が問題なら、クラスのインスタンス変数の代わりに、Form Requestにあるように、rules()という名前でコントローラーの関数にしてはどうでしょう、ということで以下になりました。

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

use App\User;

class UserController extends Controller
{
    public function rule($action, User $user = null)
   {
        // 共有するルール kana, mailcode, phone_with_dashはカスタムバリデーター
        $rules = [
            'name'        => 'required',
            'name_kana'   => 'required|kana',
            'mailcode'    => 'required|mailcode',
            'prefecture'  => 'required',
            'city'        => 'required',
            'address1'    => 'required',
            'phone'       => 'required|phone_with_dash',
        ];

       // 共有しないルール
       switch ($action) {
            case 'store': // レコード作成のためのルールを追加
                $rules['password']      = 'required|min:8|max:20|confirmed';
                $rules['email']         = 'required|email|unique:users';
                break;

            case 'update': // レコード編集のためのルールを追加
                $rules['email'] = [
                    'required',
                    'email',
                    Rule::unique('users')->ignore($user->id), //現在のレコード以外のレコードで重複がないかチェック
                ];
                break;
        }

        return [
            // rules
            $rules,
            // messages
            [
                'password.min' => '8から20文字長でお願いします',
                'password.max' => '8から20文字長でお願いします'
            ]
            // attributes
        ];
    }
...
    public function store(Request $request)
    {
        $request->validate(...$this->rules('store'));
        ...
    }
...
    public function update(Request $request, User $user)
    {
        $request->validate(...$this->rules('update', $user));
        ...
    }
...
}

rules()の関数には、2つの引数があります。最初の$actionはメソッド名で、それにより返す連想配列$rulesを特定し、次の$userは編集で必要とされる値(ここでは重複のチェックのためにレコードid)を取得のためが目的です。

関数の引数は状況に応じて自由に変えてください。以下は、前回に紹介したカスタムルールの例ですが、同じ入力画面からの違う項目のdate_startの値を取得しています。

   public function rules($action, Request $request)
    {
        // store
        $rules = [
            'date_start'    => 'date',
            'date_end'      => [
                'date',
                'after_or_equal:date_start',
                new RestrictPeriodRule($request->date_start), 6)
            ],
        ];

        return [$rules];
    }

最後に、rules()が返す配列は、$request->validate()の引数にマッチする、rulesmessagesattributesとなりますが、必要に応じてrulesだけでもOKです。このように不特定数の引数があるときは、コールするときに、 $request->validate(...$this->rules('store'))のようにphp5.6より登場した3つのドットを使って渡します。

By khino