カテゴリー
Linux

Laravel7 のバリデーションで公式ドキュメントで触れられていないルールオブジェクトの便利な使い方

Laravel7 で入力値そのものと入力値を分割した配列の両方を一度にバリデーションするためのルールの書き方 – oki2a24 を改善する話です。

ルールオブジェクトまとめ

公式ドキュメントに書いてあること

  • ルールオブジェクトの使用 – カスタムバリデーションルールのメソッドを定義
  • php artisan make:rule <name> コマンドで app/Rules ディレクトリに新しいルールオブジェクトのファイルを生成できる。
  • passes($attribute, $value) メソッドでバリデーションを行う。
    • $attribute: フォームの属性名
    • $value: フォームに入力された属性値。これをバリデーションすることになる。
  • フォームリクエストクラスの rule メソッでのルールオブジェクトの使用方法は、ルールを書く場所にインスタンス化すれば良い。そのため、ルールを書く際は文字列を | で区切るのではなく、配列で定義することになる。

公式ドキュメントに書いてないこと (こちらを本ページで扱う)

  • クロージャの使用 – カスタムバリデーションルールのメソッドを定義 のみを使う場合、カスタムバリデーションルールが 2 つ以上になるとテスト時にどのクロージャがテストに該当するのかわからなくなる。そのため、クロージャを利用せずにルールオブジェクトのみを使うのも良いと思う。
  • どうやらルールオブジェクトは 1 回のバリデーションで使ったオブジェクトをそのままエラーメッセージ出力時でも使うようだ。したがって、ルールオブジェクトクラスのコンストラクタやプロパティ (クラスのメンバ変数) を使って応用を効かせることができる。
  • passes メソッドで $attribute$value 以外の値を使いたい (例えばバリデーション時の判定に使う最大数とか) 場合は、ルールオブジェクトのコンストラクタで渡す。
  • message メソッドで例えば passes メソッド内で出てきた値を使いたい場合は、passes メソッド内からプロパティに渡してやればよい。

実演1

次のクロージャを使用したカスタムバリデーションルールをルールオブジェクトへとリファクタリングしようと思います。

  • カスタムバリデーションルール: 半角カンマで分割した要素数は最大 10 個であること

前のページ にもありますけれども、該当部分はこちらです。

            'cc_emails' => [
                'nullable',
                'max:2550',
                function ($attribute, $value, $fail) {
                    $values = explode(',', $value);
                    if (count($values) > 10) {
                        $fail(trans($attribute . ' に指定できる email は最大 10 個です。'));
                    }
                },
                ... 略 ...
            ]

まず、ルールオブジェクトを作ります。

$ php artisan make:rule DelimitedMax
Rule created successfully.
$

そして、クロージャを使用したカスタムバリデーションルールの内容を、ルールオブジェクトへと書いていきます。この時のポイントです。

  • "最大 10 個" だが、この数字を使う側が決められるようにコンストラクタで渡した。
  • 文字列を分割する区切り文字は "半角カンマ" だが、この文字を使う側が決められるようにコンストラクタで渡した。ただ、 "半角カンマ" であることは多いと思うので、デフォルト値として "半角カンマ" を設定した。
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class DelimitedMax implements Rule
{
    private $maxCount;
    private $delimiter;

    /**
     * Create a new rule instance.
     *
     * @param  int  $maxCount
     * @param  string  $delimiter
     * @return void
     */
    public function __construct(int $maxCount, string $delimiter = ',')
    {
        $this->maxCount = $maxCount;
        $this->delimiter = $delimiter;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $values = explode($this->delimiter, $value);
        return count($values) <= $this->maxCount;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return ':attribute に指定できる email は最大 ' . $this->maxCount . ' 個です。';
    }
}

これを、フォームリクエストクラスで、次のようにして使いました。

            'cc_emails' => [
                'nullable',
                'max:2550',
                new DelimitedMax(10),
                ... 略 ...
            ]

そして、該当部分のユニットテストは 前のページ から次のように修正しました。

  • Validatorfailed メソッドの返すオブジェクトが、 Illuminate\Validation\ClosureValidationRul から App\Rules\DelimitedMax へと変わり、具体的になった。
  • $this->assertEquals('cc_emails に指定できる email は最大 10 個です。', $actualMessage); から $this->assertEquals('cc emails に指定できる email は最大 10 個です。', $actualMessage); へと変更した。"cc_emails" を "cc emails" に変更したのだが、なぜこうなるのかは実はよくわからない。そもそもフォームリクエストクラスのユニットテストでエラーメッセージの確認はしない方が良いのかもしれない。エラーメッセージの確認は、 (可能ならば) ルールオブジェクトのユニットテストで行う方が良いのではないだろうか?
    /**
     * @test
     * @return void
     */
    public function cc_emailsの最大email数エラーとなること(): void
    {
        $data = [
            'cc_emails' => '1@example.com,2@example.com,3@example.com,4@example.com,5@example.com,6@example.com,7@example.com,8@example.com,9@example.com,10@example.com,11@example.com',
            'name' => '名前',
            'email' => 'email@example.com',
        ];
        $request = new UserSaveRequest();
        $rules = $request->rules();

        $validator = Validator::make($data, $rules);

        $result = $validator->passes();
        $this->assertFalse($result);
        $expectedFailed = [
            'cc_emails' => ['App\Rules\DelimitedMax' => [],],
        ];
        $this->assertEquals($expectedFailed, $validator->failed());
        $actualMessage = $validator->errors()->all()[0];
        $this->assertEquals('cc emails に指定できる email は最大 10 個です。', $actualMessage);
    }

ちなみに変更箇所のみ抜き出すと

        $expectedFailed = [
            'cc_emails' => ['App\Rules\DelimitedMax' => [],],
        ];

        $this->assertEquals('cc emails に指定できる email は最大 10 個です。', $actualMessage);

です。

実演 2

ポイントは、実演 1 で全て出しました。実演 2 では次のカスタムバリデーションルールをルールオブジェクト化したコードのみを掲載します。

  • 半角カンマで分割した各要素は email のフォーマットであること

ルールオブジェクトのコード app/Rules/DelimitedEmail.php

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Validation\Concerns\ValidatesAttributes;

class DelimitedEmail implements Rule
{
    use ValidatesAttributes;

    private $invalidEmails;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $values = explode(',', $value);
        $parameters = ['rfc', 'spoof'];
        $this->invalidEmails = collect($values)
            ->filter(function ($value) use ($attribute, $parameters) {
                return !$this->validateEmail($attribute, $value, $parameters);
            })
            ->implode(', ');
        return !$this->invalidEmails;
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return ':attribute に指定した次のメールアドレスは不正です。: ' . $this->invalidEmails;
    }
}

ルールオブジェクトを使うコード

$ git diff app/Http/Requests/UserSaveRequest.php
diff --git a/app/Http/Requests/UserSaveRequest.php b/app/Http/Requests/UserSaveRequest.php
index 003bd7c..c95824e 100644
--- a/app/Http/Requests/UserSaveRequest.php
+++ b/app/Http/Requests/UserSaveRequest.php
@@ -2,16 +2,14 @@

 namespace App\Http\Requests;

+use App\Rules\DelimitedEmail;
 use App\Rules\DelimitedMax;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Support\Facades\Auth;
-use Illuminate\Validation\Concerns\ValidatesAttributes;
 use Illuminate\Validation\Rule;

 class UserSaveRequest extends FormRequest
 {
-    use ValidatesAttributes;
-
     /**
      * Determine if the user is authorized to make this request.
      *
@@ -41,18 +39,7 @@ class UserSaveRequest extends FormRequest
                 'nullable',
                 'max:2550',
                 new DelimitedMax(10),
-                function ($attribute, $value, $fail) {
-                    $values = explode(',', $value);
-                    $parameters = ['rfc', 'spoof'];
-                    $invalidEmails = collect($values)
-                        ->filter(function ($value) use ($attribute, $parameters) {
-                            return !$this->validateEmail($attribute, $value, $parameters);
-                        })
-                        ->implode(', ');
-                    if ($invalidEmails) {
-                        $fail($attribute . ' に指定した次のメールアドレスは不正です。: ' . $invalidEmails);
-                    }
-                },
+                new DelimitedEmail(),
             ],
             'zip' => 'nullable|regex:/^\d{7}$/',
         ];

$

フォームリクエストクラスでルールオブジェクトを使用している箇所のユニットテストのコード

$ git diff tests/Feature/UserSaveRequestTest.php
diff --git a/tests/Feature/UserSaveRequestTest.php b/tests/Feature/UserSaveRequestTest.php
index 3c41c29..96568c9 100644
--- a/tests/Feature/UserSaveRequestTest.php
+++ b/tests/Feature/UserSaveRequestTest.php
@@ -84,13 +84,13 @@ class UserSaveRequestTest extends TestCase
         $result = $validator->passes();
         $this->assertFalse($result);
         $expectedFailed = [
-            'cc_emails' => ['Illuminate\Validation\ClosureValidationRule' => [],],
+            'cc_emails' => ['App\Rules\DelimitedEmail' => [],],
         ];
         //dd($validator->failed());
         $this->assertEquals($expectedFailed, $validator->failed());
         //dd($validator->errors());
         $actualMessage = $validator->errors()->all()[0];
-        $this->assertEquals('cc_emails に指定した次のメールアドレスは不正です。: 1example.com, 3atexample.com', $actualMessage);
+        $this->assertEquals('cc emails に指定した次のメールアドレスは不正です。: 1example.com, 3atexample.com', $actualMessage);
     }



$

おわりに

前回 の課題を解決できて、満足です。

以上です。

コメントを残す