深度解析Thinkphp6的事件机制 | PHP开发教程

silverwq
2022-09-30 / 0 评论 / 370 阅读 / 正在检测是否收录...

概述

tp6的事件,是通过观察者模式设计的,事件的基本原理是,首先在系统初始化的时候注册一些监听事件,然后观察事件的触发,只要事件触发的时候就就能执行这些观察者。
事件机制再不修改框架代码的情况下,提升了框架的扩展性。

源码解析

别名功能

事件的别名,触发的时候可以直接触发这个别名,系统自定义的基本事件别名已经写死在事件类的属性里了。

class Event
{
    // 事件的别名,触发的时候可以直接触发这个别名
    protected $bind = [
        'AppInit'     => event\AppInit::class,
    ];

    // 批量注册别名
    public function bind(array $events)
    {
        $this->bind = array_merge($this->bind, $events);

        return $this;
    }
}

事件配置,因为事件相关的配置,最好放在一个配置文件里,所以通过key来分开

return [
    'bind'    =>    [
        'UserLogin' => 'app\event\UserLogin',
    ],
];

然后系统初始化加载配置的时候,会批量加载别名,配置文件如下

$this->event->bind($event['bind']);

绑定别名后,就可以通过别名触发事件

$this->event->trigger('AppInit');

当然这个别名不设置也是可以的,只要触发的时候,直接触发事件类名称就好了,不要触发别名,下面触发事件的方式和上面别名触发的方式效果是一样的

$this->event->trigger(AppInit::class);

需要注意的是,这个别名最好是大写开头,因为事件自动订阅模式,截取on开头后,第一个字符是大写,详情见下面的事件订阅

事件的监听

事件的监听就是观察者,是事件的核心

class Event
{
    // 监听类的类名
    protected $listener = [];

    // 批量注册监听
    public function listenEvents(array $events)
    {
        foreach ($events as $event => $listeners) {
            if (isset($this->bind[$event])) {
                $event = $this->bind[$event];
            }

            $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners);
        }

        return $this;
    }

    // 单个监听
    public function listen(string $event, $listener)
    {
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }
        $this->listener[$event][] = $listener;

        return $this;
    }
}

由上我门可以知道,监听基本的参数是事件的名称、监听类(到时候会执行监听类里的handle方法)或者闭包,例如批量监听的时候,是通过系统配置的方式

return [
    'listen'  =>    [
        'UserLogin'    =>    ['app\listener\UserLogin'],
    ],
];

这个配置文件,会在系统初始化的是被批量注册

$this->event->listenEvents($event['listen']);

当然,我们还可以注册单个

// 类名称,触发的时候会执行UserLogin里的handle方法
Event::listen('UserLogin', UserLogin::class);
// 闭包
Event::listen('UserLogin', function($user) {
    // 
});
// 对象的具体方法
$this->listen('UserLogin', [$userLoginObject, 'onUserLogin']);

订阅功能

订阅的概念跟关注差不多,类似一次性可以关注多个明星,因此我们也可以一次性订阅多个事件,例如用户相关的事件,可以放在用户订阅类里,这样好管理事件的监听

// 事件类
class Event
{
    // 批量订阅,可以传入字符串或者对象
    public function subscribe($subscriber)
    {
        // 如果不是数组的,转成数组,支持订阅单个
        $subscribers = (array) $subscriber;
        foreach ($subscribers as $subscriber) {
            if (is_string($subscriber)) {
                $subscriber = $this->app->make($subscriber);
            }

            // 如果订阅类里有subscribe方法,代表需要手动订阅
            if (method_exists($subscriber, 'subscribe')) {
                // 手动订阅,在该方法里,手工动态监听事件
                $subscriber->subscribe($this);
            } else {
                // 智能订阅,根据规则,自动注册事件监听
                $this->observe($subscriber);
            }
        }

        return $this;
    }

    // 根据方法on+事件别名名称的方法,自动注册对应事件监听
    public function observe($observer, string $prefix = '')
    {
        if (is_string($observer)) {
            $observer = $this->app->make($observer);
        }

        $reflect = new ReflectionClass($observer);
        $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC);

        if (empty($prefix) && $reflect->hasProperty('eventPrefix')) {
            $reflectProperty = $reflect->getProperty('eventPrefix');
            $reflectProperty->setAccessible(true);
            $prefix = $reflectProperty->getValue($observer);
        }

        foreach ($methods as $method) {
            $name = $method->getName();
            if (0 === strpos($name, 'on')) {
                $this->listen($prefix . substr($name, 2), [$observer, $name]);
            }
        }

        return $this;
    }
}

同样,我们可以同配置文件,批量订阅

return [
    'subscribe'    =>    [
       'app\subscribe\User',
    ],
];

系统初始化的时候,会加载配置文件

if (isset($event['subscribe'])) {
    $this->event->subscribe($event['subscribe']);
}

当然,我们也可以动态订阅

Event::subscribe('app\subscribe\User');

订阅类有subscribe方法,手动监听到指定的方法

class User
{
    public function onUserLogin($user)
    {
        // UserLogin事件响应处理
    }

    public function subscribe(Event $event)
    {
        $event->listen('UserLogin', [$this,'onUserLogin']);
    }
}

订阅类没有subscribe方法,自动监听

class User
{
    // UserLogin必须在事件别名
    public function onUserLogin($user)
    {
        // UserLogin事件响应处理
    }
}

事件的触发

我们一般在可能需要扩展的地方触发响应的事件,触发逻辑如下所示

class Event
{
    public function trigger($event, $params = null)
    {
        // 由此可知,触发可以传入事件类的对象
        if (is_object($event)) {
            // 对象的话,把对象作为参数传入观察者
            $params = $event;
            $event  = get_class($event);
        }
        // 由此可知,触发可以传入事件类的别名
        if (isset($this->bind[$event])) {
            $event = $this->bind[$event];
        }

        $result    = [];
        $listeners = $this->listener[$event] ?? [];
        $listeners = array_unique($listeners, SORT_REGULAR);
        // 执行事件的所有监听
        foreach ($listeners as $key => $listener) {
            // 触发可以传入参数
            $result[$key] = $this->dispatch($listener, $params);
        }
        // 可以返回每个监听者的返回值
        return $result;
    }

    // 执行监听者
    protected function dispatch($event, $params = null)
    {
        if (!is_string($event)) {
            // 闭包类型的监听者
            $call = $event;
        } elseif (strpos($event, '::')) {
            // 静态方法类型的监听者
            $call = $event;
        } else {
            // 类名称类型的监听者
            $obj  = $this->app->make($event);
            $call = [$obj, 'handle'];
        }

        return $this->app->invoke($call, [$params]);
    }
}

可以通过别名触发

$this->event->trigger("AppInit");

可以通过,监听类名称触发

$this->event->trigger(AppInit::class);

可以通过对象触发

$this->event->trigger(new UserLogin([
    'login_time'=>time()// 事件对象可以设置一些参数,如果事件对象有这个属性的话
]));

可以通过静态方法触发,并且传入参数

$this->event->trigger("events\UserLogin::OnLogin",[$userId]);
0

评论 (0)

取消