Laravel5.5開發學習筆記

本博文用來整理在開發中遇到的Laravel新特性的筆記。

1、清除緩存命令

php artisan optimize
php artisan cache:clear
php artisan config:clear    // 清除配置文件緩存
php artisan route:clear
php artisan view:clear

2、composer

修改 composer.json 後需使用命令從新加載:php

composer dumpautoload

3、事務

DB::transaction() 方法會開啓一個數據庫事務,在回調函數裏的全部 SQL 寫操做都會被包含在這個事務裏,若是回調函數拋出異常則會自動回滾這個事務,不然提交事務。用這個方法能夠幫咱們節省很多代碼。mysql

// 開啓一個數據庫事務
  $order = \DB::transaction(function() use ($user, $request){
     // 具體業務...
  });

4、異常處理

異常指的是在程序運行過程當中發生的異常事件,一般是由外部問題所致使的。
異常處理是程序開發中常常遇到的任務,如何優雅地處理異常,從必定程度上反映了你的程序是否足夠嚴謹。laravel

咱們將異常大體分爲 用戶異常 和 系統異常,接下來咱們將分別對其講解和代碼實現。redis

1.用戶錯誤行爲觸發的異常

好比上章節中已經驗證過郵箱的用戶再次去申請激活郵件時觸發的異常,對於此類異常咱們須要把觸發異常的緣由告知用戶。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 屬性中定義了,若是有則不會打印到日誌文件中。

2.系統內部異常

好比鏈接數據庫失敗,對於此類異常咱們須要有限度地告知用戶發生了什麼,但又不能把全部信息都暴露給用戶(好比鏈接數據庫失敗的信息裏會包含數據庫地址和帳號密碼),所以咱們須要傳入兩條信息,一條是給用戶看的,另外一條是打印到日誌中給開發人員看的。

新建一個 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('你已經驗證過郵箱了');
        }
        .
        .
        .
    }

5、延遲任務

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);
            }
        });
    }
}

2. 觸發任務

接下來咱們須要在建立訂單以後觸發這個任務:

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,

3. 測試

默認狀況下,Laravel 生成的 .env 文件裏把隊列的驅動設置成了 sync(同步),在同步模式下延遲任務會被當即執行,因此須要先把隊列的驅動改爲 redis

.env

QUEUE_DRIVER=redis

要使用 redis 做爲隊列驅動,咱們還須要引入 predis/predis 這個包

$ composer require predis/predis

接下來啓動隊列處理器:

$ php artisan queue:work

clipboard.png

6、權限控制

爲了安全起見咱們只容許訂單的建立者能夠看到對應的訂單信息,這個需求能夠經過受權策略類(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'])]);
    }

7、封裝業務代碼

通常項目開始的時候業務比較簡單,咱們都將業務邏輯寫在了控制器,可是隨着時間的增長,咱們會發現咱們在 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 中直接複製過來的,只有些許的變化須要注意:

  • 一、$user、$address 變量改成從參數獲取。咱們在封裝功能的時候有一點必定要注意$request 不能夠出如今控制器和中間件之外的地方,根據【職責單一原則】,獲取數據這個任務應該由控制器來完成,封裝的類只須要專一於業務邏輯的實現
  • 二、CartService 的調用方式改成了經過 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 模式

Service 模式將 PHP 的商業邏輯寫在對應責任的 Service 類裏,解決 Controller 臃腫的問題。而且符合 SOLID 的單一責任原則,購物車的邏輯由 CartService 負責,而不是 CartController ,控制器是調度中心,編碼邏輯更加清晰。後面若是咱們有 API 或者其餘會使用到購物車功能的需求,也能夠直接使用 CartService ,代碼可複用性大大增長。再加上 Service 能夠利用 Laravel 提供的依賴注入機制,大大提升了 Service 部分代碼的可測試性,程序的健壯性越佳。

8、容器

容器是現代 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'),
        ],
    ],
];

二、容器建立

咱們一般在 AppServiceProviderregister() 方法中往容器中注入實例:

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')

clipboard.png

能夠看到已經OK了。

9、事件與監聽器

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:

clipboard.png

而後在終端裏進入 tinker:

php artisan tinker

在 tinker 中觸發訂單支付成功的事件,事件對應的訂單就是咱們剛剛在數據庫中找的那一條:

>>> event(new App\Events\OrderPaid(App\Models\Order::find(16)))

clipboard.png

這個時候看到啓動隊列處理的窗口有了輸出:

clipboard.png

能夠看到更新庫存的事件監聽器已經在隊列中執行了。

10、MySQL命令導出數據

由於這是一個一次性的工做,沒有必要專門寫代碼來處理導入和導出,因此咱們選擇直接用 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

命令解析:

  • -t 選項表明不導出數據表結構,這些表的結構咱們會經過 Laravel 的 migration 遷移文件來建立;
  • laravel-shop 表明咱們要導出的數據庫名稱,後面則是要導出的表列表;
  • database/admin.sql 把導出的內容保存到 database/admin.sql 文件中。
在 Homestead 環境中咱們執行 Mysql 相關的命令都不須要帳號密碼,由於 Homestead 都已經幫咱們配置好了。在線上執行 Mysql 命令時則須要在命令行裏經過 -u 和 -p 參數指明帳號密碼,如: mysqldump -uroot -p123456 laravel-shop > database/admin.sql

clipboard.png

相關文章
相關標籤/搜索