第一部分 概念解釋 請自行查看觀察者模式php
第二部分 源碼分析 (邏輯較長,不喜歡追代碼能夠直接看使用部分)laravel
第三部分 使用redis
當一個用戶閱讀了一篇文章,可能須要給文章增長點擊量,給閱讀的用戶增長積分,給文章做者發送通知等功能。對於以上操做,數組
咱們可使用laravel提供的事件機制進行良好的解耦。以上的用戶閱讀一篇文章,就是laravel中的一個事件,用戶閱讀文章後觸緩存
發的一系列操做就是此事件的監聽者,他們會被逐個執行。實際上laravel的事件服務是觀察者模式的一個實現,閉包
觸發了一個事件,就好象推倒了多米諾骨牌的地一塊,剩下的操做就驕傲給提早擺好的陣型自行完成了。不一樣的是現實中咱們很難讓骨牌app
中止倒塌, 但在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']); });
使用二 設置事件自動發現ide
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和flush
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隊列篇章講解。若有錯誤,勞煩指正,感謝。
最後,祝各位十一快樂!!!