本博文用來整理在開發中遇到的Laravel新特性的筆記。
php artisan optimize php artisan cache:clear php artisan config:clear // 清除配置文件緩存 php artisan route:clear php artisan view:clear
修改 composer.json
後需使用命令從新加載:php
composer dumpautoload
DB::transaction()
方法會開啓一個數據庫事務,在回調函數裏的全部 SQL 寫操做都會被包含在這個事務裏,若是回調函數拋出異常則會自動回滾這個事務,不然提交事務。用這個方法能夠幫咱們節省很多代碼。mysql
// 開啓一個數據庫事務 $order = \DB::transaction(function() use ($user, $request){ // 具體業務... });
異常
指的是在程序運行過程當中發生的異常事件,一般是由外部問題所致使的。
異常處理是程序開發中常常遇到的任務,如何優雅地處理異常,從必定程度上反映了你的程序是否足夠嚴謹。laravel
咱們將異常大體分爲 用戶異常
和 系統異常
,接下來咱們將分別對其講解和代碼實現。redis
好比上章節中已經驗證過郵箱的用戶再次去申請激活郵件時觸發的異常,對於此類異常咱們須要把觸發異常的緣由告知用戶。sql
咱們把這類異常命名爲 InvalidRequestException
,能夠經過 make:exception
命令來建立:數據庫
$ php artisan make:exception InvalidRequestException
新建立的異常文件保存在 app/Exceptions/
目錄下:json
app/Exceptions/InvalidRequestException.php數組
<?php namespace App\Exceptions; use Exception; use Illuminate\Http\Request; class InvalidRequestException extends Exception { public function __construct(string $message = "", int $code = 400) { parent::__construct($message, $code); } public function render(Request $request) { if ($request->expectsJson()) { // json() 方法第二個參數就是 Http 返回碼 return response()->json(['msg' => $this->message], $this->code); } return view('pages.error', ['msg' => $this->message]); } }
Laravel 5.5 以後支持在異常類中定義 render()
方法,該異常被觸發時系統會調用 render()
方法來輸出,咱們在 render()
裏判斷若是是 AJAX
請求則返回 JSON
格式的數據,不然就返回一個錯誤頁面。緩存
如今來建立這個錯誤頁面:安全
$ touch resources/views/pages/error.blade.php
resources/views/pages/error.blade.php
@extends('layouts.app') @section('title', '錯誤') @section('content') <div class="panel panel-default"> <div class="panel-heading">錯誤</div> <div class="panel-body text-center"> <h1>{{ $msg }}</h1> <a class="btn btn-primary" href="{{ route('root') }}">返回首頁</a> </div> </div> @endsection
當異常觸發時 Laravel 默認會把異常的信息和調用棧打印到日誌裏,好比:
而此類異常並非由於咱們系統自己的問題致使的,不會影響咱們系統的運行,若是大量此類日誌打印到日誌文件裏反而會影響咱們去分析真正有問題的異常,所以須要屏蔽這個行爲。
Laravel 內置了屏蔽指定異常寫日誌的解決方案:
app/Exceptions/Handler.php
. . . protected $dontReport = [ InvalidRequestException::class, ]; . . .
當一個異常被觸發時,Laravel 會去檢查這個異常的類型是否在 $dontReport 屬性中定義了,若是有則不會打印到日誌文件中。
好比鏈接數據庫失敗,對於此類異常咱們須要有限度地告知用戶發生了什麼,但又不能把全部信息都暴露給用戶(好比鏈接數據庫失敗的信息裏會包含數據庫地址和帳號密碼),所以咱們須要傳入兩條信息,一條是給用戶看的,另外一條是打印到日誌中給開發人員看的。
新建一個 InternalException
類:
$ php artisan make:exception InternalException
app/Exceptions/InternalException.php
<?php namespace App\Exceptions; use Exception; use Illuminate\Http\Request; class InternalException extends Exception { protected $msgForUser; public function __construct(string $message, string $msgForUser = '系統內部錯誤', int $code = 500) { parent::__construct($message, $code); $this->msgForUser = $msgForUser; } public function render(Request $request) { if ($request->expectsJson()) { return response()->json(['msg' => $this->msgForUser], $this->code); } return view('pages.error', ['msg' => $this->msgForUser]); } }
這個異常的構造函數第一個參數就是本來應該有的異常信息好比鏈接數據庫失敗,第二個參數是展現給用戶的信息,一般來講只須要告訴用戶 系統內部錯誤 便可,由於無論是鏈接 Mysql 失敗仍是鏈接 Redis 失敗對用戶來講都是同樣的,就是系統不可用,用戶也不可能根據這個信息來解決什麼問題。
接下來咱們要把以前驗證郵箱功能中的異常替換成咱們剛剛定義的異常。
app/Http/Controllers/EmailVerificationController.php
use App\Exceptions\InvalidRequestException; . . . public function verify(Request $request) { $email = $request->input('email'); $token = $request->input('token'); if (!$email || !$token) { throw new InvalidRequestException('驗證連接不正確'); } if ($token != Cache::get('email_verification_'.$email)) { throw new InvalidRequestException('驗證連接不正確或已過時'); } if (!$user = User::where('email', $email)->first()) { throw new InvalidRequestException('用戶不存在'); } . . . } public function send(Request $request) { $user = $request->user(); if ($user->email_verified) { throw new InvalidRequestException('你已經驗證過郵箱了'); } . . . }
Laravel 提供了延遲任務(Delayed Job)功能來解決購物車長時間佔用庫存的問題。當咱們的系統觸發了一個延遲任務時,Laravel 會用當前時間加上任務的延遲時間計算出任務應該被執行的時間戳,而後將這個時間戳和任務信息序列化以後存入隊列,Laravel 的隊列處理器會不斷查詢並執行隊列中知足預計執行時間等於或早於當前時間的任務。
咱們經過 make:job
命令來建立一個任務:
$ php artisan make:job CloseOrder
建立的任務類保存在 app/Jobs
目錄下,如今編輯剛剛建立的任務類:
app/Jobs/CloseOrder.php
<?php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use App\Models\Order; // 表明這個類須要被放到隊列中執行,而不是觸發時當即執行 class CloseOrder implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $order; public function __construct(Order $order, $delay) { $this->order = $order; // 設置延遲的時間,delay() 方法的參數表明多少秒以後執行 $this->delay($delay); } // 定義這個任務類具體的執行邏輯 // 當隊列處理器從隊列中取出任務時,會調用 handle() 方法 public function handle() { // 判斷對應的訂單是否已經被支付 // 若是已經支付則不須要關閉訂單,直接退出 if ($this->order->paid_at) { return; } // 經過事務執行 sql \DB::transaction(function() { // 將訂單的 closed 字段標記爲 true,即關閉訂單 $this->order->update(['closed' => true]); // 循環遍歷訂單中的商品 SKU,將訂單中的數量加回到 SKU 的庫存中去 foreach ($this->order->items as $item) { $item->productSku->addStock($item->amount); } }); } }
接下來咱們須要在建立訂單以後觸發這個任務:
app/Http/Controllers/OrdersController.php
use App\Jobs\CloseOrder; . . . public function store(Request $request) { . . . $this->dispatch(new CloseOrder($order, config('app.order_ttl'))); return $order; }
CloseOrder 構造函數的第二個參數延遲時間咱們從配置文件中讀取,爲了方便咱們測試,把這個值設置成 30 秒:
config/app.php
'order_ttl' => 30,
默認狀況下,Laravel 生成的 .env
文件裏把隊列的驅動設置成了 sync
(同步),在同步模式下延遲任務會被當即執行,因此須要先把隊列的驅動改爲 redis
:
.env
QUEUE_DRIVER=redis
要使用 redis 做爲隊列驅動,咱們還須要引入 predis/predis
這個包
$ composer require predis/predis
接下來啓動隊列處理器:
$ php artisan queue:work
爲了安全起見咱們只容許訂單的建立者能夠看到對應的訂單信息,這個需求能夠經過受權策略類
(Policy)來實現。
經過 make:policy
命令建立一個受權策略類:
$ php artisan make:policy OrderPolicy
app/Policies/OrderPolicy.php
<?php namespace App\Policies; use App\Models\Order; use App\Models\User; use Illuminate\Auth\Access\HandlesAuthorization; class OrderPolicy { use HandlesAuthorization; public function own(User $user, Order $order) { return $order->user_id == $user->id; } }
而後在 AuthServiceProvider
中註冊這個策略:
app/Providers/AuthServiceProvider.php
use App\Models\Order; use App\Policies\OrderPolicy; . . . protected $policies = [ UserAddress::class => UserAddressPolicy::class, Order::class => OrderPolicy::class, ];
最後在 OrdersController@show()
中校驗權限:
appHttp/Controllers/OrdersController.php
public function show(Order $order, Request $request) { // 權限校驗 $this->authorize('own', $order); return view('orders.show', ['order' => $order->load(['items.productSku', 'items.product'])]); }
通常項目開始的時候業務比較簡單,咱們都將業務邏輯寫在了控制器,可是隨着時間的增長,咱們會發現咱們在 Controller 裏面寫了大量的包含複雜邏輯的業務代碼,這是一個壞習慣,這樣子隨着需求的增長,咱們的控制器很快就變得臃腫
。若是之後咱們要開發 App 端,這些代碼可能須要在 Api 的 Controller 裏再重複一遍,假如出現業務邏輯的調整就須要修改兩個或更多地方,這明顯是不合理的。所以咱們須要對 邏輯複雜
的 業務代碼
進行封裝。
這裏咱們將在項目裏採用 Service
模式來封裝代碼。購物車的邏輯,放置於 CartService
類裏,將下單的業務邏輯代碼放置於 OrderService
裏。
這裏以電商項目的訂單作示例:
首先建立一個 CartService
類:
$ mkdir -p app/Services && touch app/Services/CartService.php
app/Services/CartService.php
<?php namespace App\Services; use Auth; use App\Models\CartItem; class CartService { public function get() { return Auth::user()->cartItems()->with(['productSku.product'])->get(); } public function add($skuId, $amount) { $user = Auth::user(); // 從數據庫中查詢該商品是否已經在購物車中 if ($item = $user->cartItems()->where('product_sku_id', $skuId)->first()) { // 若是存在則直接疊加商品數量 $item->update([ 'amount' => $item->amount + $amount, ]); } else { // 不然建立一個新的購物車記錄 $item = new CartItem(['amount' => $amount]); $item->user()->associate($user); $item->productSku()->associate($skuId); $item->save(); } return $item; } public function remove($skuIds) { // 能夠傳單個 ID,也能夠傳 ID 數組 if (!is_array($skuIds)) { $skuIds = [$skuIds]; } Auth::user()->cartItems()->whereIn('product_sku_id', $skuIds)->delete(); } }
接下來咱們要修改 CartController
,將其改成調用剛剛建立的 CartService
類:
app/Http/Controllers/CartController.php
<?php namespace App\Http\Controllers; use App\Models\ProductSku; use Illuminate\Http\Request; use App\Http\Requests\AddCartRequest; use App\Services\CartService; class CartController extends Controller { protected $cartService; // 利用 Laravel 的自動解析功能注入 CartService 類 public function __construct(CartService $cartService) { $this->cartService = $cartService; } public function index(Request $request) { // select * from product_skus where id in (xxxx) $cartItems = $this->cartService->get(); $addresses = $request->user()->addresses()->orderBy('last_used_at', 'desc')->get(); return view('cart.index', ['cartItems' => $cartItems, 'addresses' => $addresses]); } public function add(AddCartRequest $request) { $this->cartService->add($request->input('sku_id'), $request->input('amount')); return []; } public function remove(ProductSku $sku, Request $request) { $this->cartService->remove($sku->id); return []; } }
這裏咱們使用了 Laravel 容器的自動解析功能,當 Laravel 初始化 Controller 類時會檢查該類的構造函數參數,在本例中 Laravel 會自動建立一個 CartService 對象做爲構造參數傳入給 CartController。
app/Http/Controllers/OrdersController.php
<?php namespace App\Http\Controllers; use App\Http\Requests\OrderRequest; use App\Models\ProductSku; use App\Models\UserAddress; use App\Models\Order; use Carbon\Carbon; use Illuminate\Http\Request; use App\Exceptions\InvalidRequestException; use App\Jobs\CloseOrder; use App\Services\CartService; class OrdersController extends Controller { public function show(Order $order, Request $request) { // 權限校驗 $this->authorize('own', $order); // 這裏的 load() 方法與上一章節介紹的 with() 預加載方法有些相似,稱爲 延遲預加載 // 不一樣點在於 load() 是在已經查詢出來的模型上調用,而 with() 則是在 ORM 查詢構造器上調用。 return view('orders.show', ['order' => $order->load(['items.productSku', 'items.product'])]); } public function index(Request $request) { $orders = Order::query() // 使用 with 方法預加載,避免N + 1問題 ->with(['items.product', 'items.productSku']) ->where('user_id', $request->user()->id) ->orderBy('created_at', 'desc') ->paginate(); return view('orders.index', ['orders' => $orders]); } // 利用 Laravel 的自動解析功能注入 CartService 類 public function store(OrderRequest $request, CartService $cartService) { $user = $request->user(); // 開啓一個數據庫事務 $order = \DB::transaction(function() use ($user, $request){ $address = UserAddress::find($request->input('address_id')); // 更新此地址的最後使用時間 $address->update(['last_used_at' => Carbon::now()]); // 建立一個訂單 $order = new Order([ 'address' => [ // 將地址信息放入訂單中 'address' => $address->full_address, 'zip' => $address->zip, 'contact_name' => $address->contact_name, 'contact_phone' => $address->contact_phone, ], 'remark' => $request->input('remark'), 'total_amount' => 0, ]); // 訂單關聯到當前用戶 $order->user()->associate($user); // 寫入數據庫 $order->save(); $totalAmount = 0; $items = $request->input('items'); // 遍歷用戶提交的 SKU foreach ($items as $data) { $sku = ProductSku::find($data['sku_id']); // 建立一個 OrderItem 並直接與當前訂單關聯 $item = $order->items()->make([ 'amount' => $data['amount'], 'price' => $sku->price, ]); $item->product()->associate($sku->product_id); $item->productSku()->associate($sku); $item->save(); $totalAmount += $sku->price * $data['amount']; // 減庫存 if ($sku->decreaseStock($data['amount']) <= 0) { throw new InvalidRequestException('該商品庫存不足'); } } // 更新訂單總金額 $order->update(['total_amount' => $totalAmount]); // 將下單的商品從購物車中移除 $skuIds = collect($request->input('items'))->pluck('sku_id'); // $user->cartItems()->whereIn('product_sku_id', $skuIds)->delete(); $cartService->remove($skuIds); return $order; }); $this->dispatch(new CloseOrder($order, config('app.order_ttl'))); return $order; } }
首先建立 OrderService
類:
$ touch app/Services/OrderService.php
app/Services/OrderService.php
<?php namespace App\Services; use App\Models\User; use App\Models\UserAddress; use App\Models\Order; use App\Models\ProductSku; use App\Exceptions\InvalidRequestException; use App\Jobs\CloseOrder; use Carbon\Carbon; class OrderService { public function store(User $user, UserAddress $address, $remark, $items) { // 開啓一個數據庫事務 $order = \DB::transaction(function () use ($user, $address, $remark, $items) { // 更新此地址的最後使用時間 $address->update(['last_used_at' => Carbon::now()]); // 建立一個訂單 $order = new Order([ 'address' => [ // 將地址信息放入訂單中 'address' => $address->full_address, 'zip' => $address->zip, 'contact_name' => $address->contact_name, 'contact_phone' => $address->contact_phone, ], 'remark' => $remark, 'total_amount' => 0, ]); // 訂單關聯到當前用戶 $order->user()->associate($user); // 寫入數據庫 $order->save(); $totalAmount = 0; // 遍歷用戶提交的 SKU foreach ($items as $data) { $sku = ProductSku::find($data['sku_id']); // 建立一個 OrderItem 並直接與當前訂單關聯 $item = $order->items()->make([ 'amount' => $data['amount'], 'price' => $sku->price, ]); $item->product()->associate($sku->product_id); $item->productSku()->associate($sku); $item->save(); $totalAmount += $sku->price * $data['amount']; if ($sku->decreaseStock($data['amount']) <= 0) { throw new InvalidRequestException('該商品庫存不足'); } } // 更新訂單總金額 $order->update(['total_amount' => $totalAmount]); // 將下單的商品從購物車中移除 $skuIds = collect($items)->pluck('sku_id')->all(); app(CartService::class)->remove($skuIds); return $order; }); // 這裏咱們直接使用 dispatch 函數 dispatch(new CloseOrder($order, config('app.order_ttl'))); return $order; } }
這裏大多數的代碼都是從 OrdersController
中直接複製過來的,只有些許的變化須要注意:
$request 不能夠出如今控制器和中間件之外的地方,根據【職責單一原則】,獲取數據這個任務應該由控制器來完成,封裝的類只須要專一於業務邏輯的實現
。app()
函數建立,由於這個 store() 方法是咱們手動調用的,沒法經過 Laravel 容器的自動解析來注入。在咱們代碼裏調用封裝的庫時必定 不能夠 使用 new
關鍵字來初始化,而是應該經過 Laravel 的容器來初始化,由於在以後的開發過程當中 CartService 類的構造函數可能會發生變化,好比注入了其餘的類,若是咱們使用 new 來初始化的話,就須要在每一個調用此類的地方進行修改;而使用 app() 或者自動解析注入等方式 Laravel 則會自動幫咱們處理掉這些依賴。
$this->dispatch()
方法來觸發任務類,但在咱們的封裝的類中並無這個方法,所以關閉訂單的任務類改成 dispatch()
輔助函數來觸發。app/Http/Controllers/OrdersController.php
<?php namespace App\Http\Controllers; use App\Http\Requests\OrderRequest; use App\Models\UserAddress; use App\Models\Order; use Illuminate\Http\Request; use App\Services\OrderService; class OrdersController extends Controller { . . . public function store(OrderRequest $request, OrderService $orderService) { $user = $request->user(); $address = UserAddress::find($request->input('address_id')); return $orderService->store($user, $address, $request->input('remark'), $request->input('items')); } }
Service 模式將 PHP 的商業邏輯寫在對應責任的 Service 類裏,解決 Controller 臃腫的問題。而且符合 SOLID
的單一責任原則,購物車的邏輯由 CartService 負責,而不是 CartController ,控制器是調度中心,編碼邏輯更加清晰。後面若是咱們有 API 或者其餘會使用到購物車功能的需求,也能夠直接使用 CartService ,代碼可複用性大大增長。再加上 Service 能夠利用 Laravel 提供的依賴注入機制,大大提升了 Service 部分代碼的可測試性,程序的健壯性越佳。
容器是現代 PHP 開發的一個重要概念,Laravel 就是在容器的基礎上構建的。咱們將支付操做類實例注入到容器中,在之後的代碼裏就能夠直接經過 app('alipay')
來取得對應的實例,而不須要每次都從新建立。
在這個示例中,咱們引入第三方支付庫yansongda/pay
,而後使用容器能夠直接調用實例代碼。
yansongda/pay
這個庫封裝了支付寶和微信支付的接口,經過這個庫咱們就不須要去關注不一樣支付平臺的接口差別,使用相同的方法、參數來完成支付功能,節省開發時間。
首先經過 composer
引入這個包:
$ composer require yansongda/pay
配置參數:
建立一個新的配置文件來保存支付所需的參數:
config/pay.php
<?php return [ 'alipay' => [ 'app_id' => '', 'ali_public_key' => '', 'private_key' => '', 'log' => [ 'file' => storage_path('logs/alipay.log'), ], ], 'wechat' => [ 'app_id' => '', 'mch_id' => '', 'key' => '', 'cert_client' => '', 'cert_key' => '', 'log' => [ 'file' => storage_path('logs/wechat_pay.log'), ], ], ];
咱們一般在 AppServiceProvider
的 register()
方法中往容器中注入實例:
app/Providers/AppServiceProvider.php
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Monolog\Logger; use Yansongda\Pay\Pay; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { // } /** * Register any application services. * * @return void */ public function register() { // 往服務容器中注入一個名爲 alipay 的單例對象 $this->app->singleton('alipay', function () { $config = config('pay.alipay'); // $config['notify_url'] = route('payment.alipay.notify'); $config['notify_url'] = 'http://requestbin.leo108.com/1nj6jt11'; $config['return_url'] = route('payment.alipay.return'); // 判斷當前項目運行環境是否爲線上環境 if (app()->environment() !== 'production') { $config['mode'] = 'dev'; $config['log']['level'] = Logger::DEBUG; } else { $config['log']['level'] = Logger::WARNING; } // 調用 Yansongda\Pay 來建立一個支付寶支付對象 return Pay::alipay($config); }); $this->app->singleton('wechat_pay', function () { $config = config('pay.wechat'); if (app()->environment() !== 'production') { $config['log']['level'] = Logger::DEBUG; } else { $config['log']['level'] = Logger::WARNING; } // 調用 Yansongda\Pay 來建立一個微信支付對象 return Pay::wechat($config); }); } }
代碼解析:
$this->app->singleton()
往服務容器中注入一個單例對象
,第一次從容器中取對象時會調用回調函數來生成對應的對象並保存到容器中,以後再去取的時候直接將容器中的對象返回。app()->environment()
獲取當前運行的環境,線上環境會返回 production。對於支付寶,若是項目運行環境不是線上環境,則啓用開發模式,而且將日誌級別設置爲 DEBUG。因爲微信支付沒有開發模式,因此僅僅將日誌級別設置爲 DEBUG。接下來咱們來測試一下剛剛注入到容器中的實例,進入 tinker:
> php artisan tinker
而後分別輸入 app('alipay')
和 app('wechat_pay')
能夠看到已經OK了。
Laravel 的事件提供了一個簡單的觀察者實現,可以訂閱和監聽應用中發生的各類事件。事件類保存在 app/Events 目錄中,而這些事件的的監聽器則被保存在 app/Listeners 目錄下。這些目錄只有當你使用 Artisan 命令來生成事件和監聽器時纔會被自動建立。
事件機制是一種很好的應用解耦方式,由於一個事件能夠擁有多個互不依賴的監聽器。
好比咱們的訂單系統,支付以後要給訂單中的商品增長銷量,好比咱們要發郵件給用戶告知訂單支付成功。
商品增長銷量和發送郵件並不會影響到訂單的支付狀態,即便這兩個操做失敗了也不影響咱們後續的業務流程,對於此類需求咱們一般使用異步事件
來解決。
php artisan make:event OrderPaid
app/Events/OrderPaid.php
<?php namespace App\Events; use Illuminate\Broadcasting\Channel; use Illuminate\Queue\SerializesModels; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use App\Models\Order; class OrderPaid { use Dispatchable, InteractsWithSockets, SerializesModels; protected $order; public function __construct(Order $order) { $this->order = $order; } public function getOrder() { return $this->order; } /** * Get the channels the event should broadcast on. * * @return \Illuminate\Broadcasting\Channel|array */ public function broadcastOn() { return new PrivateChannel('channel-name'); } }
事件自己不須要有邏輯,只須要包含相關的信息便可,在咱們這個場景裏就只須要一個訂單對象。
接下來咱們在支付成功的服務器端回調裏觸發這個事件:
app/Http/Controllers/PaymentController.php
use App\Events\OrderPaid; . . . public function alipayNotify() { . . . $this->afterPaid($order); return app('alipay')->success(); } protected function afterPaid(Order $order) { event(new OrderPaid($order)); }
咱們但願訂單支付以後對應的商品銷量會對應地增長,因此建立一個更新商品銷量的監聽器:
> php artisan make:listener UpdateProductSoldCount --event=OrderPaid
app/Listeners/UpdateProductSoldCount.php
<?php namespace App\Listeners; use App\Events\OrderPaid; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use App\Models\OrderItem; // implements ShouldQueue 表明此監聽器是異步執行的 class UpdateProductSoldCount implements ShouldQueue { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * Laravel 會默認執行監聽器的 handle 方法,觸發的事件會做爲 handle 方法的參數 * @param OrderPaid $event * @return void */ public function handle(OrderPaid $event) { // 從事件對象中取出對應的訂單 $order = $event->getOrder(); // 循環遍歷訂單的商品 foreach($order->items as $item) { $product = $item->product; // 計算對應商品的銷量 $soldCount = OrderItem::query() ->where('product_id', $product->id) ->whereHas('order', function ($query) { $query->whereNotNull('paid_at'); // 關聯的訂單狀態是已支付 })->sum('amount'); // 更新商品銷量 $product->update([ 'sold_count' => $soldCount, ]); } } }
別忘了在 EventServiceProvider
中將事件和監聽器關聯起來:
app/Providers/EventServiceProvider.php
<?php namespace App\Providers; use App\Listeners\RegisteredListener; use Illuminate\Auth\Events\Registered; use Illuminate\Support\Facades\Event; use App\Events\OrderPaid; use App\Listeners\UpdateProductSoldCount; use App\Listeners\SendOrderPaidMail; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ // 監聽器建立完成以後還須要在 EventServiceProvider 中將事件和監聽器關聯起來才能生效 // @url https://laravel-china.org/courses/laravel-shop/1584/verification-mailbox-below Registered::class => [ RegisteredListener::class, ], OrderPaid::class => [ UpdateProductSoldCount::class, SendOrderPaidMail::class, ], ]; /** * Register any events for your application. * * @return void */ public function boot() { parent::boot(); // } }
因爲咱們定義的事件監聽器都是異步的,所以在測試以前須要先啓動隊列處理器:
> php artisan queue:work
從數據庫中找到一條已經支付成功的訂單並記錄下其 ID:
而後在終端裏進入 tinker:
php artisan tinker
在 tinker 中觸發訂單支付成功的事件,事件對應的訂單就是咱們剛剛在數據庫中找的那一條:
>>> event(new App\Events\OrderPaid(App\Models\Order::find(16)))
這個時候看到啓動隊列處理的窗口有了輸出:
能夠看到更新庫存的事件監聽器已經在隊列中執行了。
由於這是一個一次性的工做,沒有必要專門寫代碼來處理導入和導出,因此咱們選擇直接用 mysqldump
這個命令行程序來導出數據庫中的數據,從成本上來講比較合適:
mysqldump -t laravel-shop admin_menu admin_permissions admin_role_menu admin_role_permissions admin_role_users admin_roles admin_user_permissions admin_users > database/admin.sql
命令解析:
在 Homestead 環境中咱們執行 Mysql 相關的命令都不須要帳號密碼,由於 Homestead 都已經幫咱們配置好了。在線上執行 Mysql 命令時則須要在命令行裏經過 -u 和 -p 參數指明帳號密碼,如: mysqldump -uroot -p123456 laravel-shop > database/admin.sql