【Laravel】入力個数を変更できるフォームでレシピ投稿機能をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、私にとって人生を好転させてくれたカナダ留学から少し時間が経ち、「もう一回行きたいな…」と感じる今日この頃です。

そして、昔撮った写真を見ていると、ほぼ毎日「クックパッド」を見ながら自分で料理をしていたことを思い出し、ある機能をつくってみたくなりました。

それが・・・・・・

個数を自由に変更できる入力フォーム

です。

つまり、以下のように自分が欲しい分だけ入力ボックスを用意することができるフォームですね。

というのも、この機能を使って「食材を複数登録できるレシピ投稿機能」をつくりたくなったからなんですね。

ということで、今回はこの「入力個数を変更できるフォーム」でレシピ投稿機能を作ってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「配列用のバリデーションも紹介します👍」

開発環境: Laravel 8.x、Vue 3

データベース構造

今回のレシピ投稿機能をつくるために以下2つのテーブルを用意します。

  • recipes ・・・ レシピ(例:カレー、肉じゃが)
  • ingredients ・・・ 材料(例:じゃがいも、にんじん)

つまり、カレーの例で言うと以下のようになります。

カレー(レシピ)
└ 1.カレールー(材料)
└ 2.じゃがいも(材料)
└ 3.にんじん(材料)
└ などなど

そうです。

これは「recipes:ingredients」が「1:多」のリレーションシップになっているということになりますね。

では、これを踏まえて実際の作業をしていきましょう❗

必要なファイルを用意する

では、今回の機能に必要なファイルつくっていきます。

まずはレシピ用です。
レシピには以下3つが必要になります。

  • モデル
  • マイグレーション
  • コントローラー

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

php artisan make:model Recipe -mc

すると、ファイルが作成されるのですが、中身の変更の前に材料用のコマンドも実行しておきましょう。

材料用に必要なのは、以下2つです。

  • モデル
  • マイグレーション

コマンドは以下になります。

php artisan make:model Ingredient -m

すると、こちらもファイルが作成され合計で5つのファイルが作成されたことになります。

では、ひとつずつ中身を変更していきましょう。

マイグレーションの設定

まずはマイグレーションです。

database/migrations/****_**_**_******_create_recipes_table.php

// 省略

public function up()
{
    Schema::create('recipes', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('料理名');
        $table->timestamps();
    });
}

database/migrations/****_**_**_******_create_ingredients_table.php

// 省略

public function up()
{
    Schema::create('ingredients', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('recipe_id')->comment('レシピID');
        $table->string('name')->comment('材料名');
        $table->timestamps();

        $table->foreign('recipe_id')->references('id')->on('recipes');
    });
}

ちなみに、この中ではrecipe_idに外部キーをセットしています。

では、マイグレーションの設定が完了したら、実際にデータベースへ反映させておきましょう。

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

php artisan migrate

すると実際のテーブルはこうなりました。

モデルの設定

次にモデルですが、今回設定が必要なのはRecipe.phpだけです。

// 省略

class Recipe extends Model
{
    use HasFactory;

    // Relationship
    public function ingredients()
    {
        return $this->hasMany(Ingredient::class, 'recipe_id', 'id');
    }
}

データベース構造」でも紹介したとおり「1:多」の関係になるので、hasMany()を使っています。

コントローラーの設定

続いてコントローラーです。

app/Http/Controllers/RecipeController.php

<?php

namespace App\Http\Controllers;

use App\Models\Ingredient;
use App\Models\Recipe;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class RecipeController extends Controller
{
    public function show(Recipe $recipe)
    {
        // TODO: ここに表示用ロジックをつくる(今はテスト)

        $recipe->load('ingredients');
        dd($recipe->toArray());
    }

    public function create()
    {
        return view('recipe.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'ingredients' => ['required'],
            'ingredients.*' => ['required', 'distinct'],
        ]);

        $result = false;

        DB::beginTransaction();

        try {

            $recipe = new Recipe();
            $recipe->name = $request->name;
            $recipe->save();

            foreach ($request->ingredients as $ingredient_name) {

                $ingredient = new Ingredient();
                $ingredient->recipe_id = $recipe->id;
                $ingredient->name = $ingredient_name;
                $ingredient->save();

            }

            DB::commit();
            $result = true;

        } catch (\Exception $e) {

            DB::rollBack();

        }

        return ['result' => $result];
    }
}

この中で重要なのが、以下のバリデーション部分です。

  • ‘ingredients’ => [‘required’]
  • ‘ingredients.*’ => [‘required’, ‘distinct’]

まず1つ目は、

配列がゼロ個ならアウト❗

という意味になります。
つまり、配列の件数をチェックしているわけですね。

そして、2つ目は、

  • 配列の各データ(今回は材料名)が空白ならアウト❗
  • さらに、中身が他と重複してもアウト❗❗

という意味になります。

つまり、同じrequiredですが、少し意味合いが違っていますので注意してください。

なお、*はワイルドカードといって「トランプのジョーカーのように何にでもなれる」ので、つまりは配列の全データが対象ということになります。

例: ingredients[0], ingredients[1], ingredients[2], ingredients[3], ….. 全てのデータが対象

そして、以下コードの「トランザクション」を使っている部分も重要です。

  • DB::beginTransaction()
  • DB::commit()
  • DB::rollBack()

トランザクションとは、シンプルに言うと「全部完了できたら実際にデータベースの書き換えをするよ」というものです。

つまり、以下2つの部分ではデータ保存していると思いきや、これはまだ「一時的な保存」の状態です。

  • $recipe->save();
  • $ingredient->save();

では、この「一時的な保存」を「本番の保存」にするにはどうすればいいかというと、それがDB::commit()になります。(結果にコミット!ですね😄)

逆に、「一時的な保存」を全てなかったことにするにはDB::rollBack()を使います。

では、なぜこんなめんどうなことをするかというと、「途中まで保存できてたのに、それ以降はムリだった。ゴメン!」より「途中失敗したから、全部なかったことにしときます(キリッ)」の方がDBに変なデータが入ってこないからです。

ビューをつくる

次に、コントローラーでセットしたビュー(HTMLテンプレート)をつくります。

resources/views/recipe/create.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-4">
    <h1 class="mb-4">レシピ投稿機能のサンプル</h1>
    <div class="row">
        <div class="col-4">
            <div class="mb-4">
                <label>料理名</label>
                <input type="text" class="form-control" v-model="params.name">
            </div>
            <div class="mb-3">
                <div class="float-end">
                    <button type="button" class="btn btn-outline-primary btn-sm" @click="addIngredient">+</button>
                </div>
                <label>食材<small>(<span v-text="params.ingredients.length"></span>件)</small></label>
            </div>
            <div class="mb-4">
                <div class="position-relative mb-3" v-for="(ingredient,index) in params.ingredients">
                    <input type="text" class="form-control" v-model="params.ingredients[index]">
                    <div class="position-absolute" style="right:10px;top:8px;">
                        <small>
                            <a href="#" type="button" @click="removeIngredient(index)">削除</a>
                        </small>
                    </div>
                </div>
            </div>
            <div class="text-end pt-2">
                <button type="button" class="btn btn-primary btn-lg" @click="onSubmit">保存する</button>
            </div>
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://unpkg.com/vue@3.1.1/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                params: {
                    name: '',
                    ingredients: []
                }
            }
        },
        methods: {
            addIngredient() {

                this.params.ingredients.push('');

            },
            removeIngredient(removingIndex) {

                this.params.ingredients.splice(removingIndex, 1);

            },
            onSubmit() {

                if(confirm('保存します。よろしいですか?')) {

                    const url = '{{ route('recipe.store') }}';

                    axios.post(url, this.params)
                        .then(response => {

                            if(response.data.result === true) {

                                alert('保存が完了しました。');

                            }

                        })
                        .catch(error => {

                            // TODO: ここでエラー処理をする
                            console.log(error.response.data);
                            alert('入力エラーがありました');

                        });

                }

            }
        }
    }).mount('#app');

</script>
</body>
</html>

この中では、各パラメータをVueでリアクティブに変更しているだけです。

なお、splice()インデックス番号で指定することで配列のデータを削除する関数です。覚えておくと便利ですよ👍

ルートをつくる

そして、最後にルートです。

routes/web.php

use App\Http\Controllers\RecipeController;

Route::get('recipe/create', [RecipeController::class, 'create'])->name('recipe.create');
Route::get('recipe/{recipe}', [RecipeController::class, 'show'])->name('recipe.show');
Route::post('recipe', [RecipeController::class, 'store'])->name('recipe.store');

これで作業は完了です❗

お疲れ様でした😄✨

テストしてみる

では、実際にテストしてみましょう❗
まずは「http://******/recipe/create」にブラウザでアクセスします。

すると、以下のようなフォームが表示されます。

では、「」ボタンを4回クリックして入力項目を増やしてみましょう。

すると・・・・・・

食材の入力項目が増えました。
では、それぞれ以下のように入力します。

では、「保存する」ボタンをクリックして送信してみましょう。
すると、以下のように完了メッセージが表示されました。

成功です❗

では、先ほど作成したデータがキチンと入っているかも確認しておきましょう。
http://******/recipe/1」にアクセスしてください。

すると・・・・・・

はい❗
キチンとリレーションシップした形でデータが表示されました。

全て成功です😄✨

企業様へのご提案

今回のような「入力個数を変更できるフォーム」を使えば、自社製品のグループ管理だけでなくプロジェクト・チームのメンバー管理、お客様のご家族構成など様々な機能に応用することができます。

もしご相談になりたいことがございましたら、いつでもお気軽にご相談ください。お待ちしております。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は自由に個数を変更できる入力フォームを使ってレシピ投稿機能をつくってみました。

ちなみに今回バリデーションで「distinct(重複禁止)」があるのを思い出しました。

過去に勉強しても、実際に使っていないとどうしても忘れてしまいますが、「どこを見ればいいか」だけ覚えておけばなんとかなるもんですね。

※ なお、私がよく見るのはやはり公式ドキュメントです。

最近はLaravelの「こんな機能があるよ」というような記事は書いてないこともあって、その後たっくさん新機能が追加されていることに気づきました。

また時間を見つけて「えっ、めちゃくちゃ楽になってるじゃん❗」を連発したいものです(笑)

皆さんもぜひやってみてくださいね。

ではでは〜❗

「ド庶民なので、レトルトがカレーの最高峰です」

このエントリーをはてなブックマークに追加       follow us in feedly