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

#制作環境

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/bc4b5ac4-62d1-e460-8995-1680acb137df.jpeg)

ユーザー選択でユーザーの切り替え
![user.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/a40b0443-f5df-143c-dccf-1a74c1da38a9.jpeg)

カート内の点数は()の中に表示させます
![incart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/3188efcd-d49c-30f9-7be0-85835aa2bc05.jpeg)

購入数の指定ができます
![post.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/ef813b3d-70d7-94e4-0e9d-2a547e00a843.jpeg)

カートに追加後は購入数を空に戻します
![afterpost1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/63dc2816-9850-1660-83cd-186517626fab.jpeg)

カートには点数が追加されます
![afterpost2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/dca6791f-bf99-fba4-1efc-8b94b4d3a3ae.jpeg)

ユーザーに合わせて表示されるカートの件数も変わります
![userincart.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/f345a960-e019-966b-7657-231ec92d4444.jpeg)

バリデーションも実装します
![validation.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/7e32d21d-3ccb-3a3a-cf45-f19dc75c596f.jpeg)

バリデーションでエラーがあればメッセージを表示します
![errormsg.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760252/037c0fae-6df1-a029-7c4a-4faa6aaa2077.jpeg)

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

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

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

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

```PHP
php artisan make:migration create_products_table
```

続けて

```PHP
php artisan make:migration create_carts_table
```

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

```PHP: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();
        });
    }
```

```PHP: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
php artisan make:model Models/Product
```

続けて

```PHP
php artisan make:model Models/Cart
```

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

```PHP: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');
    }
}
```

```PHP: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`を作成し、以下のように記述します。
<font color="blue">ちなみに、Vueテンプレートは一切使用していません。</font>

```PHP: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>
```

###<font color="red">ポイント</font>

```PHP
{{ $product->name }}
```

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

```PHP
@{{ items }}
```

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

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

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

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

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

```PHP
php artisan make:request CartRequest
```

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

```PHP: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' => '購入数は正の整数を入力してください'
        ];
    }
}
```

###<font color="red">ポイント</font>

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

#コントローラの作成

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

```PHP
php artisan make:controller ProductController
```

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

```PHP: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`を開いて以下のように記述します。

```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`を開き、下の方を以下のように記述します。

```JavaScript: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
                })
        }
    }
})
```

###<font color="red">ポイント</font>

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

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

<font color="red">購入数は正の整数を入力してください</font>

ではなく、

<font color="red">["購入数は正の整数を入力してください"]</font>

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

#コンパイル

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

```PHP
npm run dev 又は npm run watch-poll
```

#動作確認

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

#作成時にハマった点

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

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

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

        let that = this

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

この記述だと、ユーザー変更時にカート内の商品点数を取得してはくれるのですが、同じ処理が2回行われてしまいます。
<font color="blue">先ず、ユーザーが切り替わったことで変更とみなされ、処理が走ります(1回目)。
次に、`items`に値が入ることで変更とみなされ、処理が走ります(2回目)。</font>
2回目の後に再度`items`に値が代入されますが、値が全く同じなので、変更とみなされず処理は走りません。

<font color="red">最終的にwatchを使用し、監視する項目を指定することでうまくいきました。</font>

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

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

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

<font color="red">最終的に@errorディレクティブの使用は諦めました。</font>

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

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

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

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

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

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

##カートの件数のカウント

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

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

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

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

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

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

<font color="red">最終的にwhereで条件を指定しうまくいきました。</font>
個人的には`find()`でいけると思ったのですが・・・