Featured image of post hyperf constants实现机制

hyperf constants实现机制

阅读说明

环境说明

  • php v8.2.8
  • hyperf/constants v3.1.0

前言

Hyperf 枚举类文档中,主要分为了两个部分:

  • 枚举,主要是通过错误码获取到错误内容的作用。
  • 异常,则是对抛异常的使用带来了便利,仅传入错误码,就能从枚举类中获得错误内容。

本文主要分析枚举类获取错误内容的源码实现过程。

代码示例

hyperf/constants的使用如下。

枚举类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

declare(strict_types=1);

namespace App\Constants;

use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;

#[Constants]
class ErrorCode extends AbstractConstants
{
    /**
     * @Message("Server Error!")
     */
    const SERVER_ERROR = 500;

    /**
     * @Message("系统参数错误")
     */
    const SYSTEM_INVALID = 700;
}

错误内容通过注释的方式定义,与错误码紧挨着,这样的实现有两个好处:

  • 阅读方便,文件简洁。
  • 书写便捷,省工作量。

为什么?看看传统的实现方式你就知道了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
class ErrorCode
{
    const SERVER_ERROR = 500;
    const PARAMS_INVALID = 1000;

    public static $messages = [
        self::SERVER_ERROR => 'Server Error',
        self::PARAMS_INVALID => '参数非法'
    ];
}
$message = ErrorCode::messages[ErrorCode::SERVER_ERROR] ?? '未知错误';

当错误码定义足够多的情况下,就会显得臃肿和繁琐。

(如果你选择把错误码拆到多个文件,每个文件内容不超过一个屏幕画面的话,当我没说😂)

异常类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

declare(strict_types=1);

namespace App\Exception;

use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;

class BusinessException extends ServerException
{
    public function __construct(int $code = 0, string $message = null, Throwable $previous = null)
    {
        if (is_null($message)) {
            $message = ErrorCode::getMessage($code);
        }

        parent::__construct($message, $code, $previous);
    }
}

抛异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Constants\ErrorCode;
use App\Exception\BusinessException;

class IndexController extends AbstractController
{
    public function index()
    {
        throw new BusinessException(ErrorCode::SERVER_ERROR);
    }
}

分析

根据使用方式,先来猜测一下。

如果要获取类文件中的注释,那必定会用到反射类:ReflectionClass或者是ReflectionClassConstant,然后再通过正则取出想要的内容。

疑问

  • KQ1:怎么获取到注释里的错误内容的?
  • KQ2:在什么时候进行获取的?
  • Q3:难道每次获取错误内容都要反射一次?那会对性能有影响啊?
  • Q4:都PHP8了为什么不用注解的写法?例如:#[Message("Server Error!")]

废话不多说,马上开始逐个分析问题。

获取错误内容

先从获取错误内容的代码入手:

1
ErrorCode::getMessage($code);

再看下枚举类,是继承了Hyperf\Constants\AbstractConstants的抽象类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

declare(strict_types=1);

namespace Hyperf\Constants;

/**
 * @method static string getMessage(int|string $code, array $translate = null)
 */
abstract class AbstractConstants
{
    use ConstantsTrait;
}

用了一个Hyperf\Constants\ConstantsTrait的trait,继续点进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

declare(strict_types=1);

namespace Hyperf\Constants;

use Hyperf\Constants\Exception\ConstantsException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

/**
 * @method static string getMessage(int|string $code, array $translate = null)
 */
trait ConstantsTrait
{
    use GetterTrait;

    /**
     * @throws ConstantsException
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public static function __callStatic(string $name, array $arguments): string|array
    {
        return static::getValue($name, $arguments);
    }
}

是一个__callStatic魔术方法,再点进Hyperf\Constants\GetterTrait这个 trait。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
namespace Hyperf\Constants;

use Hyperf\Constants\Exception\ConstantsException;
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\TranslatorInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

use function array_shift;
use function is_array;
use function sprintf;
use function strtolower;
use function substr;

trait GetterTrait
{
    /**
     * @throws ConstantsException
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    public static function getValue(string $name, array $arguments): string|array
    {
        if (! str_starts_with($name, 'get')) {
            throw new ConstantsException("The function {$name} is not defined!");
        }

        if (empty($arguments)) {
            throw new ConstantsException('The Code is required');
        }

        $code = array_shift($arguments);
        $name = strtolower(substr($name, 3));

        $message = ConstantsCollector::getValue(static::class, $code, $name);

        $result = self::translate($message, $arguments);
        // If the result of translate doesn't exist, the result is equal with message, so we will skip it.
        if ($result && $result !== $message) {
            return $result;
        }

        if (! empty($arguments)) {
            return sprintf($message, ...(array) $arguments[0]);
        }

        return $message;
    }

    /**
     * @throws ContainerExceptionInterface
     * @throws NotFoundExceptionInterface
     */
    protected static function translate(string $key, array $arguments): array|string|null
    {
        if (! ApplicationContext::hasContainer() || ! ApplicationContext::getContainer()->has(TranslatorInterface::class)) {
            return null;
        }

        $replace = array_shift($arguments) ?? [];
        if (! is_array($replace)) {
            return null;
        }

        $translator = ApplicationContext::getContainer()->get(TranslatorInterface::class);

        return $translator->trans($key, $replace);
    }
}

到这里可以得知:ErrorCode::getMessage($code); 的调用,最终是Hyperf\Constants\GetterTrait::getValue() 在处理。

第一感觉有点绕啊,跳来跳去最后才见到了庐山真面目。

为什么要这样设计呢?那么多层trait,不如直接写一个getMessage方法方便?挖坑#1002。

但是这样实现有一个好处,比如可以通过ErrorCode::getTest($code);获取到自定义注释的错误内容,这是文档中没有介绍到的,挖到一点宝藏用法,但是毕竟属于没公布的内容,在业务中使用还得谨慎一些。

1
2
3
4
5
/**
 * @Message("Server Error!")
 * @Test("服务错误")
 */
const SERVER_ERROR = 500;

回到主线任务,从实现中可以看出,错误内容的获取关键代码是:

1
$message = ConstantsCollector::getValue(static::class, $code, $name);

它是一个自定义的收集器,在Hyperf\Constants\ConfigProvider中可以看到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php

declare(strict_types=1);
namespace Hyperf\Constants;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'annotations' => [
                'scan' => [
                    'collectors' => [
                        ConstantsCollector::class,
                    ],
                ],
            ],
        ];
    }
}

进去Hyperf\Constants\ConstantsCollector

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

declare(strict_types=1);

namespace Hyperf\Constants;

use Hyperf\Di\MetadataCollector;

class ConstantsCollector extends MetadataCollector
{
    protected static array $container = [];

    public static function getValue($className, $code, $key): string
    {
        return static::$container[$className][$code][$key] ?? '';
    }
}

错误内容是从一个数组类型的静态变量(这里称为容器)中获取的,并且容器中存储不止一个枚举类的数据。

swoole静态变量的生命周期是多久?和传统PHP有什么区别?挖坑#1000。

把容器的数据结构打印出来看看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var_dump(ConstantsCollector::list());
[
  'App\Constants\ErrorCode' => [
    500 => [
      'message' => 'Server Error!',
      'test' => '服务错误',
    ],
    700 => [
      'message' => '系统参数错误',
    ],
  ],
];

也就是ErrorCode::getMessage($code);获取错误内容实际是从Hyperf\Constants\ConstantsCollector::$container静态变量里取出来的。

KQ2和Q3的问题回答了一半,那容器里的数据是怎么来的?也就是调用收集器的地方。

hyperf注解和收集器的工作原理是怎样的?挖坑#1001。

获取时机

在对hyperf的配置加载、注解和收集器的机制了解完以后(挖坑#1003),我们知道,hyperf会在启动的时候对annotations.scan.paths配置的目录进行扫描,配置结构如下。

1
2
3
4
5
6
7
'scan' => [
  'paths' => [
    BASE_PATH . '/app',
    BASE_PATH . '/src',
  ],
  ...
]

然后会把所有后缀是.php的类文件,进行ReflectionClass反射处理。

并通过getAttributes()逐个检查类文件中的classmethodproperties是否存在注解,存在就会通过newInstance()实例化这个注解类,然后执行注解类中的collectClass() / collectProperty() / collectMethod()方法,最后会将所有collector给序列化serialize然后存放到./runtime/container/scan.cache中以便下次启动时加速。

回到主线任务,也就是说,在启动的时候会扫描到枚举类文件,发现使用到了#[Constants]注解,对应Hyperf\Constants\Annotation\Constants会被实例化,看下这个文件的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

declare(strict_types=1);
namespace Hyperf\Constants\Annotation;

use Attribute;
use Hyperf\Constants\AnnotationReader;
use Hyperf\Constants\ConstantsCollector;
use Hyperf\Di\Annotation\AbstractAnnotation;
use ReflectionClass;

#[Attribute(Attribute::TARGET_CLASS)]
class Constants extends AbstractAnnotation
{
    public function collectClass(string $className): void
    {
        $reader = new AnnotationReader();

        $ref = new ReflectionClass($className);
        $classConstants = $ref->getReflectionConstants();
        $data = $reader->getAnnotations($classConstants);

        ConstantsCollector::set($className, $data);
    }
}

collectClass()就会被执行,来分析一下这段代码。

这里的$className正是App\Constants\ErrorCode枚举类,可以看到用了ReflectionClass反射该文件来获取所有的常量。

这里为什么不用Hyperf\Di\ReflectionManager::reflectClass复用的方式来获取反射呢?毕竟composer都已经依赖hyperf/di包了🤔。

哈哈,找到了一个优化点,想提交PR的心正在蠢蠢欲动🤣。

进去Hyperf\Constants\AnnotationReader::getAnnotations()方法得到了证实,代码太多我就不放出来,它是通过getDocComment()和正则拿到了注释中的错误内容。

$data结构参考如下:

1
2
3
4
5
6
7
500 => [
  'message' => 'Server Error!',
  'test' => '服务错误',
],
700 => [
  'message' => '系统参数错误',
],

最后将数据存到了Hyperf\Constants\ConstantsCollector::container静态变量中,梳理完毕。

整个过程简单点说就是,在hyperf启动时,枚举类文件会被扫描,由注解类将收集到的数据存放到收集器中,扫描逻辑将所有收集器的数据缓存到本地文件中,下次再启动时就不需要重复扫描,直接使用本地缓存文件。

至此,KQ1、KQ2、Q3都得到了回答。

等等,是不是还有Q4没有回答?

嘿嘿,偷个懒,留个课后作业给你思考一下。

总结

  • 枚举类中的错误内容是通过ReflectionClass反射,对每个常量getReflectionConstants()的注释内容getDocComment()用正则的方式取出来。
  • ErrorCode::getMessage($code)实际是从Hyperf\Constants\ConstantsCollector::container取出错误内容,数据来源则是在hyperf启动的时候,由Hyperf\Constants\Annotation\Constants注解将文件中所有常量的错误内容收集起来后,以文件类名为key,存放到container静态变量中,并且最后会将收集到的数据缓存到./runtime/container/scan.cache中,这样下次就不需要再重复这个收集过程。
Built with Hugo
主题 StackJimmy 设计