九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、唐突ですがインターネットがありとあらゆるものを取り込んだ現代、オンラインショッピング業界は以下のように高い伸び率を維持しています。
(出典:経済産業省 平成 29 年度 電子商取引に関する市場調査)
もちろんウェブ開発にもこの影響が大きくあって、実際クライアントさんから「お金」に関連する機能のご依頼も多かったりします。
ということで、今回はそんな中からオンライショップに必須のショッピングカート機能をLaravel
+ Vue
で実装してみます。(最後に今回のソースコード一式をダウンロードできます)
ぜひウェブ開発・学習の参考にしてみてくださいね。
※ 開発環境はLaravel 5.6 + Vue 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現在、LaravelShoppingCart
はLaravel 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
がついているのは、こうすると一気にindex
やstore
、update
などのメソッドを始めから用意してくれるからです。
そして、すぐ後で使うので、ProductController
のindex()
メソッドでは全てのデータを返すコードを追加しておきましょう。
<?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にVue
とaxios
、bootstrap
の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\CartController
のstore
を以下のように変更しましょう。
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
では、
- ID(ユニークなID)
- 商品名
- 個数
- 金額
- その他パラメータ(配列)
という順番で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\CartController
のindex()
でデータ取得、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
のインストールは自分自身で行っていただく必要があります。