标签归档:事件

Laravel 事件详解

https://github.com/illuminate/events

一个Laravel应用就是一个大容器,容器首先需要构建:

// bootstrap/app.php
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

// Illuminate\Foundation\Application构造函数
// 注意$this->registerBaseServiceProviders()
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    public function __construct($basePath = null)
    {
        if ($basePath) {
            $this->setBasePath($basePath);
        }

        $this->registerBaseBindings();

        $this->registerBaseServiceProviders();

        $this->registerCoreContainerAliases();
    }

    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));

        $this->register(new LogServiceProvider($this));

        $this->register(new RoutingServiceProvider($this));
    }
}

容器的register方法不过是调用具体的ServiceProvider的register方法,然后在调用具体的ServiceProvider的boot方法(如果没有boot过)。可见之类的Event,Log,Routing是核心服务提供者。

Illuminate\Events\EventServiceProvider:

class EventServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('events', function ($app) {
            return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
                return $app->make(QueueFactoryContract::class);
            });
        });
    }
}

往容器中注入绑定,events对应Illuminate\Events\Dispatcher类实例。这里的Illuminate\Events\Dispatcher类仅仅几百行代码,就是事件机制实现的全部。

总体上,首先是定义事件(一个事件类,用于承载事件的数据,类名就是事件名称,另一类是没有对应类,仅一个字符串,表示事件名称),事件可以对应一个或多个监听器,监听器接收事件对象(事件载体就是事件类对象,如果非事件类,则是自定义数据),进行代码处理,然后是触发事件,那么对应的监听器就会被执行。

添加事件的监听器:

    public function listen($events, $listener)
    {
        foreach ((array) $events as $event) {
            if (Str::contains($event, '*')) {
                $this->setupWildcardListen($event, $listener);
            } else {
                $this->listeners[$event][] = $this->makeListener($listener);
            }
        }
    }
// 用法1
dispatch.listen("TestEvent", $listener)
dispatch.listen("TestEvent2", $listener)
$this->listeners["TestEvent"][] = $listener;
$this->listeners["TestEvent2"][] = $listener;
// 用法2
dispatch.listen("TestEvent*", $listener)
$this->wildcards["TestEvent*"][] = $listener;
// 用法3
dispatch.listen(["TestEvent", "TestEvent*"], $listener)
$this->listeners["TestEvent"][] = $listener;
$this->wildcards["TestEvent*"][] = $listener;

如果要设置一批事件对应同一个监听器,可以传递一个事件数组。另外,事件名称对应的监听器是一个数组,如果多次调用,就多次添加监听器,哪怕监听器是一样的。

分发器的hasListeners不过是判断事件名称是否在$this->listeners或$this->wildcards中

    public function hasListeners($eventName)
    {
        return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]);
    }

添加了事件监听器,但是事件监听器实际被makeListener方法进行规范处理:

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

    // 1 监听器是字符串的情况,也是返回闭包,在执行闭包时,也是根据事件类型($wildcard),调用方法有所差别
    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
            );
        };
    }
    
    // 从字符串解析出类目和需要执行的方法
    // 通过类反射,判断类是否实现了Illuminate\Contracts\Queue\ShouldQueue接口(该接口未定义方法)
    // 如果实现了该接口,返回一个闭包函数,这个作为call_user_func或call_user_func_array的第一参数,事件错误时,监听器代码被推入队列
    // 根据类名生成对象,按照[类对象,类方法]返回,这个作为call_user_func或call_user_func_array的第一参数
    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];
    }

    // Test@fire => ["Test", "fire"],表示要调用fire方法
    // 如果把包含@,比如Test,那么返回["Test", "handle"],表示要调用handle方法 
    protected function parseClassCallable($listener)
    {
        return Str::parseCallback($listener, 'handle');
    }

如果传递进来的$listener是字符串,解析字符串构建监听器实例。否则就认为是一个函数(或是闭包函数)。注意这里返回的是一个统一格式的闭包,第一个是事件名称(或事件对象),第二个参数是事件载体(payload),如果事件是一个类名称,它对应的对象就是载体,如果已经是事件对象,那么这个事件对象即是事件,也是事件对应的载体(这个情况在触发事件时,直接触发事件对象即可)。

事件触发:

public function dispatch($event, $payload = [], $halt = false)
    {
        // 如果传递进来的$event是对象,那么$payload设置为等于该对象,然后把$event改为该对象的类名
        // 如果传递进来的$event不是对象,$payload就是对应的载体
        list($event, $payload) = $this->parseEventAndPayload(
            $event, $payload
        );
        // 通过载体,判断是否需要广播($payload是一个数组,第一个元素理论上对应事件对象,如果事件对象声明要广播)
        if ($this->shouldBroadcast($payload)) {
            $this->broadcastEvent($payload[0]);
        }

        $responses = [];
        // 根据事件取回监听器(按照预期格式生成,格式统一的闭包),循环执行监听器
        foreach ($this->getListeners($event) as $listener) {
            $response = $listener($event, $payload);

            // $halt用来判断一旦有输出,就返回
            if ($halt && ! is_null($response)) {
                return $response;
            }

            // 如果返回false,说明后续的监听器不在运行
            if ($response === false) {
                break;
            }

            $responses[] = $response;
        }
        return $halt ? null : $responses;
    }

事件触发都是直接或间接调用该方法。如果一个监听器是需要推入队列的,那么这个时候会生成一个job推入队列。如果一个事件声明要广播的,也会在这里处理。另外,框架中有一个全局方法对应这个方法,event(), 直接调用event(‘事件或事件对象’)。

事件触发时,需要取回监听器:

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

这个就是数组合并。
注意:getWildcardListeners()这个方法,实际是根据给定的事件名称,检查是否和通配事件名称匹配,如果匹配,对应的监听器也返回。
注意:$this->addInterfaceListeners($eventName, $listeners),这个举例来说明,比如B实现了A接口,如果A接口指定了监听器,B也对应了监听器,取回B事件对应的监听器时,也包含A的监听器(这个就是基于接口的事件)。

以上是添加事件监听器,事件监听器的构建,触发事件,以及触发事件时如何取回监听器。总体上就是把找到事件对应的监听器,然后把事件对象传递给监听器,监听器根据事件对象取回数据进行逻辑处理。

用法:

// 1 触发事件对象时,可以直接传递事件对象(常用),事件对象会直接传入监听器方法
# 定义了事件对象
class TestEvent {}
$testEvent = new TestEvent();
# 用法
dispatch($testEvent); #实际会解析成dispatch("TestEvent", $testEvent)
dispatch("TestEvent", $testEvent);

// 2 触发的事件非事件对象(只是一个名字)$payload会传入自定义的方法中(监听器,可以是闭包函数),这个需要根据具体情况编写
dispatch("aaaaa", $payload)

还有特殊的搞法:

    public function push($event, $payload = [])
    {
        $this->listen($event.'_pushed', function () use ($event, $payload) {
            $this->dispatch($event, $payload);
        });
    }

    public function flush($event)
    {
        $this->dispatch($event.'_pushed');
    }

    public function forgetPushed()
    {
        foreach ($this->listeners as $key => $value) {
            if (Str::endsWith($key, '_pushed')) {
                $this->forget($key);
            }
        }
    }

先push,然后flush直接分发。

另外有一个订阅的概念:

    public function subscribe($subscriber)
    {
        $subscriber = $this->resolveSubscriber($subscriber);

        $subscriber->subscribe($this);
    }

取到订阅器对象,执行订阅器的subscribe()方法。实际不过是一个二次包装。但是需要有一个地方运行分发器的subscribe()方法。这个在框架中:

#文件: app/Providers/EventServiceProvider.php
<?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',
    ];
}

定义订阅器:

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

}

这个不过是定义事件与监听器的另一种方式而已,完成可以把对应关系直接写入到$listen数组中。

其它用法(来自文档):

#文件: app/Providers/EventServiceProvider.php
<?php

namespace App\Providers;

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

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        'App\Events\OrderShipped' => [
            'App\Listeners\SendShipmentNotification',
        ],
    ];
}
// 生成对应的事件对象和监听器对象
php artisan event:generate

对应生成的事件类,可以编写其它逻辑,让其承载数据,而生成的监听器的handle方法必须接收这个事件对象。

// 定义事件
<?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;
    }
}

// 监听器
<?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 发访问订单...
    }
}

除了直接声明对应关系,也可以直接编写:

public function boot()
{
    parent::boot();

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

通配符事件监听器:

// 闭包特殊,第一参数必须是事件名称,第二参数是承载的数据(事件对象)
$events->listen('event.*', function ($eventName, array $data) {
    //
});

如果希望事件往下传播,监听器的handle方法返回false即可。

如果希望监听器异步运行,只要监听器类实现ShouldQueue接口(空接口,不需要实现任何方法)即可:

<?php

namespace App\Listeners;

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

class SendShipmentNotification implements ShouldQueue
{
    //
}

如果希望定义监听器进入的队列已经队列名称:

<?php

namespace App\Listeners;

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

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

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

如果监听器还要和队列进行交互,需要使用Illuminate\Queue\InteractsWithQueue:

<?php

namespace App\Listeners;

use App\Events\OrderShipped;
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);
        }
    }

    // 处理失败job
    public function failed(OrderShipped $event, $exception)
    {
        //
    }
}