【Laravel實戰】購物車從零開始用Livewire實作攻略

【Laravel實戰】購物車從零開始用Livewire實作攻略

今天上午社群有朋友問了個問題,想了解該如何更好處理訂單所有品項的金額加總功能。我本來想要直接回在社群貼文裡頭,但考慮到估計有很多新手都有類似的問題

因此我決定好好的"用一天的時間"來梳理這個問題,帶領有需要的朋友來利用Livewire 從零開始實作出一個購物車功能。若是你想要了解該如何利用 Livewire來進行開發,或者是對購物車功能有興趣,那這篇文章將能讓你在最短時間做出像這樣的功能,而且讓重載頁面說掰掰

文章最後會附上原始碼給你參考,請不用擔心程式的問題。當攻略當中提到指令就代表需要開啟Terminal在裡頭輸入指令,就不重複的說。話不多說就讓我們開始攻略購物車吧!!

如果你只對Livewire實作環節有興趣,可以跳到下面的 Livewire組件開發環節

專案建立

這次攻略我們將不著墨該如何建立Laravel開發環境,是假定你已經擁有可以開發的環境了。

但是如果你想了解該如何建立開發環境,可以參考我的這一篇拙作:Laravel開發環境建置

輸入以下指令來建立新專案

laravel new shop

別忘了你也需要新增並且設定好資料庫,可取為同名的shop,接著修改.env檔案,以使之符合資料庫正確的名稱.帳號與密碼等設定

Livewire套件安裝

因為希望能夠動態的渲染訂單明細而不需要經過頁面重載,因此決定導入Livewire來簡單搞定Ajax工作,輸入以下指令來下載Livewirew套件

composer require livewire/livewire

Layout 視圖準備

因為這次示範的頁面內容不是很複雜,很適合透過全頁組件來實作。因此我們必須要先準備好組件視圖的Layout,預設是resources/views/layouts/app.blade.php

而很不幸的,預設在Laravel8這檔案是不存在的,所以我們得手動新增,下面我給出這個視圖程式碼的重點內容

  • 載入Bootstrap
  • 載入Livewire的樣式與JS
  • 加入預設插槽,用於注入組件的內容
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Livewire Shop</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">

    @livewireStyles
</head>

<body>
    {{ $slot }}
    @livewireScripts
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous">
    </script>
</body>

</html>

資料庫準備

目前資料庫應該是空的,現在我們需著手準備購物車應用所需要的表格與假資料

表格建立

一般的購物車應用最少會需要users.items.orders.order_item這四個表格。users表格內建就有,後面三個表格為了避免過於複雜,欄位有所簡化,以足以示範關鍵功能為原則,但你可以根據自己的需要來擴展

items表格建立

這表格用於儲存所有商品資料

輸入以下指令來生成Migration檔案

php artisan make:migration create_items_table

接著輸入以下指令來生成Model檔案

php artisan make:model Item

Migration檔案內容如下,供你參考


//database/migrations/2021_03_21_061735_create_items_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title', 100); //字串欄位,長度不超過100
            $table->string('pic',255);
            $table->text('desc')->nullable(); //長字串欄位,可為空值
            $table->integer('price')->default(0)->unsigned(); //整數欄位,預設為0,不得為負數
            $table->timestamp('sell_at')->nullable(); //時間戳記欄位,可為空值
            $table->boolean('enabled')->default(true); //布林值欄位,預設為true
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}

orders表格建立

這表格用於儲存訂單相關資料

輸入以下指令來生成Migration檔案

php artisan make:migration create_orders_table

接著輸入以下指令來生成Model檔案

php artisan make:model Order

Migration檔案內容如下,供你參考


//database/migrations/2021_03_21_061933_create_orders_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateOrdersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger("user_id")->index();
            $table->foreign("user_id")->references("id")->on("users")->onDelete("cascade");
            $table->string("comment", "500")->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('orders', function (Blueprint $table) {
            $table->dropForeign(['user_id']);
        });
        Schema::dropIfExists('orders');
    }
}

item_order表格建立

這表格用於儲存訂單的明細資料

輸入以下指令來生成Migration檔案

php artisan make:migration create_item_order_table

這裡有個小重點,在建立中介pivot表格時,會用到兩個單字來連結,新手往往會不知道該如何命名,在亂猜順序之下因命名規則違反Laravel的預設而導致程式錯誤。請記得蛇底式命名法,首字母小的排在前面,這例子的item取i而order取o,i比o小,所以取名為item_order為正解

接著輸入以下指令來生成Model檔案

php artisan make:model ItemOrder

這裡有個小重點,中介pivot表格的Model模型請採用大駝峰是命名法而非蛇底式命名法。如果誤用將導致依賴關係無法正常執行,請特別注意!

//database/migrations/2021_03_21_062047_create_item_order_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateItemOrderTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('item_order', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger("order_id")->index();
            $table->foreign("order_id")->references("id")->on("orders")->onDelete("cascade");
            $table->unsignedBigInteger("item_id")->index();
            $table->foreign("item_id")->references("id")->on("items")->onDelete("cascade");
            $table->unsignedInteger("qty")->default(1);
            $table->string("desc", 500)->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('item_order', function (Blueprint $table) {
            $table->dropForeign(['item_id']);
            $table->dropForeign(['order_id']);
        });
        Schema::dropIfExists('order_item');
    }
}

完成所有Migration檔案以及修改之後,輸入以下指令來啟動migrate以生成表格

php artisan migrate

假資料建立

這個環節我們需要準備用戶與商品的假資料。用戶專案內建就已提供你工廠只要通知生產即可,而商品則需要自己實作

輸入以下指令來建立商品Seeder檔案

php artisan make:seeder ItemsTableSeeder

編輯ItemsTableSeeder內容如下,這裡只是使用到模型的create()來建立資料,並不複雜才是:

//database/seeders/ItemsTableSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Item;

class ItemsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Item::create(['title' => '牛仔褲','price' => 500,'pic'=>'1.jpg']);
        Item::create(['title' => '跑鞋', 'price' => 600, 'pic' => '2.jpg']);
        Item::create(['title' => '女用運動褲', 'price' => 700, 'pic' => '3.jpg']);
        Item::create(['title' => '男短褲', 'price' => 400, 'pic' => '4.jpg']);
        Item::create(['title' => '特色套裝', 'price' => 1500, 'pic' => '5.jpg']);
        Item::create(['title' => '性感套裝', 'price' => 800, 'pic' => '6.jpg']);
        Item::create(['title' => '品牌包', 'price' => 900, 'pic' => '7.jpg']);
        Item::create(['title' => '太陽眼鏡', 'price' => 1000, 'pic' => '8.jpg']);
        Item::create(['title' => '黑色上衣', 'price' => 300, 'pic' => '9.jpg']);
    }
}

接著編輯 DatabaseSeeder.php ,這個檔案是Seeder的入口,將用來生產用戶假資料並呼叫我們剛寫的 ItemsTableSeeder.php

//database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        //用User工廠來生成用戶假資料
        User::factory()
            ->count(1)
            ->create();

        //呼叫ItemsTableSeeder以生成商品假資料
        $this->call([
            ItemsTableSeeder::class
        ]);
    }
}

最後輸入以下指令來生成假資料

php artisan db:seed

模型程式碼撰寫

接著來看剛才這些表格的模型,有哪些需要追加的程式碼,首先是商品 Item

在這裡我們加入關聯函式 orders(),方便取得該商品位於哪些的訂單裡頭,加入withTimestamps()以便在加入訂單時自動管理 updated_at 以及 created_at

//App/Models/Item.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    use HasFactory;

    //此商品屬於哪些訂單
    public function orders()
    {
        return $this->belongsToMany(\App\Models\Order::class)->withTimestamps();
    }

}

接著是訂單 Order

這次的內容比較多,我在下面條列出來方便你去留心

  • 加入fillable以便等會新增訂單時不會被MassAssignment所擋住
  • 加入items()關聯式以方便取出該訂單的明細
  • 加入getSumAttribute()以生成魔法屬性,可取得該訂單的總金額

在items()你可以注意到我使用了withPivot('qty'),這個方法可以幫助我取出中介表格item_order裡頭的額外欄位qty,否則預設這欄位資料是不會帶出來的

關於sum這個功能就是我想要與這位網友分享最重要的一件事,重要到我選擇實作出一整個功能,^^

該網友原先的作法是將sum資料由Server計算後放入order物件內,這作法其實不算太糟,我還見過將總計直接存在資料庫裡頭的

這樣的作法有問題嗎?當然有,那就是一致性。當訂單被異動時如果程式忘了去改動這個資料就會導致sum與訂單內容不一致,這將會是一隻非常討厭的臭蟲。因為它存在但是PHP並不會提醒你這一行錯了,只有會計師會提醒你...

比較好的解法是什麼,那就是Vue.js裡好用的計算屬性,而萬能的Laravel當然也有,就是Accessor,好的設計師不會忘了有這個工具來解決總計的需求

在這個例子中,我們宣告了getSumAttribute()來實作總計屬性給你參考

//App/Models/Order.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    protected $fillable = ['user_id'];

    //此訂單擁有那些商品
    public function items()
    {
        return $this->belongsToMany(\App\Models\Item::class)->withTimestamps()->withPivot('qty');
    }

    //總計屬性
    public function getSumAttribute()
    {
        $orderItems = $this->items;
        $sum = 0;
        foreach ($orderItems as $item) {
            $sum += ($item->price * $item->pivot->qty);
        }
        return $sum;
    }
}

Livewire組件開發

總算是進入 Livewire 環節了,讓對 Livewire 感興趣的人等太久,真是抱歉,現在就開始說明該如何實作它

建立組件

這個環節很簡單,請輸入以下指令,它將為你生成組件類別以及對應視圖,名稱你可以自己取,只要記得你取的名字即可

php artisan make:livewire shop-page

編輯組件內容

首先我們來編輯組件類別,內容如下:

//App/Http/Livewire/ShopPage.php

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Item;
use App\Models\Order;
use Log;

class ShopPage extends Component
{
    public $items;
    public Order $order;

    //組件創建時被呼叫
    public function mount()
    {
        $this->items = Item::get();
        $order = Order::create(['user_id'=>1]);
        session(['order'=>$order]);
    }

    //渲染組件視圖
    public function render()
    {
        return view('livewire.shop-page');
    }

    //Livewire行動,於前端超連結被點下時呼叫,$id為商品ID
    public function addCart($id)
    {
        Log::debug('addCart');
        $order = session()->get('order');
        $this->order = Order::with('items')->findOrFail($order->id);
        $item = Item::findOrFail($id);
        $order->items()->save($item, ['qty' => 1]);
        $this->order = Order::with('items')->findOrFail($order->id);
    }
}

這裡有個重點在於因為Ajax應用會經歷多次請求,為了要保留狀態,這裡選擇透過Session來保存對應的訂單,目的是讓使用者在進行下單時都會把內容存入相同的訂單內而非建立新訂單

另外因為需要在視圖中取出訂單內的明細,需要關聯到商品表格,所以使用了with('items')這個技巧

接著來編輯組件視圖檔案,內容如下:

//resources/views/livewire/shop-page.blade.php

<div class="container">
    <div class="row">
        <div class="col-sm">
            <h2>商品列表</h2>
            <ul>
                @foreach ($items as $item)
                <li>
                    <img src="{{ url('images/'.$item->pic) }}" width=50 height=50>
                    {{ $item->title }} (${{ $item->price }})&nbsp;&nbsp;&nbsp;&nbsp;<a href="#"
                        wire:click="addCart({{$item->id}})">Buy</a>
                </li>
                @endforeach
            </ul>
        </div>
        <div class="col-sm">
            @isset($order)
            <h2>訂單明細</h2>
            <ul>
                @foreach ($order->items as $item)
                <li>{{ $item->title }} {{ $item->price }} x {{ $item->pivot->qty }}</li>
                @endforeach
            </ul>
            總計: {{ $order->sum }}
            @endisset
        </div>
    </div>
</div>

建立路由&測試

終於快到終點了,現在為剛才辛苦寫好的組件建立路由規則吧,開啟web.php,加入以下規則

//routes/web.php

Route::get('/shop',App\Http\Livewire\ShopPage::class);

完成之後,開啟你的瀏覽器,看看專案網址的路徑後面加上/shop路徑能否正常訪問商品頁,以及點Buy連結是否正常進行購買

如果一切如我所預期,將滑順的如巧克力一般,相信你的客戶會滿意的!

後記

這次的 Livewire 實戰教學是我的一次新的嘗試,沒有意外的話也會在下週變成一支影片,如果你對這次教學的內容有不清楚的話,可以訂閱我的頻道:哥布林挨踢頻道並開啟小鈴鐺到全部,就會收到這次實戰影片的通知唷!

如果有任何其他問題或者是建議,也歡迎在底下留言告訴我,我每一個留言都會看過唷!

需要原代碼嗎?給你傳送門

我是哥布林工程師,我們空中再見,掰~


分享這篇文章:

關聯文章:

訂閱電子報,索取 Laravel 學習手冊

價值超過 3000 元,包含常用 Laravel 語法與指令!

Laravel 百萬年薪特訓營

從最基礎的 PHP 語法開始,包含所有你該知道的網頁基礎知識,連同 Laravel 從零開始一直到實戰,最後還將告訴你如何找好工作,讓你及早擁有百萬年薪