0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel 6.x 非同期通信(Ajax) 【axios】 【Vue.js】 【Laravel-mix】 で簡易的なECサイトのカート(買い物かご)を作成

Last updated at Posted at 2020-12-15

制作環境

Windows 10
Laravel : 6.18.35
Laravel/ui : 1.0
Laravel-mix : 5.0.1
Bootstrap : 4.0.0
Vue.js : 2.5.17
XAMPP
PHP : 7.4.3
Visual Studio Code

はじめに

この記事はプログラミングをはじめたばかりの素人が、できたことをメモするのに利用しています。
内容には誤りがあるかもしれません。

買い物かごを作成する際にうまくいかなかったので、改善策を見つけるのに作成した小規模プログラムです。
とりあえず完成した物を先に紹介し、後ほどうまくいかず試行錯誤した点を記載したいと思います。

機能実装が目的のため、デザイン(見た目)にはあまりこだわっていません。
また、記述も必要最低限にしています。
一部デザインの整形にBootstrapを使用しております。
Bootstrap、Vue.jsも含め、Laravel-mixを使用して記述してます。

作成要件

  • ユーザーの切り替えができる(ログイン機能を付けないので、手動でユーザーを切り替える仕様にします)。
  • 商品の情報を取得し、一覧表示する。
  • 商品の購入数を指定し、非同期通信でカートに追加できる。
  • カートに入っている商品点数を、非同期通信で表示する。
  • カートに追加する際はバリデーションを行い、エラーがあればメッセージを表示させる。
  • カートへの登録が完了したら、購入数は空に戻す。
  • ユーザーを切り替えるとカートに入っている商品点数も、非同期通信で変更される。

イメージ

メインページ
デフォルト.jpg

ユーザー選択でユーザーの切り替え
user.jpg

カート内の点数は()の中に表示させます
incart.jpg

購入数の指定ができます
post.jpg

カートに追加後は購入数を空に戻します
afterpost1.jpg

カートには点数が追加されます
afterpost2.jpg

ユーザーに合わせて表示されるカートの件数も変わります
userincart.jpg

バリデーションも実装します
validation.jpg

バリデーションでエラーがあればメッセージを表示します
errormsg.jpg

それでは、作成していきましょう。

マイグレーションファイルの作成

データベースへテーブルを作成したいと思います。
今回作成するのは、商品を管理するproducts_tableとカート用のcarts_tableです。

プロジェクトのディレクトリでターミナルを起動し、以下を実行してください。

php artisan make:migration create_products_table

続けて

php artisan make:migration create_carts_table

作成が完了したら、database>migrations内にある作成されたファイルを開き、以下のように記述します。

create_products_table
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name')->comment('商品名');
            $table->string('price')->comment('価格');
            $table->timestamps();
        });
    }
create_carts_table
    public function up()
    {
        Schema::create('carts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedTinyInteger('user_id')->comment('ユーザーID');
            $table->unsignedBigInteger('product_id')->comment('商品ID');
            $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
            $table->string('quantity')->comment('購入数');
            $table->timestamps();
        });
    }

モデルの作成

次にモデルを作成していきます。
モデルはModelsフォルダの中に作成していきます。

ターミナルを起動し、以下を実行してください。

php artisan make:model Models/Product

続けて

php artisan make:model Models/Cart

作成されたファイルはapp>Modelsの中にあります。
各ファイルを以下のように記述してください。

Product.php
// リレーションのため追記
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Product extends Model
{
    // 変更を許可するカラムを指定します
    protected $fillable = [
        'name', 'price'
    ];

    // リレーションのためのメソッドです
    // これでカートの情報がProduct側から取得できます
    public function cart()
    {
        // 紐付けるモデルを指定し返します
        return $this->belongsTo('App\Models\Cart');
    }
}
Cart.php
// リレーションのため追記
use Illuminate\Database\Eloquent\Relations\HasMany;

class Cart extends Model
{
    protected $fillable = [
        'user_id', 'product_id', 'quantity'
    ];

    public function product()
    {
        return $this->hasMany('App\Models\Product');
    }
}

ビューの作成

resources>views内に新しくproduct.blade.phpを作成し、以下のように記述します。
ちなみに、Vueテンプレートは一切使用していません。

product.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link rel="stylesheet" href="{{ mix('css/app.css') }}">
    <title>商品一覧</title>

    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
    <div id="app" v-cloak>
        <div class="container">

            <div class="row mt-2">
                <label>ユーザー選択
                    <select name="user" id="user" v-model="user">
                        <option value="1">1</option>
                        <option value="2">2</option>
                        <option value="3">3</option>
                    </select></label>
                <p class="cart_text ml-auto">◆カートの中身(@{{ items }})</p>
            </div>

            <h1>商品一覧</h1>

                <p class="err_msg text-danger">@{{ errors.quantity }}</p>

            <div class="row justify-content-center">

                    @foreach ($productTable as $product)

                    <div class="product p-3 border border-success col-3">

                        <h3>商品名</h3>
                        <p>{{ $product->name }}</p>

                        <h3>価格</h3>
                        <p>{{ $product->price }}円</p>
                            <form id="form{{ $product->id }}">
                                @csrf
                                <label>購入数: <input type="text" name="quantity" size="2"></label>
                                <br>
                                <input type="hidden" name="product_id" value="{{ $product->id }}">
                                <input type="hidden" name="user_id" v-model="user">
                                <button type="button" @click="addCart({{ $product->id }})">カートに追加</button>
                            </form>

                    </div>

                    @endforeach

            </div>

        </div>
    </div>

    <script src="{{ mix('js/app.js') }}"></script>
</body>
</html>

ポイント

{{ $product->name }}

のように表示されているのはblade側の構文で

@{{ items }}

のように@が先頭についているのはVue側の構文です。

    <style>
        [v-cloak] {
            display: none;
        }
    </style>

ここで設定しているスタイルは、Vue側の構文を表示する際に一瞬{{ }}が表示されるのを防ぐためのものです。

フォームリクエストの作成

バリデーションはフォームリクエストで行うようにします。
ターミナルで以下を実行して下さい。

php artisan make:request CartRequest

app>Http>Requests内にファイルが作成されるので、開いて以下のように記述します。

CartRequest.php
class CartRequest extends FormRequest
{
    public function authorize()
    {
        // 今回認証は行わないのでtrueにします
        return true;
    }


    // 正規表現で正の整数だけパスするようにしてます
    public function rules()
    {
        return [
            'quantity' => 'regex:/^\d+$/'
        ];
    }


    public function messages()
    {
        return [
            'quantity.regex' => '購入数は正の整数を入力してください'
        ];
    }
}

ポイント

今回は非同期通信の中でフォームリクエストを使用しバリデーションを行いますが、通常のバリデーション処理と違い注意点があります。
通常であれば自動的に元のページへのリダイレクトレスポンスが作成され、エラーメッセージもフラッシュメッセージとしてセッションに保存されますが、非同期通信の場合はJSONが返されるだけで、レスポンスの作成は行われず、リダイレクトもしません。

コントローラの作成

ターミナルで以下を実行します。

php artisan make:controller ProductController

app>Http>Controllers内にファイルが作成されるので、開いて以下のように記述します。

ProductContoroller
// モデル利用のため追記
use App\Models\Product;
use App\Models\Cart;
// フォームリクエスト使用のため追記
use App\Http\Requests\CartRequest;

class ProductController extends Controller
{
    public function index()
    {
        // 商品情報を全て取得します
        $productTable = Product::all();
        // 取得した内容をビューに渡します
        return view('product', compact('productTable'));
    }

    // カートに商品を追加するメソッドです
    public function add_cart(CartRequest $request)
    {
        // フォームリクエストを通過したリクエストの値を全て$formに代入します
        $form = $request->all();
        // 不要な項目を削除します
        unset($form['_token']);

        // Cartモデルをインスタンス化(実体化)します
        $cartTable = new Cart;
        // 登録する値を各項目に一気に代入します
        $cartTable->fill($form);

        // Cartテーブルにデータを保存します
        $cartTable->save();

        // カートからユーザーIDが同じ物だけ抽出して数をカウントします
        $cart = $cartTable->where('user_id', $request->user_id)->count();
        // カウントした数を返します
        return $cart;
    }

    // カートの商品点数をカウントするメソッドです
    public function get_total(Request $request)
    {
        // カートからユーザーIDが同じ物だけ抽出して数をカウントして返します
        $cart = Cart::where('user_id', $request->user_id)->count();
        return $cart;
    }
}

ルーティングの作成

routes内のweb.phpを開いて以下のように記述します。

web.php
Route::get('/product', 'ProductController@index')->name('product');
Route::post('/ajax/product', 'ProductController@add_cart')->name('add_cart');
Route::get('/ajax/product', 'ProductController@get_total')->name('cart_total');

Vue.jsの作成

resources>js内のapp.jsを開き、下の方を以下のように記述します。

app.js
// Vueをインスタンス化(実体化)しappに代入します
const app = new Vue({
    // Vueを使用する範囲(仮想DOM)を指定します
    el: '#app',

    // 初期値で渡す値を設定します
    data() {
        return {
            // 現在選択されているユーザーです
            user: '',
            // カートの商品点数です
            items: '',
            // バリデーションのエラーメッセージです
            errors: {},
        }
    },
    methods: {
        // カートに商品を非同期通信で追加するメソッドです
        addCart(id) {
            // アクセス先のURLを作成しurlに代入します
            let url = '/ajax/product'
            // アクセス先に送信するデータをparamsに代入します
            let params = $('#form' + id).serialize()

            // thisが使えなくなるのでthatに代入し使えるようにします
            let that = this

            // エラーメッセージを初期化します
            that.errors = {}

            // axiosで非同期通信を開始します
            axios.post(url, params)
                // thenで通信成功時の処理を記載します
                // コントローラからの返り値がresに代入されます
                .then(res => {
                    // コントローラからの返り値(商品点数)をitemsに代入します
                    that.items = res.data
                    // 購入数の値を空に戻します
                    $('#form' + that.user)[0].reset()

                // catchで通信失敗又はバリデーションエラー時の処理を記載します
                // フォームリクエストからの返り値がerrorに代入されます
                }).catch(error => {
                    // ここで使用する変数errorsを定義します
                    var errors = {}

                    // for...in分でキーの数だけ処理を繰り返します
                    for (var key in error.response.data.errors) {
                        // errorsにキーと値を代入します
                        errors[key] = error.response.data.errors[key].join()
                    }
                    // errorsに抽出したエラーメッセージを代入します
                    that.errors = errors
                })
        },
    },
    // watchで値の変更の監視を行います
    watch: {
        // userの値が変更された(ユーザーを切り替えた)時の処理です
        user: function() {
            // アクセス先のURLを作成しurlに代入します
            let url = '/ajax/product?user_id=' + this.user

            // thisが使えなくなるのでthatに代入し使えるようにします
            let that = this

            // エラーメッセージを初期化します
            that.errors = {}

            // axiosで非同期通信を開始します
            axios.get(url)
                .then(res => {
                    // resで受け取ったコントローラの返り値(商品点数)をitemsに代入します
                    that.items = res.data
                })
        }
    }
})

ポイント

errors[key] = error.response.data.errors[key].join()

最後の.join()が抜けると、エラーメッセージの表示が

購入数は正の整数を入力してください

ではなく、

["購入数は正の整数を入力してください"]

と、余計なものが表示されます。

コンパイル

ここまで記述したら、最後にコンパイルを行います。
ターミナルで以下を実行してください。

npm run dev 又は npm run watch-poll

動作確認

/productにアクセスし、動作を確認してみてください。
要件が全て満たされていたら成功です。

作成時にハマった点

ユーザー切り替え時のカートの点数の取得

最初Vueには以下のように記述を書いていました。

    beforeUpdate() {
        let url = '/ajax/product?user_id=' + this.user

        let that = this

        axios.get(url)
            .then(res => {
                that.items = res.data
            })
    },

この記述だと、ユーザー変更時にカート内の商品点数を取得してはくれるのですが、同じ処理が2回行われてしまいます。
先ず、ユーザーが切り替わったことで変更とみなされ、処理が走ります(1回目)。
次に、itemsに値が入ることで変更とみなされ、処理が走ります(2回目)。

2回目の後に再度itemsに値が代入されますが、値が全く同じなので、変更とみなされず処理は走りません。

最終的にwatchを使用し、監視する項目を指定することでうまくいきました。

バリデーションのエラーメッセージの取得

これが一番ハマリました。
最初は@errorディレクティブを使用し、エラーメッセージを表示するようビューに記載していたのですが、非同期通信の場合リダイレクト処理は行われないので、通常フラッシュメッセージとしてセッションに保存されるエラーメッセージが受け取れませんでした。

改善策としてセッションに手動でエラーメッセージを保存しようと試みましたが、まず非同期通信の為ページが更新されないので、セッションに保存したところで反映されませんでした。
一部のDOMだけを更新させることも考えましたが、思う通りにできる記述方法を見つけることができませんでした。
また、色々名称等を試しましたが、セッションにどういうキーで、どうい形で、どんな値が保存されているのかわからず、@errorディレクティブを動かすことができませんでした。

最終的に@errorディレクティブの使用は諦めました。

Vueでのバリデーションのエラーメッセージの取得

最初エラーメッセージを表示させて際、ポイントで記載していますが、余計なものが表示されてしまいました。
["購入数は正の整数を入力してください"]
[""]が不要です。

console.logで受け取ったデータを確認したところ、["購入数は正の整数を入力してください"]この表示の他に、[0]で購入数は正の整数を入力してくださいという値があるのがわかりました。

そこで、以下のように記述を変更しうまくいきました。

errors[key] = error.response.data.errors[key][0]

うまくいきはしたのですが、どうしても[0]がの記述が気になりました。
今回バリデーションルールが1つしかないからいいものの、複数の場合に大変そうで、更に色々探して最終的に掲載しているjoin()を使うかたちにしました。

カートの件数のカウント

これも結構ハマリました。

以下うまくいかなかったコントローラの記述です。

$cart = $cartTable->find($request->user_id)->count();

カートの点数が常に1になります。
find()では1件の値しか取れていないようです。

$cart = $cartTable->find($request->user_id)->get()->count();

テーブルのレコード全件がカウントされてしまいます。
find()で抽出しても、get()がくると全部抽出されてしまいます。

最終的にwhereで条件を指定しうまくいきました。
個人的にはfind()でいけると思ったのですが・・・

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?