laravel综合话题——事件系统

简介

Laravel 的事件提供了一个简单的观察者实现,能够订阅和监听应用中发生的各种事件。事件类保存在 app/Events 目录中,而这些事件的监听器则被保存在 app/Listeners 目录下。这些目录只有当你使用 Artisan 命令来生成事件和监听器时才会被自动创建。

观察者模式的核心是将客户端组件(观察者)从中心类(主体)中分离出来。

  • 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

  • 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

  • 何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

  • 如何解决:使用面向对象技术,可以将这种依赖关系弱化。

  • 关键代码:在抽象类里有一个 ArrayList 存放观察者们

  • 应用实例: 1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。 2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。

事件系统的执行过程大致为:

  1. 定义事件和监听者;
  2. 关联事件和监听者;
  3. 客户端程序中分发事件;
  4. 监听者程序被调用;

注册事件和监听器

Laravel 应用中的 EventServiceProvider 有个 listen 数组包含所有的事件(键)以及事件对应的监听器(值)来注册所有的事件监听器,可以灵活地根据需求来添加事件。例如,让我们增加一个 OrderShipped 事件:

/**
 * 应用程序的事件监听器映射。
 *
 * @var array
 */
protected $listen = [
    'App\Events\OrderShipped' => [
        'App\Listeners\SendShipmentNotification',
    ],
];

EventServiceProvider为服务提供者类,在config/app.php文件中作为providers数组的元素被使用,laravel启动过程中,在Illuminate\Foundation\Application::registerConfiguredProviders方法中被加载。
EventServiceProvider父类Illuminate\Foundation\Support\Providers\EventServiceProvider方法boot中,读取listen数组加载到laravel的事件系统中:

    /**
     * Register the application's event listeners.
     *
     * @return void
     */
    public function boot()
    {
        foreach ($this->listens() as $event => $listeners) {
            foreach ($listeners as $listener) {
                Event::listen($event, $listener);
            }
        }

        foreach ($this->subscribe as $subscriber) {
            Event::subscribe($subscriber);
        }
    }

Illuminate\Support\Facades\Event通过魔术方法把调用Illuminate\Events\Dispatcher的相应方法;Illuminate\Events\Dispatcher对象中保存者事件与监听者的关联关系;

手动注册事件

事件通常是在 EventServiceProvider 类的 $listen 数组中注册,但是,你也可以在 EventServiceProvider 类的boot 方法中注册基于事件的闭包。

/**
 * 注册应用程序中的任何其他事件。
 *
 * @return void
 */
public function boot()
{
    parent::boot();

    Event::listen('event.name', function ($foo, $bar) {
        //
    });
}

listen方法中将会以事件名为键名、一个闭包函数(监听器)为键值添加到listeners属性数组中。

通配符事件监听器

你可以在注册监听器时使用 * 通配符参数,这样能够在同一个监听器上捕获多个事件。通配符监听器接受事件名称作为其第一个参数,并将整个事件数据数组作为其第二个参数:

Event::listen('event.*', function ($eventName, array $data) {
    //
});

使用事件名中含有通配符的事件将会被添加到$wildcards属性数组中。

定义事件

事件类其实就只是一个保存与事件相关的信息的数据容器。例如,假设我们生成的 OrderShipped 事件接收一个 Eloquent ORM 对象:

<?php

namespace App\Events;

use App\Order;
use Illuminate\Queue\SerializesModels;

class OrderShipped
{
    use SerializesModels;

    public $order;

    /**
     * 创建一个事件实例。
     *
     * @param  Order  $order
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
}

自定义的事件需要添加到EventServiceProviderlisten数组中,在laravel启动中自动被加载到Illuminate\Events\Dispatcher中。
在客户端代码中使用Event::listen方式进行注册,以fpm模式运行的laravel工程,仅在当前请求过程中有效。
下次请求会重新创建Illuminate\Events\Dispatcher类和重新从EventServiceProviderlisten数组中加载注册事件。

定义监听器

接下来,让我们看一下例子中事件的监听器。事件监听器在 handle 方法中接收事件实例。 event:generate 命令会自动加载正确的事件类和在 handle加入的类型提示。在 handle 方法中,你可以执行任何必要的响应事件的操作:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;

class SendShipmentNotification
{
    /**
     * 创建事件监听器。
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * 处理事件
     *
     * @param  OrderShipped  $event
     * @return void
     */
    public function handle(OrderShipped $event)
    {
        // 使用 $event->order 来访问 order ...
    }
}

DispatchermakeListener会根据监听器类型(类还是闭包函数)包装监听器:

    /**
     * Register an event listener with the dispatcher.
     *
     * @param  \Closure|string  $listener
     * @param  bool  $wildcard
     * @return \Closure
     */
    public function makeListener($listener, $wildcard = false)
    {
        if (is_string($listener)) {
            return $this->createClassListener($listener, $wildcard);
        }

        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return $listener($event, $payload);
            }

            return $listener(...array_values($payload));
        };
    }

如果listen方法参数中的监听器是类,将会执行createClassListener方法包装成一个闭包函数:

    /**
     * Create a class based listener using the IoC container.
     *
     * @param  string  $listener
     * @param  bool  $wildcard
     * @return \Closure
     */
    public function createClassListener($listener, $wildcard = false)
    {
        return function ($event, $payload) use ($listener, $wildcard) {
            if ($wildcard) {
                return call_user_func($this->createClassCallable($listener), $event, $payload);
            }

            return call_user_func_array(
                $this->createClassCallable($listener), $payload
            );
        };
    }

    /**
     * Create the class based event callable.
     *
     * @param  string  $listener
     * @return callable
     */
    protected function createClassCallable($listener)
    {
        list($class, $method) = $this->parseClassCallable($listener);

        if ($this->handlerShouldBeQueued($class)) {
            return $this->createQueuedHandlerCallable($class, $method);
        }

        return [$this->container->make($class), $method];
    }

    /**
     * Parse the class listener into class and method.
     *
     * @param  string  $listener
     * @return array
     */
    protected function parseClassCallable($listener)
    {
        return Str::parseCallback($listener, 'handle');
    }

createClassCallable方法会区分监听器是否应该被放入队列中执行。
parseClassCallable方法解析监听器字符串。如果只有类名,将使用handle方法作为该监听器类的回调方法。可以通过@分割类名和方法名,指定要回调的方法名称。

停止事件传播

你可以通过在监听器的 handle 方法中返回 false 来阻止事件被其他的监听器获取。

事件监听器队列

如果你的监听器中要执行诸如发送邮件或者进行 HTTP 请求等比较慢的任务,你可以选择将其丢给队列处理。在开始使用监听器队列之前,请确保在你的服务器或本地开发环境中能够配置并启动 队列 监听器。

要指定监听器启动队列,只需将 ShouldQueue 接口添加到监听器类。由 Artisan 命令 event:generate 生成的监听器已经将此接口导入到当前命名空间中,因此你可以直接使用它:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendShipmentNotification implements ShouldQueue
{
    //
}

在使用createClassCallable方法创建类回调方法时,通过handlerShouldBeQueued方法检测该类是否实现了ShouldQueue接口,从而判断是否应该放入队列中执行。

    /**
     * Determine if the event handler class should be queued.
     *
     * @param  string  $class
     * @return bool
     */
    protected function handlerShouldBeQueued($class)
    {
        try {
            return (new ReflectionClass($class))->implementsInterface(
                ShouldQueue::class
            );
        } catch (Exception $e) {
            return false;
        }
    }

createQueuedHandlerCallable将会返回一个基于队列执行的回调函数:

    /**
     * Create a callable for putting an event handler on the queue.
     *
     * @param  string  $class
     * @param  string  $method
     * @return \Closure
     */
    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());

            if ($this->handlerWantsToBeQueued($class, $arguments)) {
                $this->queueHandler($class, $method, $arguments);
            }
        };
    }

队列执行是一个比较复杂的过程。

分发事件

如果要分发事件,你可以将事件实例传递给辅助函数 event。这个函数将会把事件分发到所有已经注册的监听器上。因为辅助函数 event 是全局可访问的,所以你可以在应用中的任何地方调用它:

<?php

namespace App\Http\Controllers;

use App\Order;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;

class OrderController extends Controller
{
    /**
     * 将传递过来的订单发货。
     *
     * @param  int  $orderId
     * @return Response
     */
    public function ship($orderId)
    {
        $order = Order::findOrFail($orderId);

        // 订单的发货逻辑...

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

下面是辅助方法event的定义:

if (! function_exists('event')) {
    /**
     * Dispatch an event and call the listeners.
     *
     * @param  string|object  $event
     * @param  mixed  $payload
     * @param  bool  $halt
     * @return array|null
     */
    function event(...$args)
    {
        return app('events')->dispatch(...$args);
    }
}

event方法内调用了Dispatcherdispatch方法,下面是dispatch方法的源代码:

    /**
     * Fire an event and call the listeners.
     *
     * @param  string|object  $event
     * @param  mixed  $payload
     * @param  bool  $halt
     * @return array|null
     */
    public function dispatch($event, $payload = [], $halt = false)
    {
        // When the given "event" is actually an object we will assume it is an event
        // object and use the class as the event name and this event itself as the
        // payload to the handler, which makes object based events quite simple.
        list($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 a response is returned from the listener and event halting is enabled
            // we will just return this response, and not call the rest of the event
            // listeners. Otherwise we will add the response on the response list.
            if ($halt && ! is_null($response)) {
                return $response;
            }

            // If a boolean false is returned from a listener, we will stop propagating
            // the event to any further listeners down in the chain, else we keep on
            // looping through the listeners and firing every one in our sequence.
            if ($response === false) {
                break;
            }

            $responses[] = $response;
        }

        return $halt ? null : $responses;
    }

dispatch主要功能是遍历执行该事件关联的监听器。
可以发现当事件监听器回调方法返回false时,将跳出循环,不再触发之后的监听器。
getListener方法将从listenerswildcards数组中取出包括使用通配符事件名的监听器:

    /**
     * Get all of the listeners for a given event name.
     *
     * @param  string  $eventName
     * @return array
     */
    public function getListeners($eventName)
    {
        $listeners = $this->listeners[$eventName] ?? [];

        $listeners = array_merge(
            $listeners, $this->getWildcardListeners($eventName)
        );

        return class_exists($eventName, false)
                    ? $this->addInterfaceListeners($eventName, $listeners)
                    : $listeners;
    }

事件订阅者

编写事件订阅者

事件订阅者是一个可以在自身内部订阅多个事件的类,即能够在单个类中定义多个事件处理器。订阅者应该定义一个 subscribe 方法,这个方法接受一个事件分发器的实例。你可以调用给定的事件分发器上的 listen 方法来注册事件监听器:

<?php

namespace App\Listeners;

class UserEventSubscriber
{
    /**
     * 处理用户登录事件。
     */
    public function onUserLogin($event) {}

    /**
     * 处理用户注销事件。
     */
    public function onUserLogout($event) {}

    /**
     * 为订阅者注册监听器。
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen(
            'Illuminate\Auth\Events\Login',
            'App\Listeners\UserEventSubscriber@onUserLogin'
        );

        $events->listen(
            'Illuminate\Auth\Events\Logout',
            'App\Listeners\UserEventSubscriber@onUserLogout'
        );
    }

}

事件订阅者的主要方法是subscribeDispatcher类中并没有专门存储订阅者的数组,要在subscribe中使用Dispacher::listen方法一个个进行事件注册。

注册事件订阅者

订阅者写好后,就将其注册到事件分发器中。你可以在 EventServiceProvider 类的 $subscribe 属性中注册订阅者。例如,将 UserEventSubscriber 添加到数组列表中:

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * 应用中事件监听器的映射。
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * 需要注册的订阅者类。
     *
     * @var array
     */
    protected $subscribe = [
        'App\Listeners\UserEventSubscriber',
    ];
}

EventServiceProvider的父类方法中,把定义的该数组传递给Dispatcher

    /**
     * Register the application's event listeners.
     *
     * @return void
     */
    public function boot()
    {
        foreach ($this->listens() as $event => $listeners) {
            foreach ($listeners as $listener) {
                Event::listen($event, $listener);
            }
        }

        foreach ($this->subscribe as $subscriber) {
            Event::subscribe($subscriber);
        }
    }

Dispatchersubscribe方法调用了订阅者的subscribe方法:

    /**
     * Register an event subscriber with the dispatcher.
     *
     * @param  object|string  $subscriber
     * @return void
     */
    public function subscribe($subscriber)
    {
        $subscriber = $this->resolveSubscriber($subscriber);

        $subscriber->subscribe($this);
    }

    /**
     * Resolve the subscriber instance.
     *
     * @param  object|string  $subscriber
     * @return mixed
     */
    protected function resolveSubscriber($subscriber)
    {
        if (is_string($subscriber)) {
            return $this->container->make($subscriber);
        }

        return $subscriber;
    }

最终执行的还是Dispatcherlisten方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值