超级管理员

79361

帖子

13

回复

238

积分

楼主
发表于 2020-09-30 23:20:03 | 查看: 18 | 回复: 0

Laravel Event的分析和使用

第一部分 概念解释 请自行查看观察者模式

第二部分 源码分析 (逻辑较长,不喜欢追代码可以直接看使用部分)

第三部分 使用

第一部分 解释

当一个用户阅读了一篇文章,可能需要给文章增加点击量,给阅读的用户增加积分,给文章作者发送通知等功能。对于以上操作,

我们可以使用laravel提供的事件机制进行良好的解耦。以上的用户阅读一篇文章,就是laravel中的一个事件,用户阅读文章后触

发的一系列操作就是此事件的监听者,他们会被逐个执行。实际上laravel的事件服务是观察者模式的一个实现,

触发了一个事件,就好象推倒了多米诺骨牌的地一块,剩下的操作就骄傲给提前摆好的阵型自行完成了。不同的是现实中我们很难让骨牌

停止倒塌, 但在laravel中我们可以很方便的停止事件的传播,即终止监听者的调用链。

第二部分 追源码

事件服务的注册
# laravel中每个服务,需要先注册再启动,其中注册是一定的,启动过程可以没有。事件服务也不例外。但事件服务的注册位置较为特殊,
# 位于Application.php
protected function registerBaseServiceProviders()
{	
    # 事件服务就是在此注册的
    # 注意application的register方法实际上调用了服务提供者的register方法
    $this->register(new EventServiceProvider($this));
    $this->register(new LogServiceProvider($this));
    $this->register(new RoutingServiceProvider($this));
}

# 事件服务提供者 Illuminate\Events\EventServiceProvider
public function register()
{
    # 注意此处的singleton绑定 后面会使用到
    $this->app->singleton('events', function ($app) {
        // 绑定的是一个disaptcher实例 并为事件服务设置了队列解析器
       	// 注意此闭包是在我们尝试从容器中解析事件服务的时候才会执行
        return (new Dispatcher($app))->setQueueResolver(function () use ($app) {
            return $app->make(QueueFactoryContract::class);
        });
    });
}

# 看Illuminate\Events\Dispatcher类
# 简单的构造方法
public function __construct(ContainerContract $container = null)
{
    $this->container = $container ?: new Container;
}

# setQueueResolver方法 一个简单的set
public function setQueueResolver(callable $resolver)
{
    $this->queueResolver = $resolver;

    return $this;
}

# 可以看到事件服务的注册实际上是向容器中注册了一个事件的分发器
事件服务的启动一(获取所有的事件和监听者)
# 框架启动的过程中会调用app/Providers下所有服务提供者的boot方法,事件服务也不例外。
App\Providers\EventServiceProvider文件
class EventServiceProvider extends ServiceProvider
{
	# 此数组键为事件名,值为事件的监听者
    # 事件服务的启动阶段会读取此配置,将所有的事件和事件监听者对应起来并挂载到事件分发器Dispatcher上
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
    ];
	
    # 事件服务启动真正调用的方法 可以看到调用了父类的boot方法
    # 也可以在boot方法中向事件分发器中自行绑定事件和监听者
    public function boot()
    {
        parent::boot();

        //
    }
}

# EventServiceProvider的父类
# 注册事件监听器
public function boot()
{	
    // getEvents方法 获取事件和监听器
    $events = $this->getEvents();
	
    foreach ($events as $event => $listeners) {
        foreach (array_unique($listeners) as $listener) {
            // 此处Event facade对应的是Dispatcher的listen方法
            // facade的原理和使用之前介绍过
            Event::listen($event, $listener);
        }
    }

    foreach ($this->subscribe as $subscriber) {
        // 调用的是Dispatcher的subscribe方法
        Event::subscribe($subscriber);
    }
}

# getEvents方法
public function getEvents()
{
    if ($this->app->eventsAreCached()) {
        $cache = require $this->app->getCachedEventsPath();

        return $cache[get_class($this)] ?? [];
    } else {
        return array_merge_recursive(
            // 如果事件非常多,也可以设置事件和监听者的目录,让框架自行帮助查找
            // 如果需要开启discoveredEvents功能,需要在App\Providers\EventServiceProvider中
           	// 重写shouldDiscoverEvents方法 并返回true 代表开启事件自动发现
           	// 如果需要指定事件和监听者的目录,需要重写discoverEventsWithin方法,其中返回目录数组
           	// 当然你也可以全部写在listen属性中
            // 当重写了以上两个方法的时候 返回的数组和$listen属性的格式是完全一致的 以事件名称为key 监听者为value
            $this->discoveredEvents(),
            
            // 返回的就是App\Providers\EventServiceProvider下的listen数组
            $this->listens()
        );
    }
}

# discoveredEvents方法 此方法触发的前提是重写了shouldDiscoverEvents方法
public function discoverEvents()
{	
    // 使用了laravel提供的collect辅助函数 文档有详细章节介绍
    // collect函数返回collection集合实例方便我们链式操作
    // reject方法的作用是 回调函数返回 true 就会把对应的集合项从集合中移除
    // reduce方法的作用是 将每次迭代的结果传递给下一次迭代直到集合减少为单个值
    return collect($this->discoverEventsWithin())
        // discoverEventsWithin方法返回查找事件监听者的目录数组
        // 默认返回 (array) $this->app->path('Listeners')
        // 我们自然可以重写discoverEventsWithin方法,返回我们指定的监听者目录
        ->reject(function ($directory) {
            // 移除集合中不是目录的元素
            return ! is_dir($directory);
        })
        ->reduce(function ($discovered, $directory) {
            return array_merge_recursive(
                $discovered,
                // 使用Symfony的Finder组件查找Listener文件
                DiscoverEvents::within($directory, base_path())
            );
        }, []);
}

# Illuminate\Foundation\Events\DiscoverEvents::within方法
# 提取给定目录中的全部监听者
public static function within($listenerPath, $basePath)
{
    return collect(static::getListenerEvents(
        (new Finder)->files()->in($listenerPath), $basePath
    ))->mapToDictionary(function ($event, $listener) {
        return [$event => $listener];
    })->all();
}

protected static function getListenerEvents($listeners, $basePath)
{
    $listenerEvents = [];
	// $listeners是Finder组件返回指定目录下的迭代器,遍历可以拿到目录下的所有文件
    foreach ($listeners as $listener) {
        try {
            $listener = new ReflectionClass(
                // 将绝对路径转换为类名
                static::classFromFile($listener, $basePath)
            );
        } catch (ReflectionException $e) {
            continue;
        }

        if (! $listener->isInstantiable()) {
            continue;
        }

        // dump($listener->getMethods(ReflectionMethod::IS_PUBLIC));

        foreach ($listener->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            // 代表着一个监听者类中 可以设置多个监听器
            if (! Str::is('handle*', $method->name) ||
                ! isset($method->getParameters()[0])) {
                continue;
            }
			
            $listenerEvents[$listener->name.'@'.$method->name] =
                // 可以认为此处返回的是事件名
                // 写在handle*方法中的参数 我建议一定要加上类型提示,并且将类型名参数作为第一个参数传入
                Reflector::getParameterClassName($method->getParameters()[0]);
        }
    }
	
    // 过滤事件参数名为空的监听器并返回
    return array_filter($listenerEvents);
}
事件服务的启动二(注册事件监听者)
# 上面获取了全部的事件监听者 下面就要注册这些事件监听者了
# 继续看EventServiceProvider::boot方法
# 使用php artisan event:list 可以查看框架中已经注册的事件
# php artisan event:cache php artisan event:clear
public function boot()
{	
    # 拿到了$listen属性和要求自动发现的所有事件(如果开启了自动发现的话)
    $events = $this->getEvents();
 	// dump($events);
    
    foreach ($events as $event => $listeners) {
        foreach (array_unique($listeners) as $listener) {
            // 调用dispatcher的listen方法
            // 事件名为key 事件监听者为value 进行事件的注册监听
            Event::listen($event, $listener);
        }
    }

    foreach ($this->subscribe as $subscriber) {
        // subscribe方法请自行查看
        Event::subscribe($subscriber);
    }
}

# Dispatcher::listen方法
# 遍历getEvents中所有的事件和监听者 通过实际调用Dispatcher的makeListener创建监听者
# 以event名为键 创建的监听者闭包为值 保存在数组属性中 供事件触发的时候查找调用
// 向调度器注册事件监听器
public function listen($events, $listener)
{
    // dump($events, $listener);
    foreach ((array) $events as $event) {
        // 如果事件名称中包含*
        if (Str::contains($event, '*')) {
            $this->setupWildcardListen($event, $listener);
        } else {
            // 正常事件名
            $this->listeners[$event][] = $this->makeListener($listener);
        }
    }
}

# 绑定事件和监听者闭包的映射
protected function setupWildcardListen($event, $listener)
{   
    // 当一系列的事件都想触发指定的监听者的时候 就可以使用*进行匹配
    $this->wildcards[$event][] = $this->makeListener($listener, true);
    // 每次更新了通配事件后 都清除缓存
    $this->wildcardsCache = [];
}

# 官方注释表名此方法向事件分发器中注册一个事件监听者
# 其实就是返回事件触发时执行的监听者闭包
# 传入的listener可以使App\Listener\MyListener 或 App\Listener\MyListener@myHandle这种字符串
# 或者是一个接收两个参数的闭包
public function makeListener($listener, $wildcard = false)
{   
    if (is_string($listener)) {
        // 如果传递的是一个字符串的话 调用createClassListener放回闭包
        return $this->createClassListener($listener, $wildcard);
    }

    // 如果listener是个闭包 那么直接将事件对象作为参数传入监听者
    // 事件触发的时候 直接执行此闭包
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            return $listener($event, $payload);
        }

        // 可变数量的参数列表
        return $listener(...array_values($payload));
    };
}

# createClassListener方法
public function createClassListener($listener, $wildcard = false)
{   
    // 当传递的是一个class名或者是带@method的字符串的时候
    return function ($event, $payload) use ($listener, $wildcard) {
        if ($wildcard) {
            // createClassCallable返回一个数组 第一个参数是$listener的实例 第二个参数是method
            return call_user_func($this->createClassCallable($listener), $event, $payload);
        }

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

# createClassCallable方法
protected function createClassCallable($listener)
{
    // 从字符串中获取类名和方法名
    [$class, $method] = $this->parseClassCallable($listener);

    // 判断是否需要队列化监听器
    if ($this->handlerShouldBeQueued($class)) {
        // class类名 method 方法名
        return $this->createQueuedHandlerCallable($class, $method);
    }
	
    // 如果不需要异步化执行监听者 直接返回[$listener, 'method']数组
    // class通过container获得 意味着我们可以利用容器方便的注入listner需要的依赖
    // 注意此处返回的是listener的实例 和 调用监听者时执行的方法名
    return [$this->container->make($class), $method];
}

# handlerShouldBeQueued方法 判断如果一个监听者实现了ShouldQueue接口 就认为此监听者需要队列化执行
protected function handlerShouldBeQueued($class)
{   
    // 检查监听者是否实现了ShouldQueue接口
    // 是否使用队列处理事件
    try {
        return (new ReflectionClass($class))->implementsInterface(
            ShouldQueue::class
        );
    } catch (Exception $e) {
        return false;
    }
}

# createQueuedHandlerCallable方法
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());
		// handlerWantsToBeQueued方法 动态判断监听者是否需要投递到队列执行
        if ($this->handlerWantsToBeQueued($class, $arguments)) {
            $this->queueHandler($class, $method, $arguments);
        }
    };
}

# handlerWantsToBeQueued
protected function handlerWantsToBeQueued($class, $arguments)
{
    $instance = $this->container->make($class);

    // 动态判断是否需要异步化事件处理
    // 需要我们在监听器shouldQueue方法中return bool值
    if (method_exists($instance, 'shouldQueue')) {
        // 可以在监听者的shouldQueue方法中返回bool值 动态判断是否需要异步化
        return $instance->shouldQueue($arguments[0]);
    }

    return true;
}

# queueHandler方法
// 判断listener的各种属性 将监听者投递到队列
// laravel 队列以后会单独讲解 此篇先到这里
protected function queueHandler($class, $method, $arguments)
{
    [$listener, $job] = $this->createListenerAndJob($class, $method, $arguments);
	// resolveQueue获取注册事件服务时设置的queueResolver
    $connection = $this->resolveQueue()->connection(
        $listener->connection ?? null
    );

    $queue = $listener->queue ?? null;

    isset($listener->delay)
        ? $connection->laterOn($queue, $listener->delay, $job)
        : $connection->pushOn($queue, $job);
}

# 以上便是事件注册的基本代码 总体来说 我们看到调用Dispatcher的listen方法 可以注册监听者和事件的绑定
# 监听者都已闭包的形式进行包裹 这样的好处是可以保存上下文变量
# 涉及到的异步处理 其他文章会进行讲解
# 值得注意的是 注册好的闭包 并不会执行 当触发相应的事件时才会执行
事件的触发
# 业务代码中调用event()方法就可以触发一个事件了 执行的就是Dispatch::dispatch方法
public function dispatch($event, $payload = [], $halt = false)
{
    // 传递事件对象本身作为disaptch的参数 会将对象类名作为事件名 并将事件对象作为payload传递到监听者
    // 参考使用方式 event(new SomeEvent()) Event::disaptch(new SomeEvent())
    [$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;
        }

        // 如果某个监听者返回了false 那么终止后续监听者的执行
        if ($response === false) {
            break;
        }

        $responses[] = $response;
    }
    // 返回结果给事件触发
    return $halt ? null : $responses;
}

# parseEventAndPayload方法
protected function parseEventAndPayload($event, $payload)
{   
    // 如果传递的是一个事件对象
    if (is_object($event)) {
        [$payload, $event] = [[$event], get_class($event)];
    }

    // 如果event是一个字符串 那么直接包装payload
    return [$event, Arr::wrap($payload)];
}

// 获取所有事件监听者
public function getListeners($eventName)
{
    $listeners = $this->listeners[$eventName] ?? [];

    $listeners = array_merge(
        $listeners,
        $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
    );
	
    // 如果插入的event类存在
    return class_exists($eventName, false)
        ? $this->addInterfaceListeners($eventName, $listeners)
        : $listeners;
}

# addInterfaceListeners方法
protected function addInterfaceListeners($eventName, array $listeners = [])
{
    foreach (class_implements($eventName) as $interface) {
        // 判断事件或者其父类实现的接口是否绑定了监听器
        if (isset($this->listeners[$interface])) {
            foreach ($this->listeners[$interface] as $names) {
                $listeners = array_merge($listeners, (array) $names);
            }
        }
    }
    // 返回合并后的监听者
    return $listeners;
}

# 部分其他方法
# until方法 
# 触发事件 并返回第一个不为null的监听器结果
public function until($event, $payload = [])
{   
    return $this->dispatch($event, $payload, true);
}

# push方法
# 调用的还是listen方法 只不过指定了payload参数
public function push($event, $payload = [])
{
    $this->listen($event.'_pushed', function () use ($event, $payload) {
        $this->dispatch($event, $payload);
    });
}

# flush方法 调用push注册的监听者
public function flush($event)
{
    $this->dispatch($event.'_pushed');
}

第三部分 使用

使用一 通过触发事件给监听者传参

1 在App\Porviders\EventServiceProvider的listen属性中绑定事件和监听者映射关系
...
use App\Events\TestEvent1;
use App\Listeners\TestListener1;
use App\Listeners\TestListener2;
...
protected $listen = [
	...
    TestEvent1::class => [
        TestListener1::class,
        // 自定义监听者闭包调用的方法myHandle
        TestListener2::class . '@myHandle'
    ]
];
...
    
2 php artisan event:generate 按照listen数组的事件监听者映射生成
    
3 我们不在TestEvent1事件中做过多处理 在本示例中保持原样即可

4 编写TestListener1文件
<?php

namespace App\Listeners;

use App\Components\Log\LogManager;
use App\Events\TestEvent1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Providers\LogManagerServiceProvider;

class TestListener1
{   
    protected $logger;
	// 通过上面的分析 我们知道监听者时通过容器解析出来的 所以可以尽情的注入
    public function __construct(LogManager $logger)
    {
        $this->logger = $logger;
    }	
	// 自定义传参给事件监听者
    public function handle(TestEvent1 $event, string $type)
    {
        // dump($type);
        // dump(debug_backtrace());
        $this->logger->driver($type)->logCertains('emergency', 'something emergency');
    }
}

5 编写TestListener2文件
<?php

namespace App\Listeners;

use App\Events\TestEvent1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class TestListener2
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
	
    // 其实指定了myHandle方法之后 原生的handle方法就可以注释或者什么都不写了
    // 通过上面的分析我们知道 框架会执行监听者中所有以handle开头的方法
    // public function handle(TestEvent1 $event)
    // {   
    //     echo '345';
    // }

    // 此方法也是会执行的
    // public function handleabc(TestEvent1 $event)
    // {   
    //     echo 'abc';
    // }
        
    public function myHandle(TestEvent1 $event)
    {
        dump('do whatever you like');
    }
}

6 编写测试路由 触发事件
Route::get('event1', function () {
    // 情况一 传递的是事件实例 如果你需要在事件实例中注入依赖 当然可以使用容器解析事件对象
    // 当注入的是一个事件对象的时候 会触发事件类名这个事件 并且将事件对象作为payload传递给handle方法
    // Event::dispatch(new \App\Events\TestEvent1());

    // 情况二 传递的是事件名 第二个参数生效
    // 演示如何不依赖事件对象 传递参数
    Event::dispatch(TestEvent1::class, [new TestEvent1(), 'stream']);
});

使用二 设置事件自动发现

1 在App\Porviders\EventServiceProvider中重写shouldDiscoverEvents方法 启用事件发现
// 启用事件自动发现
public function shouldDiscoverEvents()
{
    return true;
}

2 我们更近一步 设置自动监听者所在目录 重写discoverEventsWithin方法 指定自动发现目录
// 指定自动发现目录
// 默认的就是app_path('Listeners')
public function discoverEventsWithin()
{
    return [
        // 这里可以注释掉Listeners目录为了避免和上面的listen属性重复 导致所有的监听者都会执行两遍
        // app_path('Listeners'),
        app_path('MyListeners')
    ];
}

3 编写事件App\Events\TestEvent2文件 我这里的代码没有任何的实际意义
<?php

namespace App\Events;

class TestEvent2
{   
    protected $name = 'xy';

    public function __construct()
    {
        // 随意发挥
    }

    public function getName()
    {
        return $this->name;
    }
}

4 手动创建App\MyListeners\Listener1文件
# 通过上面的源码分析 我们知道laravel会将所有以handle开头的方法参数遍历
# 然后将第一个参数的类名作为要触发的事件名,将事件参数作为payload传入
<?php

namespace App\MyListeners;

use App\Events\TestEvent2;

class MyListener1
{
    public function handle(TestEvent2 $evt)
    {
        dump($evt->getName(), 'abc');
        return false; // 如果不注释掉此行代码,事件的调用链到此终结
    }

    public function handleAbc(TestEvent2 $evt)
    {
        dump($evt->getName());
    }
}

5 手动创建App\MyListeners\Listener2文件
<?php

namespace App\MyListeners;

use App\Events\TestEvent2;

class MyListener2
{ 
    public function handle(TestEvent2 $evt)
    {
        dump($evt->getName());
    }
}

6 创建事件自动发现路由
// 测试自动发现
Route::get('event2', function(){
    Event::dispatch(new TestEvent2());
});

使用三 implement的使用 当事件实现了其他事件接口,会自动触发其他事件绑定的监听者

对应的方法为Dispatcher::addInterfaceListeners 请看第二部分

1 创建事件接口
<?php

namespace App\Events;

interface TestEvent3
{ }

<?php

namespace App\Events;

interface TestEvent4
{ }


2 创建监听者
<?php

namespace App\Listeners;

class TestListener3
{
    public function handle()
    {
        dump('listener3 added by event interface3');
    }
}

<?php

namespace App\Listeners;

class TestListener4
{
    public function handle()
    {
        dump('listener3 added by event interface4');
    }
}
    
<?php

namespace App\Listeners;

class TestListener5 implements TestEvent3, TestEvent4
{
    public function handle()
    {
        dump('five own listener');
    }
}

3 事件实现上面的两个接口
<?php

namespace App\Events;

class TestEvent5 implements TestEvent3, TestEvent4
{ }

4 注册事件监听者
protected $listen = [
    ...
    TestEvent3::class => [
        TestListener3::class
    ],
    TestEvent4::class => [
        TestListener4::class
    ],
    # 甚至可以注释掉下面3行 只需要TestEvent5实现上面两个接口即可触发上面注册的监听者
    TestEvent5::class => [
    	TestListener5::class
    ]
]; 

5 最重要的一步 force and brutal 改源码 没错 就是改源码
# Dispatcher::getListeners方法
...    
// return class_exists($eventName, false)
    return class_exists($eventName)
    	? $this->addInterfaceListeners($eventName, $listeners)
    	: $listeners;
...
6 创建测试路由
Route::get('event5', function () {
    Event::dispatch(TestEvent5::class);
});

使用四 until和flush

until方法默认调用dispatch方法 当时间监听者返回不为null则停止执行后面的监听者 并返回结果给事件触发位置

1 配置时间监听者
protected $listen = [
...
    TestEvent6::class => [
        TestListener6::class,
        TestListener7::class,
        TestListener8::class
    ]
];

2 php artisan event:generate
    
3 简单编写事件监听者
# listener6
public function handle(TestEvent6 $event)
{
    dump('return null');
}
# listener7
public function handle(TestEvent6 $event)
{	
    // 注意此监听者是有返回值的
    return 123;
}
# listener8
public function handle(TestEvent6 $event)
{	
 	// 并不会执行7后面的监听者 根本就不会执行
    return 'return something in vain';
}

4 编写测试路由
Route::get('event6', function () {
    $res = Event::until(new TestEvent6());
    // 可以看到监听者8并没有执行 因为7返回的不是null
    dump($res);
});

使用五 push&flush 请查看上面的源码分析

push方法就是提前将event和payload注册好 供flush调用

1 在App\Providers\EventServiceProvider::boot方法中注册(这里只是举例在boot中进行注册,你可以在你喜欢的任何地方注册)
public function boot()
{
    parent::boot();
	// 注册一个保存了payload的事件监听者
    Event::push('longAssEventName', ['type' => 'redis']);
    Event::listen('longAssEventName', function ($type) {
        dump($type);
    });
}

2 创建测试路由
Route::get('event7', function () {
    Event::flush('longAssEventName');
});

以上用法没那么常见,这里只是简单演示下,细节还需各位自行尝试,常见使用还要各位仔细查阅文档。

至于监听者的异步化,只需要监听者实现ShouldQueue接口,然后简单配置就可以了。大家可以先行查看文档事件部分,
具体使用会在laravel队列篇章讲解。如有错误,劳烦指正,感谢。

最后,祝各位十一快乐!!!


本帖子中包含资源

您需要 登录 才可以下载,没有帐号?立即注册

您需要登录后才可以回帖 登录 | 立即注册

2018-2020 ©v2.1 冀ICP备19027484号-2

点击这里给我发消息