Laravel中的Event机制

生来就可爱ヽ(ⅴ<●) 提交于 2020-03-17 01:24:25

某厂面试归来,发现自己落伍了!>>>

Laravel 的 事件(Event) 提供了简单的 观察者模式 实现,允许你 订阅 和 监听 应用中的事件。

事件类通常存放在 app/Events 目录,监听器存放在 app/Listeners

事件为应用功能模块解耦提供了行之有效的解决办法,因为单个事件可以有多个监听器,一个监听器也可以监听多个事件,而这些事件之间,监听器之间并不相互依赖。

1,事件 放在 app/Events目录下,比如

<?php

namespace App\Events\Order\Shipped;

use App\Models\Order as OrderModel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;

class Shipped
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * @var $order OrderModel
     */
    public $order;

    public $goodsId;

    public function __construct(OrderModel $order)
    {
        $this->order = $order;
        $this->goodsId = $order->goods_id;
    }
}

事件类是一个处理与事件相关的简单数据容器例如在这里,假设我们生成的 OrderShipped 事件接收一个 Eloquent ORM 对象,OrderModel, 该事件类不包含任何特定逻辑,只是一个存放被购买的 Order 对象的容器,如果事件对象被序列化的话,事件使用的 SerializesModels trait 将会使用 PHP 的 serialize 函数序列化所有 Eloquent 模型。 Eloquent 模型将会在事件被执行时优雅地序列化和反序列化。如果你的事件在构造函数中接收 Eloquent 模型,只有模型的主键会被序列化到队列,当任务真正被执行的时候,事件系统会自动从数据库中获取整个模型实例。这对应用而言是完全透明的,从而避免序列化整个 Eloquent 模型实例引起的问题。 

 

2, 监听器 放在 app/Listeners目录下,比如

<?php

namespace App\Listeners\Order;

use App\Events\Order\Shipped as EventOrderShipped;
use App\Jobs\Notify\ShippedNotify;
use Illuminate\Contracts\Queue\ShouldQueue;

class Shipped implements ShouldQueue
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param object $event
     * @return void
     */
    public function handle(EventOrderShipped $event)
    {
        //发送模板消息
        dispatch(new ShippedNotify($event->order));
    }
}

3,绑定监听 在 app/Providers/EventServiceProvider 中

<?php

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;

use App\Listeners\QueryListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

use App\Events\Order\{
    Shiped as OrderShipedEvent
};
use App\Listeners\{
    Order\Shipped as OrderShippedListener
};

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [

        Registered::class => [
            SendEmailVerificationNotification::class, //这一对事件和监听器是框架自带的示例
        ],
       
        OrderShippedEvent::class => [
            OrderShippedListener::class, //这是我们自己绑定的事件和监听器
            //other listeners
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }

注意: 监听器是按照从上到下的先后顺序执行的,前面的监听器执行完之后再执行后面的。如果其中一个监听器 return false 了,则当前事件将被中断,不再被后面的监听器监听到。

上面这种是比较“笨拙”的方式。更有效率的方式是,直接在 EventServiceProvider的 $listener 中 添加上要创建的Event和Listener,并指明目录,比如添加

protected $listen = [
    OrderShippedEvent::class => [
        OrderShippedListener::class, //已存在的listener
        \App\Listeners\Others\Other::class //本次新增的
    ],
];

然后在终端执行

php artisan event::generate

然后就会发现,在app/Listenners/Others/目录下 自动生成了 Other.php 的类文件,如下

<?php

namespace App\Listeners\Others;

use App\Events\Order\Shipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class Other
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  Shipped  $event
     * @return void
     */
    public function handle(Shipped $event)
    {
        //停止事件处理
        return false;

        //处理业务逻辑
        logger()->channel('abc')->info('to process order:'.$event->order->id);
        var_dump($event->goodsId);

    }
}

停止事件继续往下传播 有时候,你希望停止事件被传播到其它监听器,你可以通过从监听器的 handle 方法中返回 false 来实现。

注意:当使用SerializesModels trait时 可以减少 事件系统中所传递的数据大小。但是,由于它只传递了主键,然后再使用主键去表中获取数据,如果该数据在事件被监听到并被处理之前 发生了改变,则会导致 进入事件的模型数据 和 事件被处理时的模型数据 不一致。特别是 当前事件是处在一个事务中,且这个事件一个异步的事件,比如 处在队列中时,前面的事务更改了模型的属性,在提交事务之前,加入异步事务。如果事件比异步事务更晚被处理,则事件被处理时,模型的属性仍未做变更,此时数据仍然是一致的,但会引起一个问题,比如事务是将订单从未发货状态改为已发货状态,然而事件去读的时候,发现仍然是未发货状态。但如果事务更快被处理完,则属性被更改,异步事务后续读到的属性也是被更改后的,这将引起数据的不一致性。

所以我们一定要注意事件的使用场景,事件是用来做解耦的,它既可以做同步操作,也可以把它放入队列,做异步操作。

 

事件监听器队列

如果监听器将要执行耗时任务比如发送邮件或者发送 HTTP 请求,那么将监听器放到队列是一个不错的选择。在队列化监听器之前,确保已经配置好队列并且在服务器或本地环境启动一个队列监听器。 要指定某个监听器需要放到队列,只需要让监听器类实现 ShouldQueue 接口即可,通过 Artisan 命令 event:generate 生成的监听器类已经将这个接口导入当前命名空间,所有你可以直接拿来使用:

<?php

namespace App\Listeners\Others;

use App\Events\Order\Shipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class Other implements ShouldQueue
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  Shipped  $event
     * @return void
     */
    public function handle(Shipped $event)
    {
        //
    }
}

就是这么简单!当这个监听器被调用的时候,将会使用 Laravel 的队列系统通过事件分发器自动推送到队列。如果通过队列执行监听器的时候没有抛出任何异常,队列任务会在执行完成后被自动删除。 

自定义队列连接 & 队列名称 如果你想要自定义事件监听器使用的队列连接和队列名称,可以在监听器类中定义 $connection  $queue 属性:

<?php

namespace App\Listeners;

use App\Events\Order\Shipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    /**
     * 任务将被推送到的连接名称.
     *
     * @var string|null
     */
    public $connection = 'sqs';

    /**
     * 任务将被推送到的连接名称.
     *
     * @var string|null
     */
    public $queue = 'listeners';
}

手动访问队列

如果你需要手动访问底层队列任务的 delete  release 方法,在生成的监听器中,默认导入的 Illuminate\Queue\InteractsWithQueue trait 为这两个方法提供了访问权限:

<?php

namespace App\Listeners;

use App\Events\Order\Shipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event)
    {
        if (true) {
            $this->release(30);
        }
    }
}

其中 release 方法是将 事件 重新放入 事件队列中(参数是延迟秒数),deleted 方法 是将事件从事件队列中删除。

源代码如下

    /**
     * Release the job back into the queue.
     *
     * @param  int   $delay
     * @return void
     */
    public function release($delay = 0)
    {
        if ($this->job) {
            return $this->job->release($delay);
        }
    }

    /**
     * Delete the job from the queue.
     *
     * @return void
     */
    public function delete()
    {
        if ($this->job) {
            return $this->job->delete();
        }
    }

处理失败任务

有时候队列中的事件监听器可能会执行失败。如果队列中的监听器任务执行时超出了队列进程定义的最大尝试次数,监听器上的 failed 方法会被调用,failed 方法接收事件实例和导致失败的异常:

<?php

namespace App\Listeners;

use App\Events\Order\Shipped;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(OrderShipped $event)
    {
        //
    }

    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}

分发事件

要分发一个事件,可以传递事件实例到辅助函数 event,这个辅助函数会分发事件到所有注册的监听器。由于辅助函数 event 全局有效,所以可以在应用的任何地方调用它:

<?php

namespace App\Http\Controllers;

use App\Order;
use App\Events\Order\Shipped as EventOrderShipped;
use App\Http\Controllers\Controller;

class OrderController extends Controller
{
    /**
     * 处理给定订单.
     *
     * @param  int  $orderId
     * @return Response
     */
    public function ship($orderId)
    {
        $order = Order::findOrFail($orderId);

        // 订单处理逻辑...

        event(new EventOrderShipped($order));
    }
}

 

这样,大概的流程就是,在业务层发起事务-> ORM模型被序列化(__sleep())放入事件中->事务被EventServiceProvider 监听到-> 调用监听器列表 -> 各监听器从上到下依次执行-> 事件到达第一个监听器 -> ORM模型从事件中被反序列化(__wakeup())出来 -> 监听器执行业务 -> 如果有队列,则handle中release/delete等可以使用,与handle同级有fail()可使用->如果return false,则本次事件结束,后续监听器将不能再监听到本次事件。

总结

1,Event 的使用场景,就是 解耦,将一些和当前业务没有很大关联的边支逻辑,从主业务逻辑中解耦出来,当某些主业务发生时,可以用事件来处理这些边支逻辑。比如,发信息,当订单支付的时候可以发信息,订单发货的时候也可以发信息,而订单的这些业务操作,和发信息是没有太多耦合关系的,那么就可以把 发信息 做成一个事件,每次有订单支付,订单发货,都可以调用此事件。这是Event端。在 监听器端,比如发信息,可以发短息,发微信,发邮件等等,则这个 发信息的事件,就可以对应多个 监听器了。

2,Event 当不使用 ShouldQueue时,它是一个同步操作。当监听器实现了ShouldQueue时,如果 queue.php 这个配置文件中的 default 值是 sync ,它仍然是同步操作。只有变更 default 或者 QUEUE_DRIVER 的值,比如 redis 时,它才是一个异步操作。

3,一个Event可被多个Listener监听,多个Listener会按照先后顺序依次执行,除非被 return false 中断。

4,Event 关注的是 已经发送过的事情,用在已经完成的事情,类似于英语的过去式。如 注册完成,更新完成,发布完成,发送完成。而 Job 关注 进行中的事情。

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!