Laravel 中的 Event 和事件的概念

lizhiqiang666發表於2018-12-06

概述

事件是一種常見的觀察者模式的應用。簡單的來說,就是當...幹...。這個當...和幹...在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就可以了。

具體的建議大家去看看手冊吧,有些內容我這裡就不完全說了。我只是拋磚引玉

相關文章