📓

Laravel RuleIf なるカスタムルールを作ってみた(少し複雑なルール向け)

2021/06/26に公開

はじめに

Laravel の既存のルールクラスをパクリ参考にしつつ、RuleIf なるカスタムルールを作ってみました。大したものでは無いですが。(ソースは下です)

使い所としては、多少込み入った場合というか、複数のフィールド(他の項目)との絡みがある時などにいいです。もちろん、本当に込み入った時は、専用のカスタムルールを使って下さい。

Laravel にもコールバックを伴う sometimes() やAfterフックなんてのもありますが、
処理が、ちょっと離れた所に行ったりして、そこはちょっと微妙なんですよね。
それらを置き換える事ができたりします。

日本語ドキュメント:複雑な条件のバリデーション(sometimes)
日本語ドキュメント:フォームリクエストへのAfterフックを追加

Ver.8.43~から動きます。

使い方

基本の使い方は、new RuleIf(コールバック関数) とし、コールバック関数の中で、適用させたいルール名を返すのみです。(コールバック関数では、2つのパラメータを受け取れます)
下記の場合、年齢が30歳を超える場合、仕事欄が必須になります。

    'job' => [
        new RuleIf(function ($validator, $input) {
            return $input->age > 30 ? 'required' : 'nullable';
        }),
        'max:4', // 追加のルール(おまけ)
    ],

ただ、上記の場合、あまり使う意味がありません。
三項演算子や RequiredIf を使っても書けます。(下記は、FormRequestを想定)

    'job' => [
        $this->input('age') > 30 ? 'required' : 'nullable',
        'max:4', // 追加のルール
    ],

このルールの使い所として、コールバックの第1パラメータで、$validator インスタンスを受け取れますので、既に年齢欄でエラーが発生している場合は、仕事欄は必須としない(年齢チェックしない)という事もできます。

    'job' => [
        new RuleIf(function ($validator, $input) {
            if ($validator->errors()->has('age')) {
                return 'nullable';
            }

            return $input->age > 30 ? 'required' : 'nullable';
        }),
        'max:4', // 追加のルール
    ],

また、$validator インスタンス がある為、ある条件の時は、ガツッとエラーにするという事もできます。

    'job' => [
        new RuleIf(function ($validator, $input) {
	        if (true) {
	            $validator->errors()->add('job', 'XXXでお願いします。');
	        }

	        return 'nullable';
        }),
        'max:4', // 追加のルール
    ],

ルールは、|で繋げて複数返してもOKです。

     return $input->age > 30 ? 'required|min:2' : 'nullable';

その他細かい話

第2パラメータの $input は、バリデーションの対象となるデータ(入力値)です。
プロパティ形式、配列形式、どちらでもOKです。未定義の場合でもエラーにはならずに、null を返します。

    $input->age;
    $input['age'];

細かい話をすれば、例えば、FormRequest の validationData() メソッドを使って、バリデーションの対象データを書き換えている時は、そちらのデータが取得されます。

RuleIf コード

以下、コードです。app/Rules に配置します。

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ImplicitRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Fluent;
use InvalidArgumentException;

class RuleIf implements ImplicitRule, ValidatorAwareRule, DataAwareRule
{
    /**
     * The validator performing the validation.
     *
     * @var \Illuminate\Validation\Validator
     */
    protected $validator;

    /**
     * The data under validation.
     *
     * @var array
     */
    protected $data;

    /**
     * The failure messages, if any.
     *
     * @var array
     */
    protected $messages = [];

    /**
     * Callable
     *
     * @var callable
     */
    protected $callable;

    /**
     * Create a new required validation rule based on a callable.
     *
     * @param  callable  $callable
     * @return void
     */
    public function __construct($callable)
    {
        if (is_string($callable) || ! is_callable($callable)) {
            throw new InvalidArgumentException('The argument must be a callable.
                String type is disabled for security reasons.');
        }

        $this->callable = $callable;
    }

    /**
     * Set the data under validation.
     *
     * @param  array  $data
     * @return $this
     */
    public function setData($data)
    {
        $this->data = $data;

        return $this;
    }

    /**
     * Set the current validator.
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @return $this
     */
    public function setValidator($validator)
    {
        $this->validator = $validator;

        return $this;
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        $rule = call_user_func($this->callable, $this->validator, new Fluent($this->data));

        $validator = Validator::make($this->data, [
            $attribute => $rule,
        ]);

        if ($validator->fails()) {
            return $this->fail($validator->messages()->all());
        }

        return true;
    }

    /**
     * Get the validation error message.
     *
     * @return array
     */
    public function message()
    {
        return $this->messages;
    }

    /**
     * Adds the given failures, and return false.
     *
     * @param  array|string  $messages
     * @return bool
     */
    protected function fail($messages)
    {
        $messages = collect(Arr::wrap($messages))->map(function ($message) {
            return $this->validator->getTranslator()->get($message);
        })->all();

        $this->messages = array_merge($this->messages, $messages);

        return false;
    }
}

まとめ

もしバグとかあった際は、すいません。予めご了承下さい。(コメント下さい)

自分で作っておきながら、今の所使う予定は無し…。

Discussion