Laravelで予約が入っているかチェックする独自バリデーションを作る

さてさて、実は現在もLaravel最新版を使った開発を行っている最中なのですが、この開発の中で、デフォルトでは存在しないちょっと複雑なバリデーションをする必要がありました。

しかし、そこはさすがLaravelです。

そんなイレギュラーな状況にも対応できるように「独自のバリデーション」を定義できるようにしてくれています。

・・・ということで、今回はこの機能を使って「ある部屋の、ある時間に予約が入っているか」をチェックするバリデーションを作ってみます。

ぜひ参考にしてみてください!

やりたいこと

例えば、ある居酒屋「よいどれ(仮名)」の「桜の間(部屋番号:1)」が次の時間帯に空いているかどうかをチェックするというものです。

2018年10月1日 19時00分 〜 23時00分

では、少し長いですがひとつずつ見ていきましょう!

予約テーブルをつくる

まず予約を保存しておくテーブルがないと始まりません。

そこでreservationsというテーブルをマイグレーションを使って作成します。以下のコマンドを実行しましょう。

php artisan make:migration create_reservations_table

これで、

/database/migrations/フォルダに****_**_**_******_create_reservations_table.phpというファイルが作成されるので、このファイルを開いてup()内に必要なフィールド項目を追加します。

public function up()
{
    Schema::create('reservations', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('room_id')->comment('部屋番号');
        $table->dateTime('start_at')->comment('開始時間');
        $table->dateTime('end_at')->comment('終了時間');
        $table->timestamps();
    });
}

そして、以下のコマンドでマイグレーションを実行してテーブルを作成しましょう。

php artisan migrate

コマンド実行すると以下のようなテーブルができます。(ちなみにcomment()を使ってフィールドの説明を追加しておくと、保守管理が楽になりますよ!)

送信フォームをつくる

では、次に予約データを送信するフォームを作成します。このフォームから送信されたデータを使って、すでに予約があるかどうかをチェックします。

今回はテストなので、Ajaxは使わずノーマルなHTMLの送信フォームで実行します。

まずは予約を実行するための以下2つのRouteを/routes/web.phpに追加します。

Route::get('reservation', 'ReservationController@create'); // 入力フォーム
Route::post('reservation', 'ReservationController@store'); // 送信先

このままではReservationControllerが存在せずエラーになってしまうので、以下のコマンドでコントローラーも作成し、必要なメソッドcreatestoreを追加します。

php artisan make:controller ReservationController
class ReservationController extends Controller
{
    public function create() {

        return view('reservation');

    }

    public function store() {

        // ここで予約データ保存

    }
}

さらに、このままではビューが存在していないので、/resources/viewsフォルダにreservation.blade.phpを作成してここに送信フォームを作ります。

ファイルの中身は以下になります。(本来はlayoutsなどを使うべきですが、今回はテストなので直にHTMLタグなどを書き込んでいます)

<html>
<body>
    <form method="POST" action="/reservation">
        @csrf
        <select name="room_id">
            <option></option>
            <option selected value="1">桜の間</option>
            <option value="2">松の間</option>
            <option value="3">竹の間</option>
            <option value="4">梅の間</option>
        </select>
        <br>
        <br>
        <input type="date" name="start_date" value="2018-10-01">
        <input type="time" name="start_time" value="19:00">
        <br>
        <input type="date" name="end_date" value="2018-10-01">
        <input type="time" name="end_time" value="23:00">
        <br>
        <br>
        <button type="submit">予約する</button>
    </form>
</body>
</html>

ページへアクセスするとこうなります。

バリデーションを使う準備をする

では、先ほどのフォームから送信されたデータをチェックするバリデーションを設定するためにReservationRequestという専用のフォームリクエストを作成しましょう。

以下のコマンドを実行してください。

php artisan make:request ReservationRequest

実行すると、/app/Http/Requests/ファルダにReservationRequest.phpというファイルができているので、ReservationControllerstoreメソッドに以下のように追加します。

public function store(ReservationRequest $request) {

    // ここで予約データ保存

}

(ちなみに以下のネームスペースを省略しているので注意してください)

use App\Http\Requests\ReservationRequest;

では、これでバリデーションを使う準備ができました。ReservationRequest.phpを開いてauthorize()all()rules()を以下のように変更しましょう。

public function authorize()
{
    return true;
}
public function all($keys = null)
{
    $results = parent::all($keys);
    $results['start_at'] = $results['start_date'] .' '. $results['start_time'];
    $results['end_at'] = $results['end_date'] .' '. $results['end_time'];
    return $results;
}

all()内では開始日時を$this->start_at、終了日時を$this->end_atとして取得できるようオーバーライドしています。

public function rules()
{
    return [
        'room_id' => 'required', // 本来はexistsを入れるべきですが、今回は割愛します
        'start_date' => 'required|date',
        'start_time' => 'required|date_format:H:i',
        'end_date' => 'required|date',
        'end_time' => 'required|date_format:H:i',
        'start_at' => [
            new ReservationRule(
                $this->room_id, // 部屋番号
                $this->start_at, // 開始日時
                $this->end_at // 終了日時
            )
        ]
    ];
}

そして、rules()ではnew ReservationRule()という部分が独自バリデーションになります。次はこの部分を作っていきます。

※ ちなみに、その他のバリデーションは全53種類!Laravel 5.6のバリデーション実例を参考にしてください。

モデルを作る

では、独自バリデーション内部で必要になるので、事前にReservationモデルを作って「予約が入っているかどうかをチェックする」独自スコープ(whereメソッド)を作っておきましょう。

モデルは以下のコマンドで作成します。

php artisan make:model Reservation

そして、app/Reservation.phpを開いて以下のようにscopeWhereHasReservation()というメソッドを追加します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Reservation extends Model
{
    protected $guarded = ['id'];

    // Scope
    public function scopeWhereHasReservation($query, $start, $end) {

        $query->where(function($q) use($start, $end) { // 解説 - 1

            $q->where('start_at', '>=', $start)
                ->where('start_at', '<', $end);

        })
        ->orWhere(function($q) use($start, $end) { // 解説 - 2

            $q->where('end_at', '>', $start)
                ->where('end_at', '<=', $end);

        })
        ->orWhere(function($q) use ($start, $end) { // 解説 - 3

            $q->where('start_at', '<', $start)
                ->where('end_at', '>', $end);

        });

    }
}

ちなみにスコープの中では、以下3パターンをor検索しています。

  • チェックしたい時間帯の中から始まる予約がある
  • チェックしたい時間帯の中で終わる予約がある
  • チェックしたい時間帯の中にすっぽり入る予約がある

つまり、絵で紹介すると以下のようになります。

パターン1

パターン2

パターン3

そして、このスコープを設定するとwhereHasReservation()メソッドが使えるようになり、引数に開始日時と終了日時を入れるだけで「その時間帯に予約データが存在している」という条件を追加することができます。

\App\Reservation::whereHasReservation($start_at, $end_at)->get();

※ なおモデル内には保存時にエラーが発生しないよう、以下のコードも追加しているので注意してください。

protected $guarded = ['id'];

独自バリデーションを作る

やっと本題にたどりつきました(笑)
では、以下のコマンドを実行して独自バリデーションのファイルを作成します。

php artisan make:rule ReservationRule

すると、/app/Rules/ReservationRule.phpが作成されるので、この中に予約データをチェックするコードを書いていきます。

まずコンストラクタでReservationRequestから受け取るデータをメンバ変数に格納し、どこからでもアクセスできるようにします。

private $_room_id,
        $_start_at,
        $_end_at;

public function __construct($room_id, $start_at, $end_at)
{
    $this->_room_id = $room_id;
    $this->_start_at = $start_at;
    $this->_end = $end_at;
}

次にpasses()メソッドです。
このメソッドでtrueを返せば、このバリデーションは通過します。

public function passes($attribute, $value)
{
    return \App\Reservation::where('room_id', $this->_room_id)
        ->whereHasReservation($this->_start_at, $this->_end)
        ->doesntExist();
}

そのため、doesntExist()で、その他のデータが存在していないならtrueを返すようにしています。

そして、せっかくなのでmessage()でエラーメッセージも変更しておきましょう。

public function message()
{
    return '他の予約が入っています。';
}

これで独自バリデーションは完成です。

予約データを保存する部分をつくる

では、最後になりました。
ReservationControllerstore()メソッドの中に予約データを保存するコードを作ります。

\App\Reservation::create([
    'room_id' => $request->room_id,
    'start_at' => $request->start_at,
    'end_at' => $request->end_at
]);

そして、保存した後はback()with()を使って完了メッセージとともに元ページへリダイレクトするようにします。

return back()->with('result', '予約が完了しました。');

成功&エラーメッセージが表示されるようにする

上の作業で、reservation.blade.phpに送信フォームを作りましたが、まだここには以下2つのメッセージが表示されるようにはなっていませんので、これらを作ってから最後に予約データを実際に送信してみることにします。

  • 予約が成功した時の完了メッセージ
  • バリデーションで失敗した場合のエラー
<form method="POST" action="/reservation">
    @csrf

    <!-- 完了メッセージ -->
    @if (session('result'))
        <div style="color:green;">
            {{ session('result') }}
        </div>
        <br>
    @endif

    <!-- 省略 -->

    <!-- エラー表示 -->
    @if($errors->any())
        <div style="color:red;">
            【エラー】<br><br>
            @foreach ($errors->all() as $error)
                {{ $error }}<br>
            @endforeach
        </div>
        <br>
    @endif

    <button type="submit">予約する</button>
</form>

※ ちなみに独自バリデーション以外のエラーメッセージは日本語化されていません。日本語化の方法は【Laravel5.6】インストール直後にやること3点の「2.バリデーション」をご参照ください。

実際に予約データを登録してみる

では、お待たせしました。
実際に予約データを送信してみましょう!

すでにデフォルト値が入っているのでこのまま「予約ボタン」をクリックします。
すると・・・??

うまくいきました!
念のため、データベース内も見てみましょう。

こちらも問題なく、部屋番号と予約時刻が保存されています。

では次に今の状態で再度データを送信してみましょう。すでに先約があるので、独自バリデーションがうまく働いているなら、エラーが発生するはずです。

うまくエラーが表示されました。

では、次に開始と終了の時間だけを変更して送信してみましょう。

この場合、すでに19:00〜23:00で予約が入っているので、エラーが表示されるべきです。

うまくエラーが表示されました。(今回はテストなので入力値は元に戻っています)

では最後に同じ日付の同じ時間帯ですが、違う部屋を予約してみましょう。

もちろん、「梅の間」に予約はまだ1件も入っていませんので、予約は成功するべきです。

これもうまくいきました。
DBには、部屋番号が違う同じ時間帯のデータが追加になり、全2件になっています。

これで、今回の作業は終了です。

お疲れ様でした!

今回使ったコードをダウンロード

今回の記事で使った全コードを以下からダウンロードできます。細かな部分は別途実装する必要がありますが、根本部分は完了しているのでぜひ参考にしていただけたら嬉しいです。

Laravelで予約チェックするバリデーション・全コード

おわりに

ということで、今回は予約チェックをするLaravelの独自バリデーションを作ってみました。

いつも感じていることですが、きっと今回のように独自のカスタマイズがしやすいLaravelの包容力が世界中の開発者をとりこにしているのは間違いないのではないでしょう。^^

また、今回作ったwhereHasReservation()などのような汎用的に使えるメソッドはトレイト化しておけば後でなんどでも使えますし、開発速度も早くすることができて一石二鳥ですね。

みなさんの参考になれば幸いです!

ではでは。