🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Google フォーム

Google フォームは、 Google社が提供しているアンケートフォーム作成および集計ができる無料のツール。Googleアカウントがあれば利用が可能です。集計データは、スプレッドシートに収集され、データ分析もできます。

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

リダイレクト

プログラムの入力元や出力先を通常とは別の場所に転送させることをリダイレクトと呼びます。

Q&A

解決済

1回答

3719閲覧

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

kamille-mio

総合スコア24

Google フォーム

Google フォームは、 Google社が提供しているアンケートフォーム作成および集計ができる無料のツール。Googleアカウントがあれば利用が可能です。集計データは、スプレッドシートに収集され、データ分析もできます。

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

リダイレクト

プログラムの入力元や出力先を通常とは別の場所に転送させることをリダイレクトと呼びます。

0グッド

0クリップ

投稿2020/01/06 19:30

編集2020/01/14 18:21

環境

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

1<?php 2 3namespace App\Http\Requests; 4 5use Illuminate\Foundation\Http\FormRequest; 6// use App\Http\Requests\Request; 7 8class CreateReserveRequest extends FormRequest 9{ 10 11 public function rules() 12 { 13 return [ 14 'facility_name' => 'not_in:0', 15 'dateinfo' => 'required|date', 16 'start_time' => 'required|date_format:H:i', 17 'end_time' => 'required|date_format:H:i', 18 ]; 19 } 20} 21 22

model

php

1<?php 2 3namespace App; 4 5use Illuminate\Database\Eloquent\Model; 6 7class Reserve extends Model { 8 public $incrementing = false; 9 10 protected $fillable = [ 11 'user_id', 12 'facility_id', 13 'start_time', 14 'end_time', 15 16 ]; 17 18 public function user() { 19 return $this->belongsTo('App\User'); 20 } 21 22 public function facility() { 23 return $this->belongsTo('App\Facility'); 24 } 25 26 static function SearchReserveDates($facility_name, $dateinfo) { 27 28 $result = \App\Reserve::whereHas('Facility', function($query) use($facility_name, $dateinfo) { 29 $query 30 ->where('facility_name',$facility_name) 31 ->whereDate('start_time',$dateinfo) 32 ->whereDate('end_time',$dateinfo); 33 })->select('id', 'start_time', 'end_time')->get()->all(); 34 return $result; 35 } 36}

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

php

1<?php 2 3class ReserveController extends Controller { 4 5 // フォームから受け取った情報をもとにテーブルを検索、施設名から施設IDを引っ張り、日付と合わせて予約情報を取得しAjaxに返す。 6 public function searchReservation(Request $request) { 7 8 $data = $request->all(); 9 10 if(isset($data['dateinfo']) && isset($data['facility_name'])) { 11 12 $dateinfo = $data['dateinfo']; 13 $facility_name = $data['facility_name']; 14 15 $reserveinfo = Reserve::SearchReserveDates($facility_name, $dateinfo); 16 17 \Debugbar::info(); 18 19 return json_encode($reserveinfo, JSON_PRETTY_PRINT); 20 21 } else { 22 echo 'FAIL TO AJAX REQUEST'; 23 } 24} 25 26 // 入力確認画面に最終的なフォームの値を渡す。Requestにはバリデーションの拡張クラスを渡す。 27 public function confirm(CreateReserveRequest $request) { 28 29 // 予約情報取得 30 31 $data = $request->all(); 32 33 if(isset($data['dateinfo']) && isset($data['facility_name'])) { 34 35 $dateinfo = $data['dateinfo']; 36 $facility_name = $data['facility_name']; 37 38 $reserveinfo = Reserve::SearchReserveDates($facility_name, $dateinfo); 39 } 40 41 42 // 取得したレコードを配列の形にする。 43 // start_timeとend_timeそれぞれの値で配列を作る。 44 45 $arr_start_time = array_column($reserveinfo, 'start_time'); 46 // var_dump($arr_start_time); 47 $arr_end_time = array_column($reserveinfo, 'end_time'); 48 // var_dump($arr_end_time); 49 50 // 選択された時間帯が予約時間と重複していないか検証 51 if(isset($data['start_time']) && isset($data['end_time'])) { 52 53 // datetime型に整形 54 $start_datetime =$data['dateinfo'] .' '. $data['start_time']; 55 $end_datetime =$data['dateinfo'] .' '. $data['end_time']; 56 57 // Carbonに整形 58 59 $st = new Carbon($start_datetime); 60 $start = $st->format('Y-m-d H:i:s'); 61 // var_dump($start); 62 $ed = new Carbon($end_datetime); 63 $end = $ed->format('Y-m-d H:i:s'); 64 // var_dump($end); 65 // テーブルから予約情報取得して配列に格納 66 // それぞれ$arr_start_time[]と$arr_end_times[]で呼び出せるようにする 67 // forかwhile文で$start_time[n]と$end_time[n]まで検証する 68 69 // $arr_start_time及び$arr_end_timeの配列の数を取得する。 70 $c1 = count($arr_start_time); 71 $c2 = count($arr_end_time); 72 73 74 // 時間帯比較の関数 75 function isTimeDuplication($start, $end, $start_time, $end_time) { 76 return ($start < $end_time && $start_time < $end); 77 } 78 // try-catchで例外が出たら入力フォームまでロールバックする。 79 80 try { 81 82 for ($i=0; $i < $c1 && $c2 ; $i++) { 83 84 $start_time = $arr_start_time[$i]; 85 $end_time = $arr_end_time[$i]; 86 87 $result = isTimeDuplication($start, $end, $start_time, $end_time); 88 89 if($result === TRUE) { 90 throw new ReserveDuplicationException; 91 } 92 } 93 94 // 入力確認ページのviewにdataを渡す 95 return view('reserve-confirm', [ 96 'data' => $data, 97 ]); 98 99 } catch(ReserveDuplicationException $e) { 100 throw $e; 101 } 102 103 104 } 105} 106 107 // 予約完了メールの発送 108 public function send(Request $request) { 109 110 //フォームから受け取ったactionの値を取得 111 $action = $request->input('action'); 112 $inputs = $request->except('action'); 113 114 //actionの値で分岐 115 if($action !== 'submit'){ 116 return redirect() 117 ->route('reserve.index') 118 ->withInput($inputs); 119 120 } else { 121 $user = Auth::user(); 122 $email = $user->email; 123 \Mail::to($email)->send(new ContactSendmail($inputs)); 124 125 // トークンを再発行して再送信防止 126 127 $request->session()->regenerateToken(); 128 129 } 130 131 } 132 133 134// 予約確定による、データベースへの情報の追加 135 136 public function store(Request $request) { 137 138 // バリデーション 139 140 $request->validate([ 141 'facility_name' => 'required', 142 'dateinfo' => 'required', 143 'start_time' => 'required', 144 'end_time' => 'required', 145 ]); 146 147 148 // 再度ダブルブッキングのチェックを行う。 149 $this->confirm($request); 150 151 // 以下、ダブルブッキングなしの場合の更新処理 152 153 //フォームから受け取ったactionを除いたinputの値を取得 154 $inputs = $request->except('action'); 155 156 // 検索用にfacility_nameをのみ別に変数に取り出しておく 157 158 $facility_name = $inputs['facility_name']; 159 160 // facility_idを抽出するメソッド 161 162 $facility_id = Facility::SearchFacility_id($facility_name); 163 164 165 // dateinfoとstart_time及びend_timeを組み合わせてdatetime型にする。 166 $start_time =$inputs['dateinfo'] .' '. $inputs['start_time']; 167 $end_time =$inputs['dateinfo'] .' '. $inputs['end_time']; 168 169 // 予約番号生成 170 171 $reserve_number = uniqid(bin2hex(random_bytes((1)))); 172 173 // ユーザー情報取得 174 $user = Auth::user(); 175 $user_id = $user->id; 176 177 // データベースに追加 178 $reserve = new Reserve(); 179 $reserve->user_id = $user_id; 180 $reserve->facility_id = $facility_id; 181 $reserve->start_time = $start_time; 182 $reserve->end_time = $end_time; 183 $reserve->reserve_number = $reserve_number; 184 $reserve->save(); 185 186 \Debugbar::info(); 187 188 $this->send($request); 189 190 // viewへ遷移 191 return view('reserve-complete' ,[ 192 'reserve_number' => $reserve_number, 193 ]); 194 } 195

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

####console.log一覧
イメージ説明

####定義しているテーブル
イメージ説明

追記

####Handler.php

php

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

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m.ts10806

2020/01/09 01:56

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

2020/01/09 18:17

m.ts10806様 コメントありがとうございます。 申し訳ない、これに関しては当方PHP・Laravel歴1ヶ月なのでまだまだ混同して覚えてることが多く、今回作ってるものは雛形はあるもののコード自体は自力で試行錯誤しながら書いているものも多いのでこういったことが起きてしまいました。 大変お恥ずかしい限りです。 ご指摘を受け、調べたところLaravelではフォームからの情報の受け取りを$request->input()や直接input()の部分をフォームのname属性を指定してやることで受け取れるということですので今回の場合は$data = $request->all();の形でフォームの内容をすべて取得していますから、個別のものに関しては$data[]の形で書くのが適切でした。 そのように修正しておきましたのでご確認ください。
guest

回答1

0

ベストアンサー

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

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

今回は要件定義に基づき、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/13 01:14

編集2020/01/13 02:19
FKM

総合スコア3647

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kamille-mio

2020/01/14 17:49

FKMさん、回答ありがとうございます。 返信が遅くなって申し訳ありません。 今回、初めて自力でシステム(webアプリ)を作るということで https://tennisfull.com/koutou/godaikameido.html?date=2019/12/3 こちらの利用登録システムを元に自分でやれるレベルまでデチューンしながら制作していて、テーブル設計も初めてだったのですが、やなりそこから甘かったということを痛感しました。 一応、テーブル設計については過去以下のような質問をして得られた回答を元に設計したものになっています。 https://teratail.com/questions/225110 今回は一応動作的には確認できていますが、独立性阻害要因の排除が甘いですし ご指摘の通り、時間帯の最初と最後で保持させるより時間帯を1時間毎に保持させた方がスケジュールの有無は判定させやすいですし、タイムライン周りは今回の件でかなりややこしいということがわかったので反省点として次回の作成に活かしたいと思います。
kamille-mio

2020/01/14 18:23 編集

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

2020/01/15 00: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 このシステムだとこれが通ってしまう危険があるかも
kamille-mio

2020/01/15 09:25

再度、返信ありがとうございます。 本筋とは関係ないところにもご指摘頂けて大変助かります。 確認しましたが前者は利用終了時刻のプルダウンで17時が弾かれるのでそもそも選択ができない(念の為12時~18時で確認しましたがエラーでリダイレクトできました)のと、後者は12~16時で確認したところやはり同様にエラーでリダイレクトを確認しました。 ただ、正直すべての予期せぬ確定処理や脆弱性をテストするのは私にはまだまだ勉強不足で難しいのでFKMさんが危惧された以外のところから通ってしまう危険は否定できないですね……ここが初学者の難しいところです。 一応このまま体裁を整えて成果物としてGithubやherokuにUPしてQiitaに作成記録等々をまとめて他の方に実際に動かしてくれた方や、Qiitaの記事を読んでいただけた方のレビューを待つしかないのかなと思っています。 他力本願で心苦しいところではあります。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問