【Laravel】1回のsubmitで親と子のデータを同時に送信、保存する方法
■はじめに
WEBエンジニア1年目のゆりです。
今回は1回のsubmitで親と子のデータを同時に送信、保存する方法をご紹介したいと思います。
↓下記でお悩みの方におすすめの記事
→全て1つのsubmitボタンで実装可能です!!!図で表すとこんな感じ。
親テーブルのcreateページに親テーブルのフォームと子テーブルのフォームを用意しました。
では、早速実装していきましょう。
完成したソースコードだけ見たい方は、
右の目録からて「完成したソースコード」をクリックして解説を飛ばしてください。
■開発環境
・MacOS Catalina
・PHP:7.3.11
・Laravel:8.49.2
・MySQL:8.0
■手順
①マイグレーションファイルの作成
②親と子のルートを記述
③それぞれモデルに親と子をリレーションを記述
④createページを作成
⑤親テーブル用のフォームを作成する
⑥取得したレコードを保存する(親)→コントローラ
⑦子テーブル用のフォームを作成する
⑧取得したレコードを保存する(子)
⑨詳細ページで取得したデータを表示する→コントローラ
⑩詳細ページで取得したデータを表示させる→ビュー
■実装&解説
①マイグレーションファイルの作成
userテーブルのマイグレーションは割愛させていただきます
①-1 Questionテーブル(親)のマイグレーションファイルを作成
$ php artisan migrate:make create_questions_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateQuestionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('questions', function (Blueprint $table) {
$table->id();
$table->integer('user_id')->comment('ユーザーID');
$table->string('title', 50)->comment('題名');
$table->string('contents', 2000)->nullable()->comment('内容');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('questions');
}
}
①-2 voteテーブル(子)のマイグレーションファイルを作成
$ php artisan migrate:make create_votes_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateVotesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->integer('question_id')->comment('質問ID');
$table->string('vote')->comment('選択肢');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('votes');
}
}
①-3 migrateを実行する
$ php artisan migrate
②親と子のルートを記述
※今回の実装とは関係のないルートは省略します
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Auth::routes();
// Question(親のルート)
// 投稿ページを表示させるためのルート
Route::get('/questions/create', [App\Http\Controllers\QuestionController::class, "create"])
->middleware('auth')->name("questions.create");
// formから送信されたデータを保存するルート
Route::post('/questions/store', [App\Http\Controllers\QuestionController::class, "store"])
->middleware('auth')->name("questions.store");
・middleware('auth')はログインしているユーザーのみ、アクセスや動作を許すための記述(ログインしていないユーザーは自動的にログイン画面に遷移します)
・name("questions.create")は名前付きルートを指定したいときに記述
「名前付きルート」につきましてはこちらをご確認ください。https://readouble.com/laravel/5.6/ja/routing.html
③それぞれモデルに親と子をリレーションを記述
③-1 親テーブルのモデルにhasManyで子テーブルとリレーション(1→多)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Vote; //ここを記述
class Question extends Model
{
use HasFactory;
protected $fillable = [
'id',
'title',
'contents',
'user_id',
];
// ここから下を記述
public function vote()
{
return $this->hasMany(Vote::class);
}
}
③-2 子テーブルのモデルにbelongToで親テーブルとリレーション (多→1)
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Question; // ここを記述
class Vote extends Model
{
protected $fillable = [
'vote',
'question_id',
];
// ここから下を記述
public function question()
{
return $this->belongsTo(Question::class);
}
}
これでquestionテーブルとvoteテーブルの親子関係が確立されました。
④createページを作成
questionテーブル(親)のコントローラでcreateアクションを作成します。
createアクションは質問ページを表示させたいだけなので、下記の1行でOKです。
public function create()
{
return view('questions.create');
}
⑤親テーブル用のフォームを作成する
<form method="POST" action="{{ route('questions.store') }}">
@csrf
<div class="form-group">
<label for="title">
タイトル
</label>
<input type="text" name="title" class="form-control">
</div>
<div class="form-group">
<label for="subject">
内容
</label>
<textarea type="text" name="contents" class="form-control"></textarea>
</div>
</form>
name属性 は、要素に名前を付与します。
フォームから送られたデータをコントローラで受け取る際に使用するので重要です。
取得するカラム名で命名すると無難です。
type属性 は、フォームの種類を指定する際に使用します。
指定するtype属性により一行テキストボックス・チェックボックス・ラジオボタン・送信ボタン・リセットボタンなど種類を指定することが可能です。
どのような種類があるかは下記をご確認ください。
⑥取得したレコードを保存する(親)
<?php
namespace App\Http\Controllers;
use App\Models\Question; //ここを追加
use App\Models\Vote; //ここを追加
use Illuminate\Support\Facades\DB; //ここを追加
use Illuminate\Http\Request;
use App\Http\Requests\QuestionRequest; //ここを追加
class QuestionController extends Controller
{
public function store(QuestionRequest $request)
{
DB::beginTransaction();
try{
$question = new Question($request->get('question',[
'title' => $request->title,
'contents' => $request->contents,
'user_id' => auth()->user()->id,
]));
$question->save();
}catch(Exception $e){
DB::rollback();
return back()->withInput();
}
DB::commit();
return redirect(route('questions.index'));
}
}
⑥-1storeアクションを作成
public function store(QuestionRequest $request)
{
//
}
⑥-2 処理開始(トランザクション開始)
public function store(QuestionRequest $request)
{
DB::beginTransaction();
}
DB::beginTransaction()でDBへ保存する処理を開始する合図をします。
⑥-3 取得したリクエストデータを取り出し、データベースに保存
public function store(QuestionRequest $request)
{
DB::beginTransaction();
try{
$question = new Question($request->get('question',[
'title' => $request->title,
'contents' => $request->contents,
'user_id' => auth()->user()->id,
]));
$question->save();
}
}
③リクエストから送られてきたデータをQuestionインスタンスに格納
④ 'formのname属性'=> $request->格納したいカラム名, で記述する
⑤取得した親テーブルのレコードを保存する
⑥-4 処理が成功した場合(commit)、処理が失敗した場合(rollback)を記述
public function store(QuestionRequest $request)
{
DB::beginTransaction();
try{
$question = new Question($request->get('question',[
'title' => $request->title,
'contents' => $request->contents,
'user_id' => auth()->user()->id,
]));
$question->save();
} catch(Exception $e){ // ⑥
DB::rollback();
return back()->withInput();
}
// ⑦
DB::commit();
return redirect(route('questions.index'));
}
⑥失敗した場合、処理を取り消し破棄(ロールバック)するように指定。
バリデーションがエラーで戻った時に値を保持しておきたい時は最後にwithInput() を加えておく
⑦処理が成功した場合、処理を確定する(コミット)入力をする。成功したら質問一覧にリダイレクトするように指定する
⑦子テーブル用のフォームを作成する
<form method="POST" action="{{ route('questions.store') }}">
@csrf
<div class="form-group">
<label for="title">
タイトル
</label>
<input type="text" name="title" class="form-control">
</div>
<div class="form-group">
<label for="subject">
内容
</label>
<textarea id="name" type="textarea" name="contents" class="form-control"></textarea>
</div>
<!-- ここから子フォーム記述 -->
<div class="form-group">
<label for="subject">
選択肢1
</label>
<input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">
</div>
<div class="form-group">
<label for="subject">
選択肢2
</label>
<input class="form-control" name="vote[1][vote]" type="text" id="vote" value="{{old('vote')}}">
</div>
<!-- ここまで子フォーム記述 -->
<a class="btn btn-secondary" href="{{ route('questions.index') }}">
キャンセル
</a>
<button type="submit" class="btn btn-primary">
投稿する
</button>
</form>
<input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">
同じカラムで複数のデータを送りたい場合は、
name属性に配列を記述すると同時にコントローラへデータを送信できる。
⑧取得したレコードを保存する(子)
public function store(QuestionRequest $request)
{
DB::beginTransaction();
try{
$question = new Question($request->get('question',[
'title' => $request->title,
'contents' => $request->contents,
'user_id' => auth()->user()->id,
]));
$question->save();
// ここから下を記述
$votes = $request->all(); // ①
// ②
foreach ($votes['vote'] as $vote) {
foreach ($vote as $key => $value) {
$data = [
'vote' => $value,
'question_id' => $question->id,
];
// ③
$vote = Vote::insert($data);
}
// ここまでを記述
}
}catch(Exception $e){
DB::rollback();
return back()->withInput();
}
DB::commit();
return redirect(route('questions.index'));
}
①取得したリクエストデータを全て取得(この時点では配列が2つ取得されている)
②取得した配列をforeachで分解し、voteカラムと**$question->save();** でリレーションされている質問カラムのidをを取得する
③取得した複数のレコ-ドをinsert() でVoteテーブルのデータベースにまとめて保存する(bulk insert)
・createメソッド、saveメソッド、insertメソッドの違いはこちら
・bulk insertの説明
⑨詳細ページで取得したデータを表示する(コントローラ)
public function show($id)
{
$question = Question::find($id); //①
$votes= $question->vote; // 親に紐づいたvoteのテーブルを取得
return view('questions.show', compact('question', 'votes'));
}
① 親テーブルのデータをidで取得
② ①に紐づいているvoteテーブルのデータを取得
⑩詳細ページで取得したデータを表示させる(ビュー)
レイアウトはまだ完成してないので殺風景です(笑)
投票機能を作成してる例なのでformが用意されてます。
<!-- 親テーブルのデータを表示 -->
{{ $question->id }}
{{ $question->contents }}
{{ $question->user->name }}
<!-- /親テーブルのデータを表示 -->
<form method="post" action="{{ route('votes.vote')}}">
@csrf
@method('put')
<!-- 子テーブルのデータを表示 -->
@foreach ($votes as $vote)
<div class="form-check">
<input name="vote" value="{{ $vote->vote }}" type="radio">
<label class="form-check-label" for="{{ $vote->vote }}">{{ $vote->vote }}</label>
</div>
@endforeach
<!-- 子テーブルのデータを表示 -->
<button type=”submit” class="btn btn-danger btn-primary">投票する</button>
</form>
子テーブルのデータにつきましては同じカラムで複数の値を表示させたいので
value="{{ $vote->vote }}"にし、foreachで値を順番に取得させています。
実際にフォームを使用し、取得、保存、表示できるか確認
フォームに値を入力し、「投稿する」をクリック
データが無事表示されました!
■完成したソースコード
ルート
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
// Question(親のルート)
Route::get('/questions/create', [App\Http\Controllers\QuestionController::class, "create"])
->middleware('auth')->name("questions.create");
Route::post('/questions/create', [App\Http\Controllers\QuestionController::class, "store"])
->middleware('auth')->name("questions.store");
コントローラ
<?php
namespace App\Http\Controllers;
use App\Models\Question;
use App\Models\Vote;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use App\Http\Requests\QuestionRequest;
class QuestionController extends Controller
{
public function create()
{
return view('questions.create');
}
public function store(QuestionRequest $request)
{
DB::beginTransaction();
try{
$question = new Question($request->get('question',[
'title' => $request->title,
'contents' => $request->contents,
'user_id' => auth()->user()->id,
]));
$question->save();
$votes = $request->all();
foreach ($votes['vote'] as $vote) {
foreach ($vote as $key => $value) {
$data = [
'vote' => $value,
'question_id' => $question->id,
];
$vote = Vote::insert($data);
}
}
}catch(Exception $e){
DB::rollback();
return back()->withInput();
}
DB::commit();
return redirect(route('questions.index'));
}
public function show($id)
{
$question = Question::find($id);
$votes= $question->vote;
return view('questions.show', compact('question', 'votes'));
}
}
?>
ビュー(投稿ページ)
<form method="POST" action="{{ route('questions.store') }}">
@csrf
<div class="form-group">
<label for="title">
タイトル
</label>
<input type="text" name="title" class="form-control">
</div>
<div class="form-group">
<label for="subject">
内容
</label>
<textarea id="name" type="textarea" name="contents" class="form-control"></textarea>
</div>
<!-- ここから子フォーム記述 -->
<div class="form-group">
<label for="subject">
選択肢1
</label>
<input class="form-control" name="vote[0][vote]" type="text" id="vote" value="{{old('vote')}}">
</div>
<div class="form-group">
<label for="subject">
選択肢2
</label>
<input class="form-control" name="vote[1][vote]" type="text" id="vote" value="{{old('vote')}}">
</div>
<!-- ここまで子フォーム記述 -->
<a class="btn btn-secondary" href="{{ route('questions.index') }}">
キャンセル
</a>
<button type="submit" class="btn btn-primary">
投稿する
</button>
</form>
ビュー(詳細ページ)
<!-- 親テーブルのデータを表示 -->
{{ $question->id }}
{{ $question->contents }}
{{ $question->user->name }}
<!-- /親テーブルのデータを表示 -->
<form method="post" action="{{ route('votes.vote')}}">
@csrf
@method('put')
@foreach ($votes as $vote)
<!-- 子テーブルのデータを表示 -->
<div class="form-check">
<input name="vote" value="{{ $vote->vote }}" type="radio">
<label class="form-check-label" for="{{ $vote->vote }}">{{ $vote->vote }}</label>
</div>
@endforeach
<!-- 子テーブルのデータを表示 -->
<button type=”submit” class="btn btn-danger btn-primary">投票する</button>
</form>
以上です。
最後までお読みいただきありがとうございました。
記事変更履歴
2021/08/11 記事の内容とは関係のないルートがあったため削除(子テーブルのルート)
参考サイト
Discussion