Laravel Event的分析和使用
第一部分 概念解釋 請自行查看觀察者模式
第二部分 源碼分析 (邏輯較長,不喜歡追代碼可以直接看使用部分)
第三部分 使用
第一部分 解釋
當一個用戶閱讀了一篇文章,可能需要給文章增加點擊量,給閱讀的用戶增加積分,給文章作者發送通知等功能。對於以上操作,
我們可以使用laravel提供的事件機制進行良好的解耦。以上的用戶閱讀一篇文章,就是laravel中的一個事件,用戶閱讀文章后觸
發的一系列操作就是此事件的監聽者,他們會被逐個執行。實際上laravel的事件服務是觀察者模式的一個實現,
觸發了一個事件,就好象推倒了多米諾骨牌的地一塊,剩下的操作就驕傲給提前擺好的陣型自行完成了。不同的是現實中我們很難讓骨牌
停止倒塌, 但在laravel中我們可以很方便的停止事件的傳播,即終止監聽者的調用鏈。
第二部分 追源碼
事件服務的注冊
# laravel中每個服務,需要先注冊再啟動,其中注冊是一定的,啟動過程可以沒有。事件服務也不例外。但事件服務的注冊位置較為特殊,
# 位於Application.php
protected function registerBaseServiceProviders()
{
# 事件服務就是在此注冊的
# 注意application的register方法實際上調用了服務提供者的register方法
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
# 事件服務提供者 Illuminate\Events\EventServiceProvider
public function register()
{
# 注意此處的singleton綁定 后面會使用到
$this->app->singleton('events', function ($app) {
// 綁定的是一個disaptcher實例 並為事件服務設置了隊列解析器
// 注意此閉包是在我們嘗試從容器中解析事件服務的時候才會執行
return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
return $app->make(QueueFactoryContract::class);
});
});
}
# 看Illuminate\Events\Dispatcher類
# 簡單的構造方法
public function __construct(ContainerContract $container = null)
{
$this->container = $container ?: new Container;
}
# setQueueResolver方法 一個簡單的set
public function setQueueResolver(callable $resolver)
{
$this->queueResolver = $resolver;
return $this;
}
# 可以看到事件服務的注冊實際上是向容器中注冊了一個事件的分發器
事件服務的啟動一(獲取所有的事件和監聽者)
# 框架啟動的過程中會調用app/Providers下所有服務提供者的boot方法,事件服務也不例外。
App\Providers\EventServiceProvider文件
class EventServiceProvider extends ServiceProvider
{
# 此數組鍵為事件名,值為事件的監聽者
# 事件服務的啟動階段會讀取此配置,將所有的事件和事件監聽者對應起來並掛載到事件分發器Dispatcher上
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
# 事件服務啟動真正調用的方法 可以看到調用了父類的boot方法
# 也可以在boot方法中向事件分發器中自行綁定事件和監聽者
public function boot()
{
parent::boot();
//
}
}
# EventServiceProvider的父類
# 注冊事件監聽器
public function boot()
{
// getEvents方法 獲取事件和監聽器
$events = $this->getEvents();
foreach ($events as $event => $listeners) {
foreach (array_unique($listeners) as $listener) {
// 此處Event facade對應的是Dispatcher的listen方法
// facade的原理和使用之前介紹過
Event::listen($event, $listener);
}
}
foreach ($this->subscribe as $subscriber) {
// 調用的是Dispatcher的subscribe方法
Event::subscribe($subscriber);
}
}
# getEvents方法
public function getEvents()
{
if ($this->app->eventsAreCached()) {
$cache = require $this->app->getCachedEventsPath();
return $cache[get_class($this)] ?? [];
} else {
return array_merge_recursive(
// 如果事件非常多,也可以設置事件和監聽者的目錄,讓框架自行幫助查找
// 如果需要開啟discoveredEvents功能,需要在App\Providers\EventServiceProvider中
// 重寫shouldDiscoverEvents方法 並返回true 代表開啟事件自動發現
// 如果需要指定事件和監聽者的目錄,需要重寫discoverEventsWithin方法,其中返回目錄數組
// 當然你也可以全部寫在listen屬性中
// 當重寫了以上兩個方法的時候 返回的數組和$listen屬性的格式是完全一致的 以事件名稱為key 監聽者為value
$this->discoveredEvents(),
// 返回的就是App\Providers\EventServiceProvider下的listen數組
$this->listens()
);
}
}
# discoveredEvents方法 此方法觸發的前提是重寫了shouldDiscoverEvents方法
public function discoverEvents()
{
// 使用了laravel提供的collect輔助函數 文檔有詳細章節介紹
// collect函數返回collection集合實例方便我們鏈式操作
// reject方法的作用是 回調函數返回 true 就會把對應的集合項從集合中移除
// reduce方法的作用是 將每次迭代的結果傳遞給下一次迭代直到集合減少為單個值
return collect($this->discoverEventsWithin())
// discoverEventsWithin方法返回查找事件監聽者的目錄數組
// 默認返回 (array) $this->app->path('Listeners')
// 我們自然可以重寫discoverEventsWithin方法,返回我們指定的監聽者目錄
->reject(function ($directory) {
// 移除集合中不是目錄的元素
return ! is_dir($directory);
})
->reduce(function ($discovered, $directory) {
return array_merge_recursive(
$discovered,
// 使用Symfony的Finder組件查找Listener文件
DiscoverEvents::within($directory, base_path())
);
}, []);
}
# Illuminate\Foundation\Events\DiscoverEvents::within方法
# 提取給定目錄中的全部監聽者
public static function within($listenerPath, $basePath)
{
return collect(static::getListenerEvents(
(new Finder)->files()->in($listenerPath), $basePath
))->mapToDictionary(function ($event, $listener) {
return [$event => $listener];
})->all();
}
protected static function getListenerEvents($listeners, $basePath)
{
$listenerEvents = [];
// $listeners是Finder組件返回指定目錄下的迭代器,遍歷可以拿到目錄下的所有文件
foreach ($listeners as $listener) {
try {
$listener = new ReflectionClass(
// 將絕對路徑轉換為類名
static::classFromFile($listener, $basePath)
);
} catch (ReflectionException $e) {
continue;
}
if (! $listener->isInstantiable()) {
continue;
}
// dump($listener->getMethods(ReflectionMethod::IS_PUBLIC));
foreach ($listener->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
// 代表着一個監聽者類中 可以設置多個監聽器
if (! Str::is('handle*', $method->name) ||
! isset($method->getParameters()[0])) {
continue;
}
$listenerEvents[$listener->name.'@'.$method->name] =
// 可以認為此處返回的是事件名
// 寫在handle*方法中的參數 我建議一定要加上類型提示,並且將類型名參數作為第一個參數傳入
Reflector::getParameterClassName($method->getParameters()[0]);
}
}
// 過濾事件參數名為空的監聽器並返回
return array_filter($listenerEvents);
}
事件服務的啟動二(注冊事件監聽者)
# 上面獲取了全部的事件監聽者 下面就要注冊這些事件監聽者了
# 繼續看EventServiceProvider::boot方法
# 使用php artisan event:list 可以查看框架中已經注冊的事件
# php artisan event:cache php artisan event:clear
public function boot()
{
# 拿到了$listen屬性和要求自動發現的所有事件(如果開啟了自動發現的話)
$events = $this->getEvents();
// dump($events);
foreach ($events as $event => $listeners) {
foreach (array_unique($listeners) as $listener) {
// 調用dispatcher的listen方法
// 事件名為key 事件監聽者為value 進行事件的注冊監聽
Event::listen($event, $listener);
}
}
foreach ($this->subscribe as $subscriber) {
// subscribe方法請自行查看
Event::subscribe($subscriber);
}
}
# Dispatcher::listen方法
# 遍歷getEvents中所有的事件和監聽者 通過實際調用Dispatcher的makeListener創建監聽者
# 以event名為鍵 創建的監聽者閉包為值 保存在數組屬性中 供事件觸發的時候查找調用
// 向調度器注冊事件監聽器
public function listen($events, $listener)
{
// dump($events, $listener);
foreach ((array) $events as $event) {
// 如果事件名稱中包含*
if (Str::contains($event, '*')) {
$this->setupWildcardListen($event, $listener);
} else {
// 正常事件名
$this->listeners[$event][] = $this->makeListener($listener);
}
}
}
# 綁定事件和監聽者閉包的映射
protected function setupWildcardListen($event, $listener)
{
// 當一系列的事件都想觸發指定的監聽者的時候 就可以使用*進行匹配
$this->wildcards[$event][] = $this->makeListener($listener, true);
// 每次更新了通配事件后 都清除緩存
$this->wildcardsCache = [];
}
# 官方注釋表名此方法向事件分發器中注冊一個事件監聽者
# 其實就是返回事件觸發時執行的監聽者閉包
# 傳入的listener可以使App\Listener\MyListener 或 App\Listener\MyListener@myHandle這種字符串
# 或者是一個接收兩個參數的閉包
public function makeListener($listener, $wildcard = false)
{
if (is_string($listener)) {
// 如果傳遞的是一個字符串的話 調用createClassListener放回閉包
return $this->createClassListener($listener, $wildcard);
}
// 如果listener是個閉包 那么直接將事件對象作為參數傳入監聽者
// 事件觸發的時候 直接執行此閉包
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
return $listener($event, $payload);
}
// 可變數量的參數列表
return $listener(...array_values($payload));
};
}
# createClassListener方法
public function createClassListener($listener, $wildcard = false)
{
// 當傳遞的是一個class名或者是帶@method的字符串的時候
return function ($event, $payload) use ($listener, $wildcard) {
if ($wildcard) {
// createClassCallable返回一個數組 第一個參數是$listener的實例 第二個參數是method
return call_user_func($this->createClassCallable($listener), $event, $payload);
}
return call_user_func_array(
$this->createClassCallable($listener), $payload
);
};
}
# createClassCallable方法
protected function createClassCallable($listener)
{
// 從字符串中獲取類名和方法名
[$class, $method] = $this->parseClassCallable($listener);
// 判斷是否需要隊列化監聽器
if ($this->handlerShouldBeQueued($class)) {
// class類名 method 方法名
return $this->createQueuedHandlerCallable($class, $method);
}
// 如果不需要異步化執行監聽者 直接返回[$listener, 'method']數組
// class通過container獲得 意味着我們可以利用容器方便的注入listner需要的依賴
// 注意此處返回的是listener的實例 和 調用監聽者時執行的方法名
return [$this->container->make($class), $method];
}
# handlerShouldBeQueued方法 判斷如果一個監聽者實現了ShouldQueue接口 就認為此監聽者需要隊列化執行
protected function handlerShouldBeQueued($class)
{
// 檢查監聽者是否實現了ShouldQueue接口
// 是否使用隊列處理事件
try {
return (new ReflectionClass($class))->implementsInterface(
ShouldQueue::class
);
} catch (Exception $e) {
return false;
}
}
# createQueuedHandlerCallable方法
protected function createQueuedHandlerCallable($class, $method)
{
return function () use ($class, $method) {
$arguments = array_map(function ($a) {
return is_object($a) ? clone $a : $a;
}, func_get_args());
// handlerWantsToBeQueued方法 動態判斷監聽者是否需要投遞到隊列執行
if ($this->handlerWantsToBeQueued($class, $arguments)) {
$this->queueHandler($class, $method, $arguments);
}
};
}
# handlerWantsToBeQueued
protected function handlerWantsToBeQueued($class, $arguments)
{
$instance = $this->container->make($class);
// 動態判斷是否需要異步化事件處理
// 需要我們在監聽器shouldQueue方法中return bool值
if (method_exists($instance, 'shouldQueue')) {
// 可以在監聽者的shouldQueue方法中返回bool值 動態判斷是否需要異步化
return $instance->shouldQueue($arguments[0]);
}
return true;
}
# queueHandler方法
// 判斷listener的各種屬性 將監聽者投遞到隊列
// laravel 隊列以后會單獨講解 此篇先到這里
protected function queueHandler($class, $method, $arguments)
{
[$listener, $job] = $this->createListenerAndJob($class, $method, $arguments);
// resolveQueue獲取注冊事件服務時設置的queueResolver
$connection = $this->resolveQueue()->connection(
$listener->connection ?? null
);
$queue = $listener->queue ?? null;
isset($listener->delay)
? $connection->laterOn($queue, $listener->delay, $job)
: $connection->pushOn($queue, $job);
}
# 以上便是事件注冊的基本代碼 總體來說 我們看到調用Dispatcher的listen方法 可以注冊監聽者和事件的綁定
# 監聽者都已閉包的形式進行包裹 這樣的好處是可以保存上下文變量
# 涉及到的異步處理 其他文章會進行講解
# 值得注意的是 注冊好的閉包 並不會執行 當觸發相應的事件時才會執行
事件的觸發
# 業務代碼中調用event()方法就可以觸發一個事件了 執行的就是Dispatch::dispatch方法
public function dispatch($event, $payload = [], $halt = false)
{
// 傳遞事件對象本身作為disaptch的參數 會將對象類名作為事件名 並將事件對象作為payload傳遞到監聽者
// 參考使用方式 event(new SomeEvent()) Event::disaptch(new SomeEvent())
[$event, $payload] = $this->parseEventAndPayload(
$event, $payload
);
if ($this->shouldBroadcast($payload)) {
$this->broadcastEvent($payload[0]);
}
$responses = [];
foreach ($this->getListeners($event) as $listener) {
// 執行每個監聽者閉包
$response = $listener($event, $payload);
if ($halt && ! is_null($response)) {
// 直接返回結果給事件觸發
return $response;
}
// 如果某個監聽者返回了false 那么終止后續監聽者的執行
if ($response === false) {
break;
}
$responses[] = $response;
}
// 返回結果給事件觸發
return $halt ? null : $responses;
}
# parseEventAndPayload方法
protected function parseEventAndPayload($event, $payload)
{
// 如果傳遞的是一個事件對象
if (is_object($event)) {
[$payload, $event] = [[$event], get_class($event)];
}
// 如果event是一個字符串 那么直接包裝payload
return [$event, Arr::wrap($payload)];
}
// 獲取所有事件監聽者
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];
$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);
// 如果插入的event類存在
return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
# addInterfaceListeners方法
protected function addInterfaceListeners($eventName, array $listeners = [])
{
foreach (class_implements($eventName) as $interface) {
// 判斷事件或者其父類實現的接口是否綁定了監聽器
if (isset($this->listeners[$interface])) {
foreach ($this->listeners[$interface] as $names) {
$listeners = array_merge($listeners, (array) $names);
}
}
}
// 返回合並后的監聽者
return $listeners;
}
# 部分其他方法
# until方法
# 觸發事件 並返回第一個不為null的監聽器結果
public function until($event, $payload = [])
{
return $this->dispatch($event, $payload, true);
}
# push方法
# 調用的還是listen方法 只不過指定了payload參數
public function push($event, $payload = [])
{
$this->listen($event.'_pushed', function () use ($event, $payload) {
$this->dispatch($event, $payload);
});
}
# flush方法 調用push注冊的監聽者
public function flush($event)
{
$this->dispatch($event.'_pushed');
}
第三部分 使用
使用一 通過觸發事件給監聽者傳參
1 在App\Porviders\EventServiceProvider的listen屬性中綁定事件和監聽者映射關系
...
use App\Events\TestEvent1;
use App\Listeners\TestListener1;
use App\Listeners\TestListener2;
...
protected $listen = [
...
TestEvent1::class => [
TestListener1::class,
// 自定義監聽者閉包調用的方法myHandle
TestListener2::class . '@myHandle'
]
];
...
2 php artisan event:generate 按照listen數組的事件監聽者映射生成
3 我們不在TestEvent1事件中做過多處理 在本示例中保持原樣即可
4 編寫TestListener1文件
<?php
namespace App\Listeners;
use App\Components\Log\LogManager;
use App\Events\TestEvent1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Providers\LogManagerServiceProvider;
class TestListener1
{
protected $logger;
// 通過上面的分析 我們知道監聽者時通過容器解析出來的 所以可以盡情的注入
public function __construct(LogManager $logger)
{
$this->logger = $logger;
}
// 自定義傳參給事件監聽者
public function handle(TestEvent1 $event, string $type)
{
// dump($type);
// dump(debug_backtrace());
$this->logger->driver($type)->logCertains('emergency', 'something emergency');
}
}
5 編寫TestListener2文件
<?php
namespace App\Listeners;
use App\Events\TestEvent1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class TestListener2
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
// 其實指定了myHandle方法之后 原生的handle方法就可以注釋或者什么都不寫了
// 通過上面的分析我們知道 框架會執行監聽者中所有以handle開頭的方法
// public function handle(TestEvent1 $event)
// {
// echo '345';
// }
// 此方法也是會執行的
// public function handleabc(TestEvent1 $event)
// {
// echo 'abc';
// }
public function myHandle(TestEvent1 $event)
{
dump('do whatever you like');
}
}
6 編寫測試路由 觸發事件
Route::get('event1', function () {
// 情況一 傳遞的是事件實例 如果你需要在事件實例中注入依賴 當然可以使用容器解析事件對象
// 當注入的是一個事件對象的時候 會觸發事件類名這個事件 並且將事件對象作為payload傳遞給handle方法
// Event::dispatch(new \App\Events\TestEvent1());
// 情況二 傳遞的是事件名 第二個參數生效
// 演示如何不依賴事件對象 傳遞參數
Event::dispatch(TestEvent1::class, [new TestEvent1(), 'stream']);
});
使用二 設置事件自動發現
1 在App\Porviders\EventServiceProvider中重寫shouldDiscoverEvents方法 啟用事件發現
// 啟用事件自動發現
public function shouldDiscoverEvents()
{
return true;
}
2 我們更近一步 設置自動監聽者所在目錄 重寫discoverEventsWithin方法 指定自動發現目錄
// 指定自動發現目錄
// 默認的就是app_path('Listeners')
public function discoverEventsWithin()
{
return [
// 這里可以注釋掉Listeners目錄為了避免和上面的listen屬性重復 導致所有的監聽者都會執行兩遍
// app_path('Listeners'),
app_path('MyListeners')
];
}
3 編寫事件App\Events\TestEvent2文件 我這里的代碼沒有任何的實際意義
<?php
namespace App\Events;
class TestEvent2
{
protected $name = 'xy';
public function __construct()
{
// 隨意發揮
}
public function getName()
{
return $this->name;
}
}
4 手動創建App\MyListeners\Listener1文件
# 通過上面的源碼分析 我們知道laravel會將所有以handle開頭的方法參數遍歷
# 然后將第一個參數的類名作為要觸發的事件名,將事件參數作為payload傳入
<?php
namespace App\MyListeners;
use App\Events\TestEvent2;
class MyListener1
{
public function handle(TestEvent2 $evt)
{
dump($evt->getName(), 'abc');
return false; // 如果不注釋掉此行代碼,事件的調用鏈到此終結
}
public function handleAbc(TestEvent2 $evt)
{
dump($evt->getName());
}
}
5 手動創建App\MyListeners\Listener2文件
<?php
namespace App\MyListeners;
use App\Events\TestEvent2;
class MyListener2
{
public function handle(TestEvent2 $evt)
{
dump($evt->getName());
}
}
6 創建事件自動發現路由
// 測試自動發現
Route::get('event2', function(){
Event::dispatch(new TestEvent2());
});
使用三 implement的使用 當事件實現了其他事件接口,會自動觸發其他事件綁定的監聽者
對應的方法為Dispatcher::addInterfaceListeners 請看第二部分
1 創建事件接口
<?php
namespace App\Events;
interface TestEvent3
{ }
<?php
namespace App\Events;
interface TestEvent4
{ }
2 創建監聽者
<?php
namespace App\Listeners;
class TestListener3
{
public function handle()
{
dump('listener3 added by event interface3');
}
}
<?php
namespace App\Listeners;
class TestListener4
{
public function handle()
{
dump('listener3 added by event interface4');
}
}
<?php
namespace App\Listeners;
class TestListener5 implements TestEvent3, TestEvent4
{
public function handle()
{
dump('five own listener');
}
}
3 事件實現上面的兩個接口
<?php
namespace App\Events;
class TestEvent5 implements TestEvent3, TestEvent4
{ }
4 注冊事件監聽者
protected $listen = [
...
TestEvent3::class => [
TestListener3::class
],
TestEvent4::class => [
TestListener4::class
],
# 甚至可以注釋掉下面3行 只需要TestEvent5實現上面兩個接口即可觸發上面注冊的監聽者
TestEvent5::class => [
TestListener5::class
]
];
5 最重要的一步 force and brutal 改源碼 沒錯 就是改源碼
# Dispatcher::getListeners方法
...
// return class_exists($eventName, false)
return class_exists($eventName)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
...
6 創建測試路由
Route::get('event5', function () {
Event::dispatch(TestEvent5::class);
});
使用四 until
until方法默認調用dispatch方法 當時間監聽者返回不為null則停止執行后面的監聽者 並返回結果給事件觸發位置
1 配置時間監聽者
protected $listen = [
...
TestEvent6::class => [
TestListener6::class,
TestListener7::class,
TestListener8::class
]
];
2 php artisan event:generate
3 簡單編寫事件監聽者
# listener6
public function handle(TestEvent6 $event)
{
dump('return null');
}
# listener7
public function handle(TestEvent6 $event)
{
// 注意此監聽者是有返回值的
return 123;
}
# listener8
public function handle(TestEvent6 $event)
{
// 並不會執行7后面的監聽者 根本就不會執行
return 'return something in vain';
}
4 編寫測試路由
Route::get('event6', function () {
$res = Event::until(new TestEvent6());
// 可以看到監聽者8並沒有執行 因為7返回的不是null
dump($res);
});
使用五 push&flush 請查看上面的源碼分析
push方法就是提前將event和payload注冊好 供flush調用
1 在App\Providers\EventServiceProvider::boot方法中注冊(這里只是舉例在boot中進行注冊,你可以在你喜歡的任何地方注冊)
public function boot()
{
parent::boot();
// 注冊一個保存了payload的事件監聽者
Event::push('longAssEventName', ['type' => 'redis']);
Event::listen('longAssEventName', function ($type) {
dump($type);
});
}
2 創建測試路由
Route::get('event7', function () {
Event::flush('longAssEventName');
});
以上用法沒那么常見,這里只是簡單演示下,細節還需各位自行嘗試,常見使用還要各位仔細查閱文檔。
至於監聽者的異步化,只需要監聽者實現ShouldQueue接口,然后簡單配置就可以了。大家可以先行查看文檔事件部分,
具體使用會在laravel隊列篇章講解。如有錯誤,勞煩指正,感謝。
最后,祝各位十一快樂!!!