Laravelの必須チェック系ルールオブジェクト

この記事は ミライトデザイン Advent Calendar 2021 20日目の記事です。

Laravelでカスタムバリデーションを行うときに使うルールオブジェクトについて書いてみます。

### TL;DR

`ImplicitRule`使おう

```php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ImplicitRule;

class PmRequired implements ImplicitRule
{
    /**
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return now()->hour < 12 || $value !== null;
    }

    /**
     * @return string
     */
    public function message()
    {
        return ':attributeは午前中は必須です。';
    }
}
```

### バージョン

- Laravel 8.76.2

### Laravelのルールオブジェクト

Laravelには標準で様々なバリデーションチェックの種類がありますが、独自のバリデーションルールを作成したいことがしばしばあると思います。
[公式サイト](https://laravel.com/docs/8.x/validation#custom-validation-rules)によると、

- ルールオブジェクトを使う
- クロージャを使う
- Validatorファサードのextendメソッドを使う

という方式が紹介されていますが、今回はルールオブジェクトについてのお話をします。

### ルールオブジェクトの使い方

ルールオブジェクトの使い方としては、まず`make:rule`というArtisanコマンドを実行し、新規のルールオブジェクトを生成します。
今回は、奇数かどうかのチェックを行うための`OddNumber`というルールオブジェクトを作ってみます。

```
php artisan make:rule OddNumber
```

上記のコマンドを実行すると、`app/rules`下に`OddNumber.php`が作成されます。

```php:OddNumber.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class OddNumber implements Rule
{
    /**
     * 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)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

```

出来上がったRuleクラスを修正することで独自のバリデーションルールを作成することができます。
`pass`メソッドで`value`の値をチェックして、true / falseを返しましょう。

例えば奇数かどうかチェックする場合は下記のようにすればOKです。
（行数削減のために、phpdocとコンストラクタを除外しています。）

```php:OddNumber.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class OddNumber implements Rule
{
    public function passes($attribute, $value)
    {
        return ctype_digit($value) && $value % 2 === 1;
    }

    public function message()
    {
        return ':attributeは奇数ではありません。';
    }
}
```

Ruleクラスを作成したら、ValidatorやFormRequestに設定することで、独自のバリデーションルールを使用できます。

```php
use App\Rules\OddNumber;

$request->validate([
    'name' => ['required', 'string', new OddNumber],
]);
```

マニュアルや他記事でも詳しく紹介されているので、詳しく知りたい方はぜひ調べてみてください。

[^]:https://laravel.com/docs/8.x/validation#custom-validation-rules

### ルールオブジェクトは空値をチェックできないらしい

ここからが本題です。
例えば独自ルールとして、**午後に入力必須**となるバリデーションルールを作りたいとします。

さきほどと同じようにRuleを作成してみます。

```
php artisan make:rule PmRequired
```

```php:AmRequired
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Carbon;

class PmRequired implements Rule
{
    /**
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return Carbon::now()->hour < 12 || ($value !== null && $value !== '');
    }

    /**
     * @return string
     */
    public function message()
    {
        return ':attributeは午後は必須です。';
    }
}
```

```php
use App\Rules\PmRequired;

$request->validate([
    'name' => [new PmRequired],
]);
```

しかし、実際にテストしてみるとわかるのですが、このルールは午後に空文字列を渡してもエラーになりません。

これは、LaravelのRuleの仕様となっており、公式ドキュメントにも明記されています。

> By default, when an attribute being validated is not present or contains an empty string, normal validation rules, including custom rules, are not run. 

> デフォルトでは、バリデーションされる属性が存在しないか、空の文字列が含まれている場合、カスタムルールを含む通常のバリデーションルールは実行されません。

空文字列でチェック処理が動かないと、必須チェック系のカスタムルールが作成できません。
どうすればいいのでしょうか。

### 解決方法

実は解決方法は公式ドキュメントに記載されています。

[^]:https://laravel.com/docs/8.x/validation#implicit-rules

`Rule`の代わりに`ImplicitRule`を継承することで、属性が存在しなかったり空文字列の場合もルールオブジェクトによるチェックが行われるようになります。

作成方法は、artisanコマンドでRuleを作成するときに`--implicit`オプションを付けることです。

```
php artisan make:rule PmRequired --implicit
```

先ほどと同じように`app/Rules`下にファイルが作成されています。
先ほどとの違いは、継承元のクラスが`Illuminate\Contracts\Validation\Rule`ではなく`Illuminate\Contracts\Validation\ImplicitRule`になっていることです。

```php:PmRequired.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ImplicitRule;

class PmRequired implements ImplicitRule
{
    /**
     * 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)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}
```

このクラスを修正し、先ほどと同じように午後に必須チェックが行われるようにします。

```php:PmRequired.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\ImplicitRule;
use Illuminate\Support\Carbon;

class PmRequired implements ImplicitRule
{
    /**
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        return Carbon::now()->hour < 12 || ($value !== null && $value !== '');
    }

    /**
     * @return string
     */
    public function message()
    {
        return ':attributeは午後は必須です。';
    }
}
```

これで無事に必須チェック系のルールオブジェクトが作成できました。


### 参考にしたサイト

- https://readouble.com/laravel/8.x/ja/validation.html
- [Laravelの標準バリデーションのわかりにくい挙動を実験して確かめたまとめ](https://qiita.com/kd9951/items/abd063828e33a61c8c58)