Laravel 中的 Event 和事件的概念

概述

事件是一种常见的观察者模式的应用。简单的来说,就是当...干...。这个当...和干...在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就可以了。

具体的建议大家去看看手册吧,有些内容我这里就不完全说了。我只是抛砖引玉

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
lizhiqiang666
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 16

不明觉厉,挺厉害的

5年前

artisan 创建观察者的时候,提示命令不存在是什么问题?
这里是我的命令

$ php artisan make:observer UserObserver --model=User

下面是报错的提示

 Command "make:observer" is not defined.

  Did you mean one of these?
      make:auth
      make:command
      make:controller
      make:event
      make:exception
      make:factory
      make:job
      make:listener
      make:mail
      make:middleware
      make:migration
      make:model
      make:notification
      make:policy
      make:provider
      make:request
      make:resource
      make:rule
      make:seeder
      make:test
5年前

@houxin 版本差别啦

5年前

@overtrue 好吧,确实是

5年前

讲的很透彻了。。。包括使用场景很具体了。3Q.

5年前

:smile: 学到了

5年前

学习了!感谢

4年前

学习了 感谢分享

4年前

感谢分享,通过使用场景讲解,很受用,感谢!

4年前

很透彻,学习了,感谢。

3年前

看了半天,没看懂,可能是资质愚钝吧

3年前
未进化的类人猿

laravel里的事件实现不是利用观察者模式么?

3年前 评论
NoTurningBack 2年前

我想问下,event中操作数据的时候如何保持数据的完整性呢

2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
技术负责人 @ 某某
文章
91
粉丝
209
喜欢
906
收藏
1030
排名:25
访问:24.1 万
私信
所有博文
社区赞助商