Laravel Shoppingcart + Vue で簡単にショッピングカートを作る方法

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

さてさて、唐突ですがインターネットがありとあらゆるものを取り込んだ現代、オンラインショッピング業界は以下のように高い伸び率を維持しています。


(出典:経済産業省 平成 29 年度 電子商取引に関する市場調査)

もちろんウェブ開発にもこの影響が大きくあって、実際クライアントさんから「お金」に関連する機能のご依頼も多かったりします。

ということで、今回はそんな中からオンライショップに必須のショッピングカート機能Laravel + Vueで実装してみます。(最後に今回のソースコード一式をダウンロードできます)

ぜひウェブ開発・学習の参考にしてみてくださいね。

※ 開発環境はLaravel 5.6 + Vue 2です。

やりたいこと

  1. 商品の一覧ページで、好きな商品をカートにいれる(個数とサイズが選べる)
  2. カート一覧ページで選んだ商品リストと小計、税、合計金額などを表示する(削除ができる)

必要なパッケージをインストール

全てをスクラッチから開発してもいいのですが、Laravelにはとても便利なLaravelShoppingcartというパッケージがあるので今回はこれをインストールして利用します。

といっても、インストールはcomposer一発で完了します。

composer require gloudemans/shoppingcart

※ ただし、Laravel 5.4以下を利用している場合は、config/app.phpに以下の内容を追加する必要があります。

// providers
Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class,
// aliases
'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class,

※ ちなみに2018/10/20現在、LaravelShoppingCartLaravel 5.6までしか対応していません。ただし、これに対するプルリクエストが出てましたのでそのうちサポートされる可能性はあります。

では、次にconfigファイルが使えるようにしておきましょう。
このconfigファイルでは、税率などを設定することができます。

まず以下のコマンドを実行すると、パブリッシュ(ファイルのコピー)ができるリストがでてきますので、

php artisan vendor:publish

Gloudemans\Shoppingcart\ShoppingcartServiceProviderに該当する番号を入力してエンターキーを押します。

すると、configフォルダ内にcart.phpというファイルが作成されますので、以下2ヶ所を変更します。

'tax' => 8,
'format' => [

    'decimals' => 0,

    'decimal_point' => '.',

    'thousand_seperator' => ','

],

テストデータをつくる

実際に開発を進めて行く前に、これから必要となってくる商品データをデータベース内につくっておきましょう。テーブル名はproductsです。

DBのテーブルをつくる

まず、以下のコマンドでモデル+マイグレーションを作成します。

php artisan make:model Product -m

作成されたdatabase/migrations/****_**_**_******_create_products_table.phpを開いてSchemaを以下のように変更します。

Schema::create('products', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->comment('商品名');
    $table->string('amount')->comment('価格');
    $table->text('sizes')->comment('有効なサイズ'); // JSON
    $table->timestamps();
});

では、以下のコマンドを実行してテーブルを作成します。

php artisan migrate

テーブルの中身はこうなります。

JSONデータの自動変換

ちなみにsizesはどのサイズが選べるかを保存するフィールドで、JSONで保管するのでtextを使っています。

ただ、データ操作をする時に毎回json_encode()json_deocode()を実行するのはめんどうですし、忘れてしまうこともあるかもしれません。そのため、モデルapp/Product.phpの中でsizesは自動的にJSON⇔配列の変換ができるようにしておきます。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $casts = [
        'sizes' => 'json'
    ];
}

これで、

$product->sizes = ['S', 'M', 'L'];

とすれば自動的に配列をJSONデータに変換してくれますし、逆に

$sizes = $product->sizes;

とすれば、変数の中には配列データが格納されることになります。

Seederでテストデータを用意する

まず以下のコマンドで専用のSeederを作成します。

php artisan make:seed ProductsTableSeeder

次に作成されたdatabase/seeds/ProductsTableSeeder.phpを開いて以下のようにコードを追加します。

<?php

use Illuminate\Database\Seeder;

class ProductsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        for($i = 1 ; $i <= 25 ; $i++) {

            $product = new \App\Product();
            $product->name = $i .'番目の商品名';
            $product->amount = array_random([500, 1000, 1500]); // ランダム
            $product->sizes = array_random([ // ランダム
                ['M'],
                ['M', 'L'],
                ['S', 'M', 'L']
            ]);
            $product->save();

        }
    }
}

では、このSeederを同じフォルダにあるDatabaseSeeder.phpに登録してSeederを実行しましょう。

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
         $this->call(ProductsTableSeeder::class);
    }
}
php artisan migrate:fresh --seed

実行後、テーブルはこうなります。

商品ページをつくる

では、好きな商品を選んでカートへ入れるページを作成していきましょう。

ルーティングをつくる

まずは以下のURLにアクセスできるようにルーティングを作成します。必要となるのは以下の4つです。

  • 商品リストを表示するページ ・・・ ProductController
  • カートの中身を表示、内容変更するページ ・・・ CartController
  • Ajaxで商品データを取得するページ ・・・ Ajax/ProductController
  • Ajaxでカートの中身を{参照/追加/変更/削除}するページ ・・・ Ajax/CartController

そのため、routes/web.phpには以下のルーティングを追加します。

Route::get('products', 'ProductController@index');
Route::get('carts', 'CartController@index');
Route::resource('ajax/products', 'Ajax\ProductController')
    ->only(['index']);
Route::resource('ajax/carts', 'Ajax\CartController')
    ->only(['index', 'store', 'destroy']);

※ ちなみにresource()を使うと

  • index
  • create
  • store
  • edit
  • update
  • destroy

というデータ編集に必要なメソッドを一気に用意することができます。またonly()ではその中から必要なメソッドを指定しています。

コントローラーをつくる

では、ルーティングで指定したコントローラーを以下のコマンドで作成しましょう。

php artisan make:controller ProductController
php artisan make:controller CartController
php artisan make:controller Ajax\\ProductController --resource
php artisan make:controller Ajax\\CartController --resource

※ 下2つのコマンドだけ--resourceがついているのは、こうすると一気にindexstoreupdateなどのメソッドを始めから用意してくれるからです。

そして、すぐ後で使うので、ProductControllerindex()メソッドでは全てのデータを返すコードを追加しておきましょう。

<?php

namespace App\Http\Controllers\Ajax;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return \App\Product::get();
    }
}

では、この時点でデータ取得できるか/ajax/productsにアクセスしてみます。

うまくJSONでデータが表示されました。(Laravelでは、コントローラーで配列やオブジェクトをreturnすると、自動的にJSONにしてくれます。)

ビューをつくる

そして、実際にページ表示される部分を作ります。
今回は、JavaScriptにVueaxiosbootstrapのCSSをcdnで読み込むようにしましょう。(今回はIE対応したコードでいきます ^^b)

パスはresources/views/products/index.blade.phpです。

<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body style="padding-top: 10px;">
    <div id="app">
        <div class="container">
            <div class="float-right">
                カートの中身: <span class="badge badge-pill badge-light" v-text="Object.keys(cartItems).length"></span> 個
            </div>
            <h1>商品一覧</h1>
            <div class="row">
                <div v-for="(product,index) in products" class="col-sm-4">
                    <div class="card border-info">
                        <div class="card-body">
                            <h5 class="card-title" v-text="product.name"></h5>
                            <p class="card-text">
                                <label>サイズ:</label>
                                <select class="form-control">
                                    <option v-for="size in product.sizes" :value="size" v-text="size"></option>
                                </select>
                            </p>
                            <p class="card-text">
                                <label>個数:</label>
                                <input type="number" class="form-control" min="0" value="0">
                            </p>
                        </div>
                        <div class="card-footer text-right">
                            <button type="button" class="btn btn-info">カートへ入れる</button>
                        </div>
                    </div>
                    <br>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                products: [],
                cartItems: {}
            },
            methods: {
                getProducts: function() {

                    var self = this;
                    axios.get('/ajax/products')
                        .then(function(response){

                            self.products = response.data;

                        });

                }
            },
            mounted: function() {

                this.getProducts();

            }
        });

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

やっていることは、Ajaxでデータを取得し、そのデータをv-forループでの表示。また、cartItemsは現在のカートの中身で、この個数を「カートの中身」として表示します。

では、作成したビューをProductControllerで登録してアクセスしてみましょう。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index() {

        return view('products.index');

    }
}

ページはこうなりました。

カートへの追加機能をつくる

では、「カートへ入れる」ボタンの部分を作っていきましょう。

まず、参照したいサイズと個数にrefをつけてVueから参照できるようにします。

<select ref="size" class="form-control">
<input ref="size" type="number" class="form-control" min="0" value="0">

そして、ボタンにクリックイベントでaddCart()を実行するようにします。

<button type="button" class="btn btn-info" @click="addCart(index)">カートへ入れる</button>

addCart()の中身はこうなります。

addCart: function(index) {

    if(confirm('カートへ追加します。よろしいですか?')) {

        var self = this;
        var size = this.$refs.size[index].value;
        var qty = this.$refs.qty[index].value;
        var product = this.products[index];

        var url = '/ajax/carts';
        var params = {
            size: size,
            qty: qty,
            productId: product.id
        };
        axios.post(url, params)
            .then(function(response){

                self.cartItems = response.data;

            });

    }

},

やっていることは、クリックイベントで取得したindexを使って該当するサイズと個数、商品のIDを取得し、AjaxでPOST送信しているだけです。

リクエストが成功するとカートの中身cartItemsを更新します。

では、送信先のAjax\CartControllerstoreを以下のように変更しましょう。

public function store(Request $request)
{
    $product = \App\Product::find($request->product_id);
    \Cart::add(
        $product->id,
        $product->name,
        $request->qty,
        $product->amount,
        ['size' => $request->size]
    );
    return \Cart::content();
}

LaravelShoppingCartでは、

  1. ID(ユニークなID)
  2. 商品名
  3. 個数
  4. 金額
  5. その他パラメータ(配列)

という順番でadd()メソッドを実行するとデータをセッションに保持することができます。

またreturn部分の\Cart::content()は、カートの中身で、以下のような連想配列(オブジェクト)です。

カートの一覧ページをつくる

ビューを追加

では、次に商品が追加されたカートの中身を表示するページです。
すでにCartControllerは作成済みなので、index()を作り、ビューをresources/views/carts/index.blade.phpに追加しましょう。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CartController extends Controller
{
    public function index() {

        return view('carts.index');

    }
}

ビューの中身はこうなります。

<html>
<head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body style="padding-top: 10px;">
<div id="app">
    <div class="container">
        <h1>カートの中身</h1>
        <table class="table">
            <tr>
                <th>商品</th>
                <th>個数</th>
                <th>価格</th>
                <th>小計</th>
                <th></th>
            </tr>
            <tr v-for="(cartItem,rowId) in carts.items">
                <td>
                    <span v-text="cartItem.name"></span> (サイズ: <span v-text="cartItem.options.size"></span>)
                </td>
                <td v-text="cartItem.qty"></td>
                <td v-text="cartItem.price"></td>
                <th v-text="cartItem.subtotal"></th>
                <td class="text-right">
                    <button type="button" class="btn btn-danger btn-sm" @click="removeItem(rowId)">削除</button>
                </td>
            </tr>
            <tr>
                <td colspan="3"></td>
                <th>小計</th>
                <th v-text="carts.subtotal"></th>
            </tr>
            <tr>
                <td colspan="3"></td>
                <th>税</th>
                <th v-text="carts.tax"></th>
            </tr>
            <tr>
                <td colspan="3"></td>
                <th>合計</th>
                <th v-text="carts.total"></th>
            </tr>
        </table>
        <div class="text-right">
            <button type="button" class="btn btn-success">お会計へ</button>
        </div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            carts: {}
        },
        methods: {
            removeItem: function(rowId) {

                if(confirm('商品を削除します。よろしいですか?')) {

                    var self = this;
                    var cart = this.carts.items[rowId];
                    var url = '/ajax/carts/'+ cart.id;
                    var params = {
                        row_id: rowId,
                        _method: 'delete'
                    };
                    axios.post(url, params)
                        .then(function(response){

                            self.getCarts();

                        });

                }

            },
            getCarts: function() {

                var self = this;
                axios.get('/ajax/carts')
                    .then(function(response){

                        self.carts = response.data;

                    });

            }
        },
        mounted: function() {

            this.getCarts();

        }
    });

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

少し長いですが、やっていることはAjaxを通して取得したカート情報を元にリストと合計金額などを表示しているだけです。

Ajax部分をつくる

では、カートのAjax部分です。
Ajax\CartControllerindex()でデータ取得、destroy()でカート内容の削除するコードを追加します。

カートデータを取得する部分

index()の中に以下のような必要なデータを返すコードを追加します。

public function index()
{
    return [
        'items' => \Cart::content(), // カートの中身
        'subtotal' => \Cart::subtotal(), // 全体の小計
        'tax' => \Cart::tax(), // 全体の税
        'total' => \Cart::total() // 全合計
    ];
}

カートデータを削除する部分

削除部分は、DELETE送信(実際にはPOST送信)で、row_idが送信されてくるので、

public function destroy(Request $request)
{
    return \Cart::remove($request->row_id);
}

※ Ajaxの通信元では結果データは使っていませんが、今後のことも考えて削除結果をreturnしています。

カートページを確認する

では、カートページに必要なコードが全て揃いましたので実際に確認してみましょう。

※ 本来、金額の表記は右詰め、さらにカンマ + 円表記で統一すべきですが、いろいろと複雑になってしまうので省略しています。

これで、削除ボタンをクリックして商品を削除すると、削除後の合計金額や税などを自動的に再計算して表示させることができます。

おまけ

ちなみに商品一覧ページは、今のままではリロードされると初期状態でカートの中身(個数)を表示することができません。そのため、ページ表示されたらすぐにカートの情報を取得するメソッドを追加してmountedの中で実行するといいでしょう。

getCarts: function() {

    var self = this;
    axios.get('/ajax/carts')
        .then(function(response){

            self.cartItems = response.data.items;

        });

}
mounted: function() {

    this.getProducts();
    this.getCarts();

}

今回のソースコードをダウンロードする

今回の説明で紹介したソースコード一式を以下からダウンロードすることができます。

※ ただし、LaravelShoppingCartのインストールは自分自身で行っていただく必要があります。

Laravel Shoppingcart + Vue でショッピングカート: ソースコード一式

 

このエントリーをはてなブックマークに追加       follow us in feedly  
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ