天天看点

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;
    }
}
           

自定义的事件需要添加到

EventServiceProvider

listen

数组中,在laravel启动中自动被加载到

Illuminate\Events\Dispatcher

中。

在客户端代码中使用

Event::listen

方式进行注册,以fpm模式运行的laravel工程,仅在当前请求过程中有效。

下次请求会重新创建

Illuminate\Events\Dispatcher

类和重新从

EventServiceProvider

listen

数组中加载注册事件。

定义监听器

接下来,让我们看一下例子中事件的监听器。事件监听器在

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 ...
    }
}
           

Dispatcher

makeListener

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

/**
     * 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

方法内调用了

Dispatcher

dispatch

方法,下面是

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

方法将从

listeners

wildcards

数组中取出包括使用通配符事件名的监听器:

/**
     * 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\[email protected]'
        );

        $events->listen(
            'Illuminate\Auth\Events\Logout',
            'App\Listeners\[email protected]'
        );
    }

}
           

事件订阅者的主要方法是

subscribe

Dispatcher

类中并没有专门存储订阅者的数组,要在

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);
        }
    }
           

Dispatcher

subscribe

方法调用了订阅者的

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;
    }
           

最终执行的还是

Dispatcher

listen

方法。