Help us understand the problem. What is going on with this article?

laravel5 日付入力のバリデーションはどう書くのが気持ちいいか

More than 1 year has passed since last update.

laravelのバリデーションルールはとても種類が多くてありがたいのですが、
それでも既存のバリデーションでは対応してないやり方をしたい場合もあります。

7c5605de-1f49-0795-c0b0-e5d6f966bb8d.png

マイページ系の画面でこんな感じの生年月日入力はよく見かけます。
これのバリデーションを考えたいと思います。
環境はlaravel5.3ですが、たぶん5系ならどれでもいけるかと思います。

やりたいこと

  • 生年月日を「年」「月」「日」別のプルダウンで入力させたい
  • 入力必須項目ではないものとする(「年」「月」「日」が全て選択されてない場合は未入力)
  • 「年」「月」「日」のどれかが選択されててどれかが選択されてない、という場合はエラー
  • 「年」「月」「日」が全て選択されていて、不正な日付(02/30とか)の場合はエラー
  • 上記2つのエラー表示を分けたい

前準備

方針

  • カスタムバリデーションでの実装はあまり気持ちいい書き方を見つけられなかった
  • フォームリクエストバリデーションで validate() getValidatorInstance()をオーバーライドして、日付を連結したフィールドを作ることにした
  • 連結したフィールドに標準のdateバリデーションをかけてチェック

実装

ごくシンプルな投稿コントローラー

app\Http\Controllers\UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
a
use App\Http\Requests;

use App\Http\Requests\UserRequest;
use App\User;

class UserController extends Controller
{

    public function index()
    {
        return view('users.index');
    }

    public function store(UserRequest $request)
    {
        $user = User::create($request->all());

        if (!$user->birth) {
            $user->birth = null;
        }

        $user->save();
        return view('users.store');
    }

}

フォームリクエストの validate() getValidatorInstance() をオーバーライドして、
「birth_year」「birth_month」「birth_day」を連結した「birth」フィールドを作成。
あとは普通にbirthに対してdateバリデーションを行うようrules()に記述。
「年が入力されてて月が入力されてない」みたいなのはrequired_withで対応。

[追記] laravel 5.4とか5.5あたりから、validate()では捕捉できなくなったようです。
getValidatorInstance()を使用します。

app\Http\Requests\UserRequests.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'name_first'  => 'required',
            'name_last'   => 'required',
            'birth'       => 'date',
            'birth_year'  => 'required_with:birth_month,birth_day',
            'birth_month' => 'required_with:birth_year,birth_day',
            'birth_day'   => 'required_with:birth_year,birth_month',
        ];
    }

    public function getValidatorInstance()
    {
        if ($this->input('birth_day') && $this->input('birth_month') && $this->input('birth_year'))
        {
            $birthDate = implode('-', $this->only(['birth_year', 'birth_month', 'birth_day']));
            $this->merge([
                'birth' => $birthDate,
            ]);
        }

        return parent::getValidatorInstance();
    }
}

フィールドの日本語化設定をしておく。

resources\lang\ja\validation.php(抜粋)
    'attributes' => [
        'name_last'       => '姓',
        'name_first'      => '名',
        'birth'           => '生年月日',
        'birth_year'      => '生年月日(年)',
        'birth_month'     => '生年月日(月)',
        'birth_day'       => '生年月日(日)',
    ],

テンプレート。
「birth_year」「birth_month」「birth_day」「birth」に対して別々にエラーを引ける。

resources\views\users\index.blade.php(抜粋)
    <div class="form-group{{ $errors->has('birth') || $errors->has('birth_year') || $errors->has('birth_month') || $errors->has('birth_day') ? ' has-error' : '' }}">
        <label for="birth_year" class="col-md-4 control-label">生年月日</label>

        <div class="row">
            <div class="col-md-2">
                <select id="birth_year" class="form-control" name="birth_year">
                <option value="">----</option>
                @for ($i = 1980; $i <= 2005; $i++)
                <option value="{{ $i }}"@if(old('birth_year') == $i) selected @endif>{{ $i }}</option>
                @endfor
                </select>

                @if ($errors->has('birth_year'))
                    <span class="help-block">
                        <strong>{{ $errors->first('birth_year') }}</strong>
                    </span>
                @endif
            </div>

            <div class="col-md-2">
                <select id="birth_month" class="form-control" name="birth_month">
                <option value="">--</option>
                @for ($i = 1; $i <= 12; $i++)
                <option value="{{ $i }}"@if(old('birth_month') == $i) selected @endif>{{ $i }}</option>
                @endfor
                </select>

                @if ($errors->has('birth_month'))
                    <span class="help-block">
                        <strong>{{ $errors->first('birth_month') }}</strong>
                    </span>
                @endif
            </div>

            <div class="col-md-2">
                <select id="birth_day" class="form-control" name="birth_day">
                <option value="">--</option>
                @for ($i = 1; $i <= 31; $i++)
                <option value="{{ $i }}"@if(old('birth_day') == $i) selected @endif>{{ $i }}</option>
                @endfor
                </select>

                @if ($errors->has('birth_day'))
                    <span class="help-block">
                        <strong>{{ $errors->first('birth_day') }}</strong>
                    </span>
                @endif
            </div>
        </div>

        <div class="row col-md-6 col-md-offset-4">
        @if ($errors->has('birth'))
            <span class="help-block">
                <strong>{{ $errors->first('birth') }}</strong>
            </span>
        @endif
        </div>

    </div>

こんな感じ

正しい日付の場合は普通に投入できる
laravel1.png

生年月日が全て未入力の場合も普通に投入できる
laravel1.png

生年月日が一部しか入力されてない場合はエラー
※冗長なのでエラーメッセージは差し替えたほうがいいかも
laravel1.png

生年月日が全部入力されてるけど正しい日付じゃない場合はエラー
laravel1.png

気持いいポイント

  • 最終的に標準のバリデーションルールしか使ってないのが気持ちいい
  • 連結したbirthフィールドをそのままDBに突っ込めちゃうからコントローラーがすっきりして気持ちいい
  • エラーを別々にできるのがとても気持ちいい(最終的に「日付が不正だ」というエラーを$errors->first('birth_year')で引いてくる、みたいなのは避けたかった(だって別に年のエラーじゃなく日付全体のエラーなわけだし))

あとがき

laravel触りはじめてそんなに経ってない身ですので
「このやり方だとここが気持ちよくない」とか
「カスタムバリデーションならこうやれば気持ちいい」とか
「こうするともっと気持ちよくなる」とか
「俺のコードはまた別の気持ちよさが」とか
あれば是非教えて下さるとうれしいです。

参考リンク

https://laracasts.com/discuss/channels/general-discussion/l5-how-to-use-after-method-on-form-request

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away