Laravelで複数フィールドのバリデーションを作る

通常のよくあるバリデーションルールでは単体のフィールドをチェックしますが、複数のフィールドの関連性を見てチェックしたい場合にどうするのか調べてみました。ひとまず実装はできていて動くのですが、もう少し良いやり方があるかもしれません。もっと良いやり方があればTwitterなどで教えてください。

カスタムのバリデーションルールを作成するための設定

まずカスタムのバリデーションルールを作成するための方法ですが、手順としては下記のようになります。

  1. LaravelのValidatorを継承した独自のValidatorクラスを作成する
  2. 1で作成した独自のValidatorを登録するためのServiceProviderを作成する
  3. config/app.phpで2で作成したServiceProviderを読み込む

まず1のValidatorクラスですが下記のような形で作成します。

namespace App\Utils;

use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\File\File;

class CustomValidator extends Validator
{
    public function __construct(
        TranslatorInterface $translator,
        array $data,
        array $rules,
        array $messages,
        array $customAttributes
    ) {
        parent::__construct($translator, $data, $rules, $messages, $customAttributes);
    }

	// ここにルールを追加する
	// validateで始まるメソッドがルールになる
    public function validateCombiDate($attribute, $value, $parameters)
    {
        $year = Arr::get($this->data, $parameters[0] . '_year');
        $month = Arr::get($this->data, $parameters[0] . '_month');
        $day = Arr::get($this->data, $parameters[0] . '_day');

        $isAllFilled = ! empty($year) && ! empty($month) && ! empty($day);
        if ($isAllFilled) {
            return $this->validateDate($attribute, "{$year}-{$month}-{$day}");
        } else {
            return true;
        }
    }
}

次にServiceProviderは下記のような感じです。

namespace App\Providers;

use App\Utils\CustomValidator;
use Illuminate\Support\ServiceProvider;

class CustomValidatorServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        \Validator::resolver(function ($translator, $date, $rules, $messages, $attributes) {
            return new CustomValidator($translator, $date, $rules, $messages, $attributes);
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

最後にapp.phpに下記のようにServiceProviderを読み込むように設定します。

    'providers' => [

		(省略…)

        /*
         * バリデーションの独自拡張を行う
         */
        App\Providers\CustomValidatorServiceProvider::class,

		(省略…)

年・月・日の日付の正しさを検証するルール

それでは次に本題の複数フィールドをチェックするルールを作成してみたいと思います。年・月・日の日付の正しさを検証するルールです。年・月・日がそれぞれセレクトボックスになっているイメージです。先ほどの独自のValidatorの中に既に実装していたものですが、一例として下記のような形で実装できます。再掲します。

    public function validateCombiDate($attribute, $value, $parameters)
    {
        $year = Arr::get($this->data, $parameters[0] . '_year');
        $month = Arr::get($this->data, $parameters[0] . '_month');
        $day = Arr::get($this->data, $parameters[0] . '_day');

        $isAllFilled = ! empty($year) && ! empty($month) && ! empty($day);
        if ($isAllFilled) {
            return $this->validateDate($attribute, "{$year}-{$month}-{$day}");
        } else {
            return true;
        }
    }

使い方としては下記のような形になります。設定箇所は今回はFormRequestで行っていますが別のやり方でも同様に設定できると思います。

    public function rules()
    {
        return [
            'hidden_date_validation' => 'combi_date:birthday', // ここでbirthdayで渡したものはメソッドでは$parametersとして渡ってくる
        ];
    }

今回は3つのフィールドの総合的なチェックになるので、特定のセレクトボックスのフィールド名をキーにするのではなく、代表のフィールドを設けてそのフィールドに対してルールを設定するようにしています。今回はhidden_date_validationという名称にしています。

最後にblade部分は下記のようにしています。

<div class="form-group">
    <div class="col-sm-5">
        <label class="control-label">誕生日</label>
    </div>
    <div class="col-sm-7">
        <div class="row @if($errors->has('hidden_date_validation')) has-error @endif">
            <div class="col-xs-4 @if($errors->has('birthday_year')) has-error @endif">
                <select name="birthday_year" class="form-control">
                    <option value="">年</option>
                    @foreach($years as $year)
                        <option value="{{ $year }}" @if(old('birthday_year', $default['birthday_year'] ?? '') == $year) selected @endif>{{ $year }}年</option>
                    @endforeach;
                </select>
            </div>
            <div class="col-xs-3 @if($errors->has('birthday_month')) has-error @endif">
                <select name="birthday_month" class="form-control">
                    <option value="">月</option>
                    @foreach($months as $month)
                        <option value="{{ $month }}" @if(old('birthday_month', $default['birthday_month'] ?? '') == $month) selected @endif>{{ $month }}月</option>
                    @endforeach;
                </select>
            </div>
            <div class="col-xs-3 @if($errors->has('birthday_day')) has-error @endif">
                <select name="birthday_day" class="form-control">
                    <option value="">日</option>
                    @foreach($days as $day)
                        <option value="{{ $day }}" @if(old('birthday_day', $default['birthday_day'] ?? '') == $day) selected @endif>{{ $day }}日</option>
                    @endforeach;
                </select>
            </div>
        </div>
        <div class="row has-error">
            <div class="col-xs-12">
                <span class="help-block" data-role="error" data-for="birthday_year">{{ $errors->first('birthday_year') }}</span>
                <span class="help-block" data-role="error" data-for="birthday_month">{{ $errors->first('birthday_month') }}</span>
                <span class="help-block" data-role="error" data-for="birthday_day">{{ $errors->first('birthday_day') }}</span>
            </div>
        </div>

        <div class="row has-error">
            <div class="col-xs-12">
                <input name="hidden_date_validation" type="hidden" value="1">
                <span class="help-block" data-role="error" data-for="hidden_date_validation">{{ $errors->first('hidden_date_validation') }}</span>
            </div>
        </div>
    </div>
</div>

先ほどのルール設定したhidden_date_validationは実際にhiddenのinputとして存在しています。ちなみにvalueを1にしていますが、ここが空だとLaravel5.2ではフレームワーク側の判定でカスタムのバリデーションルールが実行されないようなので明示的に値を指定してやる必要がありました。

エラーの有無やエラーメッセージの取得はhidden_date_validationをキーにすればうまくいきます。

ちなみに<input name="hidden_date_validation" type="hidden" value="1">の値を指定しなくてもimplicitRulesにバリデーションルールを追加する事で指定したバリデーションルールが実行されるようにできます。方法としては作成した独自のValidatorクラスでコンストラクタで下記のようにimplicitRulesに追加するコードを書くだけです。

class CustomValidator extends Validator
{
    public function __construct(
        TranslatorInterface $translator,
        array $data,
        array $rules,
        array $messages,
        array $customAttributes
    ) {
        parent::__construct($translator, $data, $rules, $messages, $customAttributes);
        // 値がnullで実行されるルール
        $this->implicitRules[] = 'CombiDate';
    }