概述
事件是一種常見的觀察者模式的應用。簡單的來說,就是當... 干...。這個當... 和干... 在 Laravel 事件中分別對應:
當 (event)... 干 (listener)...
放置 event 和 listener 文件的位置分別是:
app/Events
app/Listeners
對於產品經理來說,事件主要用來規范你的業務邏輯,使支線邏輯與主線邏輯獨立分拆。對於程序員來說,事件可以讓 Controller 變得非常簡潔,解耦,可維護。
定義事件 (Event)
用 Artisan 命令可以快速生成一個模板:
php artisan event:generate
<?php namespace App\Events; use App\Podcast; use App\Events\Event; use Illuminate\Queue\SerializesModels; class PodcastWasPurchased extends Event { use SerializesModels;
public $podcast; /** * Create a new event instance. * * @param Podcast $podcast * @return void */ public function __construct(Podcast $podcast) { $this->podcast = $podcast; } }
這樣就定義了一個事件,這個事件里沒有任何業務邏輯,就是一個數據傳輸層 DTL(Data Transpotation Layer), 記住這個概念,在很多設計模式中都需要涉及到。
定義事件的偵聽和處理器(Listener and Handler)
你在用 artisan 命令生成 Event 的時候,對應的 Listner 也一並生成好了:
<?php namespace App\Listeners; use App\Events\PodcastWasPurchased; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class EmailPurchaseConfirmation { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param PodcastWasPurchased $event * @return void */ public function handle(PodcastWasPurchased $event) { // Access the podcast using $event->podcast... } }
handler 里就是寫業務邏輯的地方了,這里可以用 type-hint 依賴注入的方式,注入任何你需要的類。
將 Event 和 Listener 綁定並注冊
這里就用到 Service Provider: providers/EventServiceProvider.php 注冊事件和 Listener:
protected $listen = [ 'App\Events\PodcastWasPurchased' => [ 'App\Listeners\EmailPurchaseConfirmation', ], ];
觸發事件
經過上面的設置,你的事件和事件處理器就可以在 controller 里使用了:
<?php namespace App\Http\Controllers; use Event; use App\Podcast; use App\Events\PodcastWasPurchased; use App\Http\Controllers\Controller; class UserController extends Controller { /** * Show the profile for the given user. * * @param int $userId * @param int $podcastId * @return Response */ public function purchasePodcast($userId, $podcastId) { $podcast = Podcast::findOrFail($podcastId); // Purchase podcast logic... Event::fire(new PodcastWasPurchased($podcast)); } }
Event::fire (new PodcastWasPurchased ($podcast)); 就是觸發事件的寫法,程序運行到這里,就會觸發跟這個事件綁定的 listener (handler)。
Event::fire () 有個輔助函數可以簡寫:
event(new PodcastWasPurchased($podcast));
將事件加入隊列
如果要處理的事件很多,那么會影響當前進程的執行效率,這時我們需要把事件加入隊列,讓它延遲異步執行。
定義隊列執行是在 Listener 那里定義的:
<?php namespace App\Listeners; use App\Events\PodcastWasPurchased; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class EmailPurchaseConfirmation implements ShouldQueue { // }
只要 implements ShouldQueue 一下就好了。
如果你想手動指定一下任務延遲執行的時間:
<?php namespace App\Listeners; use App\Events\PodcastWasPurchased; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class EmailPurchaseConfirmation implements ShouldQueue { use InteractsWithQueue;
public function handle(PodcastWasPurchased $event) { if (true) { $this->release(10); } } }
觸發后延遲 10 秒執行。
事件訂閱 (Event Subscribers)
Event Subscribers 是一種特殊的 Listener, 前面講的是一個 listener 里只能放一個 hander(),事件訂閱可以把很多處理器(handler)放到一個類里面,然后用一個 listner 把它們集合起來,這樣不同的事件只要對應一個 listner 就可以了。
<?php namespace App\Listeners; class UserEventListener { /** * Handle user login events. */ public function onUserLogin($event) {} /** * Handle user logout events. */ public function onUserLogout($event) {} /** * Register the listeners for the subscriber. * * @param Illuminate\Events\Dispatcher $events * @return array */ public function subscribe($events) { $events->listen( 'App\Events\UserLoggedIn', 'App\Listeners\UserEventListener@onUserLogin' ); $events->listen( 'App\Events\UserLoggedOut', 'App\Listeners\UserEventListener@onUserLogout' ); } }
看后面的 subscribe (),每個事件和處理器是一一對應的。
綁定 Event Subscriber 到 Service Provider
<?php namespace App\Providers; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ // ]; /** * The subscriber classes to register. * * @var array */ protected $subscribe = [ 'App\Listeners\UserEventListener', ]; }
究竟為什么要使用 Event
使用 Event 一段時間后,你可以覺得比較麻煩,想知道到底有什么好處。
假設創建一個類 Event, 那么 $event->sendWelcomeMessage ($user) 這樣去使用, 和用觀察者模式的事件有啥區別,觀察者模式好處在哪里?
首先你要明白,事件是一種『鈎子』,Fire 事件的位置就是放置鈎子的地方。而上面那種寫法是直接嵌入的,沒有鈎子,也就是說,上面的寫法沒有事件的概念,事件是不用管你怎么做的,事件只定義發生了什么事(當... 時),這樣就可以解耦。
區別就在於,在主邏輯線上的事件,沒有做任何事情,它只是說有這樣一件事,對於這件事,你可以做點事情,也可以什么都不做。而 $event->sendWelcomeMessage ($user) 這種寫法就是 hardcoding 了,到了那個地方必須發生 sendWelcomeMessage 這個行為。
作為團隊的一個 leader,你可以把主邏輯定義后,然后在主邏輯線上設計事件節點,然后把具體怎么處理這些事件的事務交給團隊里的成員去做,成員根本不用管主邏輯和插入事件(鈎子)的地方,成員只用寫觸發事件時要處理的邏輯就可以了。
這樣是不是很方便合理啊,如果把所有處理邏輯都寫在 Event 類里面,那多人處理的時候豈不是要同時修改一個文件,這樣就會有版本沖突問題。
另外 Event 還可以異步隊列執行,這也是好處之一。
=====================================================================================================
概念 + 基礎使用
先說一下在什么場景會使用這個事件功能。
事情大概是這樣的,需求要在用戶注冊的時候發一些幫助郵件給用戶(原本用戶在注冊之后已經有發別的郵件的了,短信,IM 什么的)
原來這個注冊的方法也就 10 多行代碼。但是有時候我們為了省事,直接在注冊代碼后面添加了各種代碼。
例如這個注冊方法本來是這樣的
<?php namespace App\Htt\Controllers; use Illuminate\Http\Request; class UserController extends Controller { public function register(Request $request) { //獲取參數 //驗證參數 //寫入數據庫 //return 注冊信息 } }
現在有一個需求,要求注冊之后給用戶的郵箱發一個廣告,絕大多數的人(也包括以前的我)就直接在這后面接着寫代碼了
<?php namespace App\Htt\Controllers; use Illuminate\Http\Request; class UserController extends Controller { public function register(Request $request) { //獲取參數 //驗證參數 //寫入數據庫 //發送廣告郵件 //return 注冊信息 } }
這是比較直觀的寫法,后來又有需求要發個短信。
<?php namespace App\Htt\Controllers; use Illuminate\Http\Request; class UserController extends Controller { public function register(Request $request) { //獲取參數 //驗證參數 //寫入數據庫 //發送廣告郵件 //發送短信 //return 注冊信息 } }
然后又有需求,要發 IM 消息,這樣的需求很多。這些方法如果你封裝了,可能也就一行代碼。
但是,在實際項目中,這個注冊方法里面已經加了很多東西。如果多人開發的話各種不方便。然后想到了 laravel 似乎有這個功能,但是一直都不知道怎么應用,仔細看了一下手冊,發現和自己的想法不謀而合。
laravel 的事件功能實際上更傾向是一種管理手段,並不是沒了它我們就做不到了,只是它能讓我們做得更加好,更加優雅。
laravel 的事件是一種管理 + 實現的體現,它首先有一個總的目錄,然后我們可以宏觀的看到所有的事件,而不需要每次都要打開控制器的方法我們才能知道注冊后會發生什么,這一點很重要,非常的方便,我就不按着 laravel 的順序來講,而是按着實際情況來建立這種關系。
現在我們無非就是要在注冊之后要做一系列的事情,首先得注冊完之后調用一個事件,然后這個事件再做各種各樣的事
<?php namespace App\Htt\Controllers; use Illuminate\Http\Request; //我們先引入一個事件類,名字自定義的,之后再一步一步創建 use App\Events\Register; class UserController extends Controller { public function register(Request $request) { //獲取參數 //驗證參數 //寫入數據庫 //觸發事件,以后所有需要注冊后要做的事情,都不需要再這里加代碼了,我們只需要管理事件就好了 //event方法是laravel自帶方法, $uid是外部參數,看你需要做什么,傳什么參數了。注冊之后肯定有$uid的嘛 event(new Register($uid)); //return 注冊信息 } }
找到 \app\Providers\EventServiceProvider.php 文件。給它添加關系,告訴系統,有人用 event () 調用了事件之后要被誰監聽得到。
<?php namespace App\Providers; use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ // 用戶注冊后的事件 'App\Events\Register' => [ // 發送廣告郵件 'App\Listeners\SendAdMail', // 發送短信 'App\Listeners\SendSms', // 發送幫助信息 'App\Listeners\SendHelpInformation', ], ]; }
這里是注冊事件的入口,相當於一個總目錄,這樣就可以跟注冊代碼解耦了,以后要加東西我們就不需要再去看注冊方法的代碼了
現在注冊完之后會觸發這個 App\Events\Register 類,然后這個類會被 App\Listeners\SendAdMail,App\Listeners\SendSms,App\Listeners\SendHelpInformation 監聽得到,我們進入 app\Events 目錄,創建 Register 這個類
<?php namespace App\Events; class Register { public $uid; /** * 創建一個新的事件實例. * * @param Order $order * @return void */ public function __construct($uid) { $this->uid = $uid; } }
這樣就可以了。
然后去 app\Listeners 目錄創建各種要做的事件監聽類。
<?php namespace App\Listeners; use App\Events\Register; use App\Models\User; use Illuminate\Contracts\Queue\ShouldQueue; class SendHelpInformation implements ShouldQueue { public function __construct() { // } public function handle(Register $event) { $uid = $event->uid; $user = User::find($uid); //......各種實現 } }
這個 handle 方法就是我們要做的具體實現了,有個很方便的功能就是如果 implements ShouldQueue 這個接口的話就會異步隊列執行,如果去掉的話就是同步執行。很方便有沒有,這樣代碼就解耦了,不需要再管注冊代碼了,在這里就能很方便的管理了。多人開發也是單獨寫自己的 Listeners 就可以了。
具體的建議大家去看看手冊吧,有些內容我這里就不完全說了。我只是拋磚引玉