# laravel 觀察者模式 事件和偵聽器 https://code.tutsplus.com/tutorials/custom-events-in-laravel--cms-30331 必看 https://learnku.com/articles/20712 必看 https://www.bilibili.com/video/BV1nQ4y1N7QW/?spm_id_from=333.788.recommend_more_video.2 laravel中事件和观察者 https://www.jianshu.com/p/b258926011df ## 觀念重點區 Laravel 實現了服務提供者的概念,允許你將不同的服務注入到應用程序中 所以第一步驟要去Provider **你在用 artisan 命令生成 Event 的时候,对应的 Listner 也一并生成好了** 單個事件註冊多個偵聽器 事件助手會將事件分派給所有註冊的偵聽器,而無需我們顯式調用它們 事件(event)里没有任何业务逻辑,就是一个数据传输层 DTL(Data Transpotation Layer), 记住这个概念,在很多设计模式中都需要涉及到。 定义事件的侦听和处理器(Listener and Handler) ## 不觸發 sync 过程中确实是会有删除 question_topic 表的情况,不触发事件是因为在删除中间表的时候调用的是 Builder 这个类上面的 delete 方法,而不是 QuestionTopic 你这个模型上面的 delete 这个和 DB::table('question_topic')->delete(1) 不触发事件是同样的道理 ## Laravel 模型事件实现原理 非常重要 https://learnku.com/articles/5465/event-realization-principle-of-laravel-model ## queue 排隊事件偵聽器 如果您的偵聽器要執行緩慢的任務(例如發送電子郵件或發出 HTTP 請求),則排隊偵聽器會很有用。在使用排隊偵聽器之前,請確保配置您的隊列並在您的服務器或本地開發環境上啟動隊列工作器。 要指定偵聽器應排隊,請將ShouldQueue接口添加到偵聽器類。event:generate和make:listenerArtisan 命令生成的偵聽器已經將這個接口導入到當前命名空間中,因此您可以立即使用它: ``` <?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; class SendShipmentNotification implements ShouldQueue { // } ``` 就是這樣!現在,當這個監聽器處理的事件被調度時,監聽器將使用 Laravel 的隊列系統由事件調度器自動排隊。如果隊列執行監聽器時沒有拋出異常,則排隊的作業在處理完成後會自動刪除。 ## event vs observer vs通知 event能有條件去觸發 ![](https://i.imgur.com/ODt4QUv.png) 你可以看auth 中的 regies的trait 觀察是不能只能全部 ex 使用者全部 但可能管理員不要 通知也是 要用event去篩選 除非你是都要通知 ## Observers 事件需要与监听器关联来,当监听器变多了我们还需进行注册实现,这样会比较麻烦,这时我们可以考虑使用观察者。如果在一个模型上监听了多个事件,可以使用观察者来将这些监听器组织到一个单独的类中。 ## model的事件 (這種是用event 你也可以單純的用observers) 模型事件是专门针对模型而额外添加的功能。其给我们创建了对应的事件与监听。在使用模型事件事,我们需要遵守模型给我们的规则: retrieved當從數據庫中檢索到現有模型時,將調度該事件。第一次保存新模型時,將調度creating和created事件。在updating/updated在現有模型進行修改和事件將派遣save方法被調用。該saving/saved事件將派遣一個模型創建或更新時-即使模型的屬性沒有改變。以 結尾的事件名稱-ing在對模型的任何更改被持久化之前被調度,而以 結尾的事件-ed在對模型的更改被持久化之後被調度。 要開始監聽模型事件,請$dispatchesEvents在 Eloquent 模型上定義一個屬性。該屬性將 Eloquent 模型生命週期的各個點映射到您自己的事件類。每個模型事件類應該期望通過其構造函數接收受影響模型的實例: 在当模型的生命周期中,当发生一些事的时候,Eloquent 会触发一些事件: * creating - 对象已经 ready 但未写入数据库 * created - 对象已经写入数据库 * updating - 对象已经修改但未写入数据库 * updated - 修改已经写入数据库 * saving - 对象创建或者已更新但未写入数据库 * saved - 对象创建或者更新已经写入数据库 * deleting - 删除前 * deleted - 删除后 * restoring - 恢复软删除前 * restored - 恢复软删除后 对于每一个操作,都对应两个独立的事件。正如你可能想象的,它们指的是单独的时刻。我们已创建操作作为实例: 你有一个 creating 事件,可以理解为“创建操作即将发生”,而 created 表示“事件已经发生了”。 需要在模型处绑定 ``` protected $dispatchesEvents = [ 'saving' => \App\Events\TestEvent::class, // 'updated' => \App\Events\TestEvent::class, ]; ``` 在定義和映射 Eloquent 事件之後,您可以使用事件偵聽器來處理事件。 這樣綁定 你可以對應觸發相對的監聽器 這樣可以不用event() help 在你觸發的地方 但一樣要去povider註冊喔 ### model observer 生命週期的來源 https://learnku.com/articles/6657/model-events-and-observer-in-laravel `Illuminate\Database\Eloquent\Model` , 找到位于 517 行的 save 方法 可以看到首先触发的是 saving: `$this->fireModelEvent('saving')` ## (model events) 記住如果你在裏面有調用方法只能調用靜態的 因為你是靜態方法 沒有實例化 所以調用的也要靜態方法 ![](https://i.imgur.com/bliGYPg.png) 你可以看一下框架代码,里面是如何使用 boot() 方法的。举个例子: (當然你可以用 booted function 這樣就不用 在parent::boot(); 但不推薦) ``` public static function boot() { parent::boot(); static::updating(function (Balance $model) { // ... }); static::updated(function (Balance $model) { // ... }); } ``` 在我们的模型类里重写 static 的 boot 方法,能够给每个实例化的模型都挂载上特定事件。 另外,Eloquent 的 boot 方法(也就是以上例子中调用的 parent::boot())还会调用 bootTraits() 方法,该方法使用 class_uses_recursive() 函数来遍历某个类加载的所有 Trait,并调用每个 Trait 各自的 bootTraitName() 方法,因此可以实现一个模型引用多个不同的 Trait,每个 Trait 又挂载了不同的事件,从而实现给模型添加不同的「能力」(你可以注意到很多 Eloquent 扩展提供的 Trait 都使用 -able 词缀命名)。 可以用event去送事件 ## model 監聽多個事件 (observer) 如果你想在一个模型中监听多个事件,那么你可以把它写成一个类,类中的方法名称即是你想要监听的事件名称 ``` class UserObserver { /** * 监听数据即将创建的事件。 * * @param User $user * @return void */ public function creating(User $user) { } /** * 监听数据创建后的事件。 * * @param User $user * @return void */ public function created(User $user) { } /** * 监听数据即将更新的事件。 * * @param User $user * @return void */ public function updating(User $user) { } /** * 监听数据更新后的事件。 * * @param User $user * @return void */ public function updated(User $user) { } /** * 监听数据即将保存的事件。 * * @param User $user * @return void */ public function saving(User $user) { } /** * 监听数据保存后的事件。 * * @param User $user * @return void */ public function saved(User $user) { } /** * 监听数据即将删除的事件。 * * @param User $user * @return void */ public function deleting(User $user) { } /** * 监听数据删除后的事件。 * * @param User $user * @return void */ public function deleted(User $user) { } /** * 监听数据即将从软删除状态恢复的事件。 * * @param User $user * @return void */ public function restoring(User $user) { } /** * 监听数据从软删除状态恢复后的事件。 * * @param User $user * @return void */ public function restored(User $user) { } } ``` 然后在 AppServiceProvider 中注册此观察者 ``` <?php namespace App\Providers; use App\User; use App\Observers\UserObserver; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * 运行所有应用. * * @return void */ public function boot() { // 为 User 模型注册观察者 User::observe(UserObserver::class); } /** * 注册服务提供. * * @return void */ public function register() { // } } ``` ## 基本流程 可以用event:list去看所有的event 註冊在 app/Providers/EventServiceProvider.php $listen數組鍵對應系統中的事件,它們的值對應於系統中引發相應事件時將觸發的偵聽器 像下面的 監聽登入 然後發送訊息 ``` protected $listen = [ 'Illuminate\Auth\Events\Login' => [ 'App\Listeners\SendEmailNotification', ], ]; ``` Illuminate\Auth\Events\Login是Auth當有人登錄應用程序時插件會引發的事件。我們已將該事件綁定到App\Listeners\SendEmailNotification偵聽器,因此它將在登錄事件上觸發。 **第一步驟** 首先需要定義App\Listeners\SendEmailNotification監聽器類。與往常一樣,Laravel 允許您使用 artisan 命令創建偵聽器的模板代碼 `php artisan event:generate` event(new PodcastWasPurchased($podcast)); 或用 ::dispatch() 這是調用trait裡面的 等於上面的 此命令生成$listen屬性下列出的事件和偵聽器類 **兩個都要** **先創事件在創監聽器** 使用可以用event() helper涵式 Event::fire () 有个辅助函数可以简写: **基本樣子** ``` <?php namespace App\Listeners; use Illuminate\Auth\Events\Login; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class SendEmailNotification { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param Login $event * @return void */ public function handle(Login $event) { } } ``` handle每當觸發偵聽器時,都會使用適當的依賴項調用該方法。在我們的例子中,$event參數應該包含關於登錄事件的上下文信息——登錄的用戶信息。 當調用偵聽器時,該handle方法與關聯事件的**實例**一起傳遞 並且我們可以使用對$event像在handle方法中進行進一步的處理。在我們的例子中,我們希望向登錄用戶發送電子郵件通知。 修改後的handle方法可能類似於: ``` public function handle(Login $event) { // get logged in user's email and username $email = $event->user->email; $username = $event->user->name; // send email notification about login } ``` ## 創建自定義事件 看第一篇連結 先去povider註冊 然後創立事件 跟監聽器 ``` <?php namespace App\Providers; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ 'App\Events\ClearCache' => [ 'App\Listeners\WarmUpCache', ], ]; /** * Register any events for your application. * * @return void */ public function boot() { parent::boot(); // } } ``` 註冊 但這樣要一直寫很麻煩 可以用 ![](https://i.imgur.com/xbet7zT.png) 複寫底層 直接變成true就不用寫了 就會自動依賴注入了 不用註冊了 ## 多個event 将事件加入队列 如果要处理的事件很多,那么会影响当前进程的执行效率,这时我们需要把事件加入队列,让它延迟异步执行。 **定义队列执行是在 Listener 那里定义的:** ## Event Subscriber(事件訂閱者) Event Subscribers 是一种特殊的 Listener, 前面讲的是一个 listener 里只能放一个 hander(),事件订阅可以把很多处理器(handler)放到一个类里面,然后用一个 listner 把它们集合起来,这样不同的事件只要对应一个 listner 就可以了。 一個地方訂閱多個事件監聽器 ``` <?php // app/Listeners/ExampleEventSubscriber.php namespace App\Listeners; class ExampleEventSubscriber { /** * Handle user login events. */ public function sendEmailNotification($event) { // get logged in username $email = $event->user->email; $username = $event->user->name; // send email notification about login... } /** * Handle user logout events. */ public function warmUpCache($event) { if (isset($event->cache_keys) && count($event->cache_keys)) { foreach ($event->cache_keys as $cache_key) { // generate cache for this key // warm_up_cache($cache_key) } } } /** * Register the listeners for the subscriber. * * @param Illuminate\Events\Dispatcher $events */ public function subscribe($events) { $events->listen( 'Illuminate\Auth\Events\Login', 'App\Listeners\ExampleEventSubscriber@sendEmailNotification' ); $events->listen( 'App\Events\ClearCache', 'App\Listeners\ExampleEventSubscriber@warmUpCache' ); } } ``` 看后面的 subscribe (),每个事件和处理器是一一对应的。 绑定 Event Subscriber 到 Service Provider 它subscribe是負責註冊偵聽器的方法。該subscribe方法的第一個參數是Illuminate\Events\Dispatcher您可以使用該listen方法將事件與偵聽器綁定的類的實例 。 (感覺像單利模式那樣 laravel kenerl那種註冊的設計模式) 該listen方法的第一個參數是您要偵聽的事件,第二個參數是引發事件時將調用的偵聽器。 **寫好一樣要註冊喔** ``` <?php namespace App\Providers; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The subscriber classes to register. * * @var array */ protected $subscribe = [ 'App\Listeners\ExampleEventSubscriber', ]; /** * Register any events for your application. * * @return void */ public function boot() { parent::boot(); // } } ``` ## 不同時區 在正確的時間發送事件提醒 https://www.youtube.com/watch?v=2Eb0_lIQZk0 ## Listenser指定queue ``` <?php namespace App\Listeners; use App\Events\OrderShipped; use Illuminate\Contracts\Queue\ShouldQueue; class SendShipmentNotification implements ShouldQueue { public $queue = 'listeners'; } ``` Answer: 在 listener class 中使用 $queue 指定該 listener 使用的 queue ###### tags: `Laravel`