【Laravel系列7.5】事件系统

2023-03-03 13:32:43 浏览数 (3)

事件系统

说到事件,你会想到什么?JS 中的回调函数,按扭的回调事件?没错,这些都是事件的应用。不过在 Laravel 中,事件是一种解耦机制,是 观察者 模式的一种体现。它能够允许你订阅和监听在你的应用中发生的各种事件。最典型的例子,当你操作完订单后,需要发送短信、邮件或者应用内通知的时候,我们一般就会使用观察者模式来实现。而事件,则是对这一操作的封装,非常方便好用。

注册事件和监听器

首先我们需要创建事件和事件对应的监听器。你可以将 事件 看做是一个订阅者,然后利用监听器来对订阅的内容进行处理。一般来说,事件位于 app/Events 目录下,而 监听器 位于 app/Listeners 目录下。如果你是新安装的 Laravel 环境,可能没有这两个目录,那么我们可以手动建立,也可以直接使用命令行生成对应文件,这些目录会被自动创建。

如果你自己创建事件相关的文件类的话,需要自己去实现一些固定的方法,相对来说,命令行的方式创建会更方便一些。

代码语言:javascript复制
php artisan make:event TestEvent 

php artisan make:listener TestListener --event=TestEvent

在这里,我们利用命令行的 make:event 创建事件类,然后再使用 make:listener 创建一个监听器。我们为监听器指定了它要处理的事件对象,也就是后面传递的参数。生成的文件内容如下所示:

代码语言:javascript复制
// app/Events/TestEvent.php
class TestEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct()
    {
        //
    }

    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

// app/Listeners/TestListener.php
class TestListener
{
    public function __construct()
    {
        //
    }

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

简单的事件和监听器就完成了,你可以看到在监听器中,我们的 handle() 方法接收的参数就是一个 TestEvent 对象。接下来我们需要去 app/Providers/EventServiceProvider.php 的 listen 数组变量中注册这个事件和监听器。

代码语言:javascript复制
protected $listen = [

    TestEvent::class => [
        TestListener::class
    ],

    // …………
];

这样,整个事件和监听器的设置就完成了。

测试调用事件

要调用事件,我们先要让事件和监听器有点事可干。那么我们就简单地输出一点东西就好了。可以在事件的构造函数中添加一个变量。

代码语言:javascript复制
// app/Events/TestEvent.php
public $a = '';

public function __construct($a)
{
    //
    $this->a = $a;
}

然后在监听器中输出这个变量的值。

代码语言:javascript复制
// app/Listeners/TestListener.php
public function handle(TestEvent $event)
{
    //
    echo $event->a;
}

然后我们在路由中去触发这个事件。

代码语言:javascript复制
AppEventsTestEvent::dispatch('aaa');
IlluminateSupportFacadesEvent::dispatch(new AppEventsTestEvent('bbb'));
event(new AppEventsTestEvent('ccc'));

在这里我们使用了三种触发事件的形式。分别是使用事件类本身的 dispatch() 方法,使用 Event 门面的 dispatch() 方法,以及使用 event() 辅助函数。它们的作用都是一样的,用于事件的分发。注意这个 dispatch 关键字,它是分发,而不是触发。这里有什么深意呢?

之前我们就说过,事件系统是用于解耦的,也就说,可以让多个监听器来监听同一个事件(就和 Redis 中的 Pub/Sub 一样),这样如果事件被调用触发的话,那么其实也是分发给多个监听器来处理。就像观察者模式中的 观察者 一样。我们的 Subject 类中可以保存多个 Observer ,当调用 Subject 的 notify() 方法之后,多个观察者可以进行后续的操作。如果你对观察者模式不熟悉,或者已经忘了我们之前讲过的观察者模式的话,可以移步 PHP设计模式之观察者模式https://mp.weixin.qq.com/s/SlSToMIGNBtU06BWNCwWvg 再详细地学习一下哦。

事件订阅者

订阅者,这又是一个什么东东呢?前面我们已经看到了,当调用事件分发的时候,我们的监听器会对事件进行响应,然后就可以进行后续的处理。一般情况下一个事件对应一个监听器,当然,我们也可以使用多个监听器去监听同一个事件。那么反过来,能不能一个监听器监听所有的事件呢?当然没问题,这就是事件订阅者的作用。

事件订阅者是可以从订阅者类本身中订阅多个事件的类,允许你在单个类中定义多个事件处理程序。我们需要自己手动建立事件订阅者类,这个类中需要有一个 subscribe() 方法。

代码语言:javascript复制
namespace AppListeners;


use AppEventsTestEvent;
use AppEventsTestEvent2;
use AppEventsTestEvent3;

class TestSubscriber
{
    public function handleTestEvent1($event){
        echo 'This is TestEvent1', $event->a, "<br/>";
    }

    public function handleTestEvent2($event){
        echo 'This is TestEvent2', "<br/>";
    }

    public function handleTestEvent3($event){
        echo 'This is TestEvent3', "<br/>";
    }

    public function handleTestEventAll($event){
        echo "This is AllTestEvent";
        if(isset($event->a)){
            echo $event->a;
        }
        echo "<br/>";
    }


    public function subscribe($events)
    {
        $events->listen(
            [TestEvent::class, TestEvent2::class, TestEvent3::class,],
            [TestSubscriber::class, 'handleTestEventAll']
        );

        $events->listen(
            TestEvent::class,
            [TestSubscriber::class, 'handleTestEvent1']
        );

        $events->listen(
            TestEvent2::class,
            [TestSubscriber::class, 'handleTestEvent2']
        );

        $events->listen(
            TestEvent3::class,
            [TestSubscriber::class, 'handleTestEvent3']
        );
    }

}

通过命令行,我们再创建了两个测试事件类,分别是 TestEvent2 和 TestEvent3 。然后在新创建的这个 TestSubscriber 类的 subscribe() 方法中去设置对它们的监听。通过 $events 的 listen() 方法,我们可以指定这些事件的处理对象和方法。注意,我们可以指定多个事件同时去走一个事件处理,也可以单个的指定。这个事件订阅者我们也放在了 app/Listener 目录下,因为事件订阅者本身其实也是一个大监听器。

然后,就需要去 app/Providers/EventServiceProvider.php 中添加这个事件订阅者的注册,它使用的是 $subscribe 变量。

代码语言:javascript复制
protected $subscribe = [
    TestSubscriber::class,
];

剩下的就是直接在路由中进行测试了。

代码语言:javascript复制
Route::get('event/test2', function(){
    AppEventsTestEvent::dispatch('aaa');
    AppEventsTestEvent2::dispatch();
    AppEventsTestEvent3::dispatch();

//    aaaThis is AllTestEventaaa
//    This is TestEvent1aaa
//    This is AllTestEvent
//    This is TestEvent2
//    This is AllTestEvent
//    This is TestEvent3
});

测试结果如你所料吗?第一个 aaa 是我们上面普通监听器输出的。然后 handleTestEventAll() 方法输出 This is AllTestEventaaa ,再接着就是事件订阅者中的 handleTestEvent1() 方法输出的 This is TestEvent1aaa 。后面的内容就是 TestEvent2 和 TestEvent3 的输出了。

事件运行过程

对于事件的运行过程,我们还是从分发方法走起。通过查看源码,你会发现,不管是用事件类本身的 dispatch() 还是使用 Event 门面的 dispatch() ,最后执行的都是 event() 这个辅助方法,而这个方法其实就是实例化了一个 events 别名的对象。通过查找源码,我们发现这个方法对应的是 vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php 类。最终调用的也是这个类中的 dispatch() 方法。

代码语言:javascript复制
public function dispatch($event, $payload = [], $halt = false)
{
    [$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 ($halt && ! is_null($response)) {
            return $response;
        }

        if ($response === false) {
            break;
        }

        $responses[] = $response;
    }

    return $halt ? null : $responses;
}

从代码的 foreach() 部分可以很容易看出,这是在遍历所有的 监听器 然后直接调用监听器实例获得 response 结果。在调用监听器的时候,是将自己这个事件类作为参数传递给监听器。所以我们在监听器的 handle() 方法中可以获得事件对象。那么我们的监听器是如何加载的呢?当然是在框架启动运行的时候,通过 EventServiceProvider 来提供的。

在 config/app.php 中,providers 数组变量中就配置了 AppProvidersRouteServiceProvider::class ,也就是我们去配置的事件和订阅的服务提供者,它实际上继承的是 vendor/laravel/framework/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php 这个服务提供者。在父类里面,register() 方法内部调用 Event 门面的 listen() 方法,这个方法依然是 vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php 中的方法。

代码语言:javascript复制
public function listen($events, $listener = null)
{
    if ($events instanceof Closure) {
        return $this->listen($this->firstClosureParameterType($events), $events);
    } elseif ($events instanceof QueuedClosure) {
        return $this->listen($this->firstClosureParameterType($events->closure), $events->resolve());
    } elseif ($listener instanceof QueuedClosure) {
        $listener = $listener->resolve();
    }

    foreach ((array) $events as $event) {
        if (Str::contains($event, '*')) {
            $this->setupWildcardListen($event, $listener);
        } else {
            $this->listeners[$event][] = $this->makeListener($listener);
        }
    }
}

在这个方法中,通过最后的 makeListener() 方法,创建监听者并放在 listeners 数组中,之后在进行事件分发的时候遍历的监听器数组就是来自这里。在 makeListener() 方法中,最后返回的是一个闭包回调函数。

代码语言:javascript复制
public function makeListener($listener, $wildcard = false)
{
    if (is_string($listener)) {
        return $this->createClassListener($listener, $wildcard);
    }

    if (is_array($listener) && isset($listener[0]) && is_string($listener[0])) {
        return $this->createClassListener($listener, $wildcard);
    }

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

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

public function createClassListener($listener, $wildcard = false)
{
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            return call_user_func($this->createClassCallable($listener), $event, $payload);
        }

        $callable = $this->createClassCallable($listener);

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

这下你就应该清楚了,为什么在 dispatch() 方法的 foreach() 中,我们是这样获得 response 的了。

代码语言:javascript复制
$response = $listener($event, $payload);

继续顺着 makeListener() 向下看,由于我们定义的事件和监听器都是字符串形式的,也就是

代码语言:javascript复制
TestEvent::class => [
    TestListener::class
],

这样的定义,所以它会走 is_string() 判断中的 createClassListener() 方法,这个方法内部返回的也是一个回调函数,实际上的监听器创建到这里就结束了。为什么呢?因为回调方法是我们在正式使用的时候才会进去的。当前的 listeners 中存储的就是它了。然后在事件分发的时候,我们才会再次来到这个 createClassListener() 内部的回调函数中,这时我们再接着看这个回调函数,它的内部又会继续调用 createClassCallable() 方法。

代码语言:javascript复制
protected function createClassCallable($listener)
{
    [$class, $method] = is_array($listener)
                        ? $listener
                        : $this->parseClassCallable($listener);

    if (! method_exists($class, $method)) {
        $method = '__invoke';
    }

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

    $listener = $this->container->make($class);

    return $this->handlerShouldBeDispatchedAfterDatabaseTransactions($listener)
                ? $this->createCallbackForListenerRunningAfterCommits($listener, $method)
                : [$listener, $method];
}

protected function  parseClassCallable($listener)
{
    return Str::parseCallback($listener, 'handle');
}

查看 parseClassCallable() 方法,我们会发现有 handle 字符的出现,通过 Str::parseCallback() 这个方法会返回一个数组。这下想必你也清楚了 class, method 分别代表什么了吧。class 就是我们的监听器类,method 就是那个 handle() 方法。因此,最后我们调用的实际上就是监听器中的 handle() 方法。

调用事件和监听器加载处理的过程就介绍到这里了。事件系统本身非常庞大,里面的源码也比较复杂。从这个对象中的很多方法名字就可以看出来,号称优雅的框架在这个模块中的方法名字都这么长,就可想而知这个组件的复杂程度。剩下的内容,大家可以自己再深入的研究学习一下,最好还是使用 XDebug 调试工具来好好调试一下吧!

总结

除了我们演示的最简单的这种事件操作之外,还可以使用事件监听器队列来进行事件的处理,这样就可以实现完全的调用解耦,比如说下订单之后要发送短信、通知信息等这类比较慢的操作,都可以让队列在后台慢慢处理。这些内容大家可以自己再深入了解,当然,使用还是比较简单的,不过由于我们还没有讲队列相关的内容,这里就不多说了,后面学习完队列相关的内容之后大家可以再自己尝试一下事件中的队列处理。

其实讲到这里,大家也能看出来了,Laravel 中不需要预埋勾子函数,就是因为类似的功能都是通过事件来实现的。总体来说,事件功能还是非常好用的,也非常方便使用。你的应用中是不是也可以考虑马上应用上了呢!

参考文档:

https://learnku.com/docs/laravel/8.5/events/10387

0 人点赞