質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.71%

Laravel(PHP)において無効なフォームの情報が送信された時に入力画面にリダイレクトしたい。

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 300

kamille-mio

score 18

環境

Laravel 6.5.0
Homestead
使っているライブラリ
→flatpickr(日付)、datetimepicker(時間)、moment.js、holiday_jp.js

作成しているもの

イメージ説明

画像のようなある施設の利用予約を申請するシステムについて、
決定ボタンが押されるとテーブルからその日の予約情報を取得して、それを参照しDatetimepickerの時間選択で予約が入っている時間は利用開始・利用終了時刻から選択できないようになる。
また、実際は決定ボタンを押したあとに、利用時刻の選択とフォームの全体の情報をPOSTするSubmitボタンが出現し、それを押すと確認画面が表示され訂正か確定かを選択させるといった処理になります。
また、今回の場合は9時~13時、13時~14時、15時~17時で予約が入っていることが前提条件となっています。

質問したいこと

フォームの仕様は以下の通りです。

イメージ説明
イメージ説明
イメージ説明

このようなコードを書いて冒頭の画像の利用開始時刻と、利用終了時刻の選択内容をバインドしています。
ですが、例えばこの場合利用開始に14:00、利用終了に18:00を選択して14:00~18:00の時間帯を予約したいというリクエストができてしまいます。
当然それでは前述の前提条件とバッティングしてしまうのでその場合、確認画面に遷移せずフォームにリダイレクトしてエラーメッセージを表示させたいのですがこの場合バリデーションでやるのか自分で独自例外を作成してtry-catchでその例外に投げるのかどちらにするのかがわからないのでお聞きしたいです。

2020/01/10
コメント欄でご指摘いただいた箇所を修正、かつ自分なりにエラーハンドリングをしてみたのでControllerにそれを反映。

以下コード他です。

AjaxでPHPから帰ってくる予約情報

ちなみにid3は2019-11-30とは別の日付の予約情報なので弾かれています。

 {
        "id": "1",
        "start_time": "2019-11-30 09:00:00",
        "end_time": "2019-11-30 13:00:00"
    },
    {
        "id": "2",
        "start_time": "2019-11-30 15:00:00",
        "end_time": "2019-11-30 17:00:00"
    },
    {
        "id": "4",
        "start_time": "2019-11-30 13:00:00",
        "end_time": "2019-11-30 14:00:00"
    }

バリデーション

とりあえず基本的なところだけ。rulesメソッドのみ抜粋。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
// use App\Http\Requests\Request;

class CreateReserveRequest extends FormRequest
{

    public function rules()
    {
        return [
            'facility_name' => 'not_in:0',
            'dateinfo' => 'required|date',
            'start_time' => 'required|date_format:H:i',
            'end_time' => 'required|date_format:H:i',
        ];
    }
}

model

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Reserve extends Model {
    public $incrementing = false;

    protected $fillable = [
        'user_id',
        'facility_id',
        'start_time',
        'end_time',

    ];

    public function user() {
        return $this->belongsTo('App\User');
    }

    public function facility() {
        return $this->belongsTo('App\Facility');
    }

    static function SearchReserveDates($facility_name, $dateinfo) {

        $result = \App\Reserve::whereHas('Facility', function($query) use($facility_name, $dateinfo) {
        $query
            ->where('facility_name',$facility_name)
            ->whereDate('start_time',$dateinfo)
            ->whereDate('end_time',$dateinfo);
                        })->select('id', 'start_time', 'end_time')->get()->all();
        return $result;
    }
}

Controller(自力で予約確定まで組んでみたものです)

<?php

class ReserveController extends Controller {

    // フォームから受け取った情報をもとにテーブルを検索、施設名から施設IDを引っ張り、日付と合わせて予約情報を取得しAjaxに返す。
    public function searchReservation(Request $request) {

        $data = $request->all();

        if(isset($data['dateinfo']) && isset($data['facility_name'])) {

        $dateinfo = $data['dateinfo'];
        $facility_name = $data['facility_name'];

        $reserveinfo = Reserve::SearchReserveDates($facility_name, $dateinfo);

        \Debugbar::info();

        return json_encode($reserveinfo, JSON_PRETTY_PRINT);

        } else {
       echo 'FAIL TO AJAX REQUEST';
    }
}

    // 入力確認画面に最終的なフォームの値を渡す。Requestにはバリデーションの拡張クラスを渡す。
    public function confirm(CreateReserveRequest $request) {

        // 予約情報取得

        $data = $request->all();

        if(isset($data['dateinfo']) && isset($data['facility_name'])) {

        $dateinfo = $data['dateinfo'];
        $facility_name = $data['facility_name'];

        $reserveinfo = Reserve::SearchReserveDates($facility_name, $dateinfo);
    }


        // 取得したレコードを配列の形にする。
        // start_timeとend_timeそれぞれの値で配列を作る。

        $arr_start_time = array_column($reserveinfo, 'start_time');
        // var_dump($arr_start_time);
        $arr_end_time = array_column($reserveinfo, 'end_time');
        // var_dump($arr_end_time);

         // 選択された時間帯が予約時間と重複していないか検証
        if(isset($data['start_time']) && isset($data['end_time'])) {

        // datetime型に整形
        $start_datetime =$data['dateinfo'] .' '. $data['start_time'];
        $end_datetime =$data['dateinfo'] .' '. $data['end_time'];

        // Carbonに整形

        $st = new Carbon($start_datetime);
        $start = $st->format('Y-m-d H:i:s');
        // var_dump($start);
        $ed = new Carbon($end_datetime);
        $end = $ed->format('Y-m-d H:i:s');
        // var_dump($end);
        // テーブルから予約情報取得して配列に格納
        // それぞれ$arr_start_time[]と$arr_end_times[]で呼び出せるようにする
        // forかwhile文で$start_time[n]と$end_time[n]まで検証する

        // $arr_start_time及び$arr_end_timeの配列の数を取得する。
        $c1 = count($arr_start_time);
        $c2 = count($arr_end_time);


        // 時間帯比較の関数
        function isTimeDuplication($start, $end, $start_time, $end_time) {
            return ($start < $end_time && $start_time < $end);
        }
        // try-catchで例外が出たら入力フォームまでロールバックする。

        try {

            for ($i=0; $i < $c1 && $c2 ; $i++) {

            $start_time = $arr_start_time[$i];
            $end_time = $arr_end_time[$i];

            $result = isTimeDuplication($start, $end, $start_time, $end_time);

                if($result === TRUE) {
                    throw new ReserveDuplicationException;
                }
            }

            // 入力確認ページのviewにdataを渡す
            return view('reserve-confirm', [
                'data' => $data,
            ]);

        } catch(ReserveDuplicationException $e) {
            throw $e;
        }


    }
}

        // 予約完了メールの発送
        public function send(Request $request) {

            //フォームから受け取ったactionの値を取得
            $action = $request->input('action');
            $inputs = $request->except('action');

            //actionの値で分岐
            if($action !== 'submit'){
                return redirect()
                    ->route('reserve.index')
                    ->withInput($inputs);

            } else {
                $user = Auth::user();
                $email = $user->email;
                \Mail::to($email)->send(new ContactSendmail($inputs));

            // トークンを再発行して再送信防止

                $request->session()->regenerateToken();

            }

    }


// 予約確定による、データベースへの情報の追加

    public function store(Request $request) {

        // バリデーション

        $request->validate([
             'facility_name' => 'required',
             'dateinfo' => 'required',
             'start_time' => 'required',
             'end_time' => 'required',
        ]);


        // 再度ダブルブッキングのチェックを行う。
        $this->confirm($request);

        // 以下、ダブルブッキングなしの場合の更新処理

        //フォームから受け取ったactionを除いたinputの値を取得
        $inputs = $request->except('action');

        // 検索用にfacility_nameをのみ別に変数に取り出しておく

        $facility_name = $inputs['facility_name'];

        // facility_idを抽出するメソッド

        $facility_id = Facility::SearchFacility_id($facility_name);


        // dateinfoとstart_time及びend_timeを組み合わせてdatetime型にする。
        $start_time =$inputs['dateinfo'] .' '. $inputs['start_time'];
        $end_time =$inputs['dateinfo'] .' '. $inputs['end_time'];

        // 予約番号生成

        $reserve_number =  uniqid(bin2hex(random_bytes((1))));

        // ユーザー情報取得
        $user = Auth::user();
        $user_id = $user->id;

        // データベースに追加
        $reserve = new Reserve();
        $reserve->user_id = $user_id;
        $reserve->facility_id = $facility_id;
        $reserve->start_time = $start_time;
        $reserve->end_time = $end_time;
        $reserve->reserve_number = $reserve_number;
        $reserve->save();

        \Debugbar::info();

        $this->send($request);

        // viewへ遷移
        return view('reserve-complete' ,[
                'reserve_number' => $reserve_number,
            ]);
    }

質問に際してやったこと・作ったもの

console.log一覧

イメージ説明

定義しているテーブル

イメージ説明

追記

Handler.php

 public function prepareResponse($request, Exception $e) {
        // 競合違反を条件分岐
        if($e instanceof ConflictHttpException) {
            return $this->invaildHttpRequest($request, $e);
        } 
        // 予約時間が競合していた場合エラーハンドリング
        if($e instanceof ReserveDuplicationException) {
            return redirect()->back()->withInput()->withErrors('その時間帯はすでに予約が入っています');
        }

        return parent::prepareResponse($request, $e);
    }
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • m.ts10806

    2020/01/09 10:56

    >$_POST
    Laravel使ってるのであれば$_POST使う機会なんてないと思いますが何を参考に作りました?
    日本語ドキュメントもかなり公式に近い状態のものがあって充実しているのですけどそちらは確認されたのでしょうか。

    キャンセル

  • kamille-mio

    2020/01/10 03:17

    m.ts10806様
    コメントありがとうございます。

    申し訳ない、これに関しては当方PHP・Laravel歴1ヶ月なのでまだまだ混同して覚えてることが多く、今回作ってるものは雛形はあるもののコード自体は自力で試行錯誤しながら書いているものも多いのでこういったことが起きてしまいました。
    大変お恥ずかしい限りです。

    ご指摘を受け、調べたところLaravelではフォームからの情報の受け取りを$request->input()や直接input()の部分をフォームのname属性を指定してやることで受け取れるということですので今回の場合は$data = $request->all();の形でフォームの内容をすべて取得していますから、個別のものに関しては$data[]の形で書くのが適切でした。

    そのように修正しておきましたのでご確認ください。

    キャンセル

回答 1

checkベストアンサー

+1

必要な技術はバリデーションでも例外でもなく、テーブル設計の見直しです。

バリデーション:特定の値に対し、入力条件に一定の制限をかける処理
例外処理:特定のメソッドや関数実行に対し、処理外の論理上エラーが発生した場合に例外エラーを発生させる処理

今回は要件定義に基づき、start_timeに対してelse ifの対応をさせるだけですが、こういう時間指定の場合のダブルブッキングを防ぐ方法として、1日ごとのタイムスケジュールを設定する、あるいは利用者と使用時間を主キーとして施設利用スケジュール用のトランザクションを用意した方が確実ですよ。start_timeやend_timeはタイムスタンプ(時刻)をとるのにはいいですが、不特定多数が絡んでくるタイムライン(時間)をとるのに使ったりすると、色々と困った問題が発生しますし、SQLの検索条件がかなりえぐいことになります。

小学生の算数に顧みて、時刻と時間の概念から見直してみましょう。

#施設利用トランザクション
利用者番号(主キー)
日付(主キー)
施設番号

たとえば、id1が9時から13時を利用している間は、

id1,2019-11-30 09:00:00,施設番号
id1,2019-11-30 10:00:00,施設番号
id1,2019-11-30 11:00:00,施設番号
id1,2019-11-30 12:00:00,施設番号

と保持させておいて、新たに11月30日の午前11時からの開始時刻で申請しようとしたid5が現れても、その時間はスケジュールが埋まっているので、あとは選択のプルダウンでその値を候補から外したり、また旅館予約サイトみたいに、明示的に予約済かどうか案内するリストも簡単に作ることができますね。

それをLaravelに落とし込むだけです。

なお、利用トランザクションの場合、利用者を主キーに置くか、施設を主キーに置くかですが、場合によりけりです。もし、会員制施設で非会員などを許可する場合は施設番号を主キーに置きます。逆に会員制で利用者に必ずidが付される場合は利用者を主キーに置いた方がいいでしょう。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/01/15 02:49

    FKMさん、回答ありがとうございます。
    返信が遅くなって申し訳ありません。

    今回、初めて自力でシステム(webアプリ)を作るということで
    https://tennisfull.com/koutou/godaikameido.html?date=2019/12/3
    こちらの利用登録システムを元に自分でやれるレベルまでデチューンしながら制作していて、テーブル設計も初めてだったのですが、やなりそこから甘かったということを痛感しました。
    一応、テーブル設計については過去以下のような質問をして得られた回答を元に設計したものになっています。
    https://teratail.com/questions/225110

    今回は一応動作的には確認できていますが、独立性阻害要因の排除が甘いですし
    ご指摘の通り、時間帯の最初と最後で保持させるより時間帯を1時間毎に保持させた方がスケジュールの有無は判定させやすいですし、タイムライン周りは今回の件でかなりややこしいということがわかったので反省点として次回の作成に活かしたいと思います。

    キャンセル

  • 2020/01/15 03:17 編集

    追伸。

    今回の回答を落とし込んだものではなくあくまでそれを見るまでに考えたものにはなりますが、予約確定までの処理をControllerの方に追記しましたのでよろしければご覧になっていただけると幸いです。
    初めて成果物を作っていますが、まだまだ自分の力が足りないなということを思い知らされる毎日です。
    特にエラーハンドリングとトランザクションは今回、基礎レベルではまだまだ足りないことを実感したのでで実例を経験しながらより理解を深めていきたいと思います。

    キャンセル

  • 2020/01/15 09:51

    確認しましたが、以下の場合にも対応しているか確認した方がいいです。

    ●開始時刻はダブルブッキングを判定できるが、終了時刻は別の人の予約時間に含まれているケース
    予約済:11:00-14:00、16:00-17:00
    予約したい人: 15:00-17:00

    ●開始時刻と終了時刻の間に別の人の予約が含まれているケース
    予約済:9:00-11:00, 13:00-15:00,17:00-18:00
    予約したい人:12:00-16:00

    このシステムだとこれが通ってしまう危険があるかも

    キャンセル

  • 2020/01/15 18:25

    再度、返信ありがとうございます。
    本筋とは関係ないところにもご指摘頂けて大変助かります。

    確認しましたが前者は利用終了時刻のプルダウンで17時が弾かれるのでそもそも選択ができない(念の為12時~18時で確認しましたがエラーでリダイレクトできました)のと、後者は12~16時で確認したところやはり同様にエラーでリダイレクトを確認しました。
    ただ、正直すべての予期せぬ確定処理や脆弱性をテストするのは私にはまだまだ勉強不足で難しいのでFKMさんが危惧された以外のところから通ってしまう危険は否定できないですね……ここが初学者の難しいところです。

    一応このまま体裁を整えて成果物としてGithubやherokuにUPしてQiitaに作成記録等々をまとめて他の方に実際に動かしてくれた方や、Qiitaの記事を読んでいただけた方のレビューを待つしかないのかなと思っています。
    他力本願で心苦しいところではあります。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.71%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる
  • トップ
  • PHPに関する質問
  • Laravel(PHP)において無効なフォームの情報が送信された時に入力画面にリダイレクトしたい。