发现 Reflection 扩展的变化

Reflection 扩展 用于对对象、类、方法和函数等进行反省。ReflectionClassReflectionObject 分别生成类或对象实例的信息。ReflectionFunction 提供程序级函数的信息。此外,Reflection 扩展还有一组由刚才提到的主类产生的辅助类。这些辅助类包括由 ReflectionClass::getMethod() 生成的 ReflectionMethod、由 ReflectionClass::getProperty() 生成的 ReflectionProperty 等。

你可能会问 谁会使用这个扩展?答案是:任何需要对外部类集进行分析的应用程序。这可能包括执行自动代码生成、测试或文档生成的软件。执行 hydration(从数组中填充对象)的类也可以从 Reflection 扩展中受益。

本书没有足够的篇幅介绍每一个 Reflection 扩展类和方法。如果您想了解更多信息,请参阅此处的文档参考: https://www.php.net/manual/en/book.reflection.php

现在让我们来看看 Reflection 扩展的使用示例。

反射扩展的使用

现在,我们将展示一个代码示例,演示如何使用 Reflection 扩展来生成 docblockdocblock 是一种 PHP 注释,它使用一种特殊的语法来表示方法的目的、输入参数和返回值)。下面是实现这一点的步骤:

  1. 我们首先定义一个 __construct() 方法,该方法用于创建目标类的 ReflectionClass 实例,如下所示:

    // /repo/src/Services/DocBlockChecker.php
    namespace Services;
    use ReflectionClass;
    class DocBlockChecker {
        public $target = ''; // class to check
        public $reflect = NULL; // ReflectionClass instance
        public function __construct(string $target) {
            $this->target = $target;
            $this->reflect = new ReflectionClass($target);
        }
  2. 然后,我们定义了一个 check() 方法,该方法可抓取所有类方法,并返回一个 ReflectionMethod 实例数组,如下所示:

    public function check() {
        $methods = [];
        $list = $this->reflect->getMethods();
  3. 然后,我们在所有方法中循环,并使用 getDocComment() 检查 docblock 是否已经存在,如下所示:

    foreach ($list as $refMeth) {
        $docBlock = $refMeth->getDocComment();
  4. 如果 docblock 不存在,我们就新建一个 docblock,然后调用 getParameters(),返回一个 ReflectionParameter 实例数组,如下代码所示:

    if (!$docBlock) {
        $docBlock = "/**\n * "
            . $refMeth->getName() . "\n";
        $params = $refMeth->getParameters();
  5. 如果有参数,我们会收集信息以便显示,具体如下:

    if ($params) {
        foreach ($params as $refParm) {
            $type = $refParm->getType()
                ?? 'mixed';
            $type = (string) $type;
            $name = $refParm->getName();
            $default = '';
            if (!$refParm->isVariadic()
                && $refParm->isOptional()) {
                $default=$refParm->getDefaultValue(); }
            if ($default === '') {
                $default = "(empty string)"; }
            $docBlock .= " * @param $type "
                . "\${$name} : $default\n";
        }
    }
  6. 然后,我们设置返回类型,并将 docblock 赋值给 $methods 数组,然后返回,如下所示:

                        if ($refMeth->isConstructor())
                            $return = 'void';
                        else
                            $return = $refMeth->getReturnType()
                                ?? 'mixed';
                        $docBlock .= " * @return $return\n";
                        $docBlock .= " */\n";
                    }
                    $methods[$refMeth->getName()] = $docBlock;
                }
            return $methods;
        }
    }
  7. 现在,新的 docblock 检查类已经完成,我们可以定义一个调用程序,如下面的代码片段所示。调用程序的目标是 /repo/src/Php7/Reflection/Test.php 类(此处未显示)。该类包含多个带参数和返回值的方法:

    // //repo/ch07/php7_reflection_usage.php
    $target = 'Php7\Reflection\Test';
    require_once __DIR__ . '/../src/Server/Autoload/Loader.php';
    use Server\Autoload\Loader;
    use Services\DocBlockChecker;
    |$autoload = new Loader();
    $checker = new DocBlockChecker($target);
    var_dump($checker->check());

调用程序的输出如下所示:

root@php8_tips_php7 [ /repo/ch07 ]#
php php7_reflection_usage.php
/repo/ch07/php7_reflection_usage.php:10:
array(4) {
    '__construct' => string(75)
    "/**
    * __construct
    * @param PDO $pdo : (empty string)
    * @return void
    */"
    'fetchAll' => string(41)
    "/**
    * fetchAll
    * @return Generator
    */"
    'fetchByName' => string(80)
    "/**
    * fetchByName
    * @param string $name : (empty string)
    * @return array
    */"
    'fetchLastId' => string(38)
    "/**
    * fetchLastId
    * @return int
    */"
}

正如您所看到的,该类构成了潜在的自动文档或代码生成应用程序的基础。

现在让我们来看看 Reflection 扩展的改进。

了解反射扩展改进

此外,Reflection 扩展还做了一些改进,您可能有必要了解一下。请记住,尽管使用 Reflection 扩展的开发人员数量有限,但总有一天您可能会发现自己正在处理使用该扩展的代码。如果您在 PHP 8 升级后发现了奇怪的行为,本节所涉及的内容将为您的故障排除过程提供一个良好的开端。

反射类型修改

在 PHP 8 中,ReflectionType 类现在是抽象类。当您使用 ReflectionProperty::getType()ReflectionFunction: :getReturnType() 方法时,您可能会注意到返回的是一个 ReflectionNamedType 实例。这一变化不会影响程序代码的正常运行,除非您依赖返回的 ReflectionType 实例。不过,ReflectionNamedType 扩展了 ReflectionType,因此任何 instanceof 操作都不会受到影响。

值得注意的是,isBuiltIn() 方法已从 ReflectionType 移至 ReflectionNamedType。同样,由于 ReflectionNamedType 扩展了 ReflectionType,因此在您当前的代码中不会出现任何向后兼容的中断。

增强的 ReflectionParameter::*DefaultValue* 方法

在 PHP 早期版本中,与默认值有关的 ReflectionParameter 方法无法反映 PHP 内部函数。在 PHP 8 中,这种情况有所改变。以下 ReflectionParameter 方法现在也能从内部函数返回默认值信息:

  • getDefaultValue()

  • getDefaultValueConstantName()

  • isDefaultValueAvailable()

  • isDefaultValueConstant()

从列表中可以看出,这些方法的名称不言自明。现在我们将展示一个利用这些增强功能的代码示例。下面是实现这一功能的步骤:

  1. 首先,我们定义一个函数,该函数接受一个 ReflectionParameter 实例,并返回一个包含参数名称和默认值的数组,如下所示:

    // /repo/ch07/php8_reflection_parms_defaults.php
    $func = function (ReflectionParameter $parm) {
        $name = $parm->getName();
        $opts = NULL;
        if ($parm->isDefaultValueAvailable())
            $opts = $parm->getDefaultValue();
  2. 接下来,我们定义一个 switch() 语句来清理选项,如下所示:

        switch (TRUE) {
            case (is_array($opts)) :
                $tmp = '';
                foreach ($opts as $key => $val)
                    $tmp .= $key . ':' . $val . ',';
                $opts = substr($tmp, 0, -1);
                break;
            case (is_bool($opts)) :
                $opts = ($opts) ? 'TRUE' : 'FALSE';
                break;
            case ($opts === '') :
                $opts = "''";
                break;
            default :
                $opts = 'No Default';
        }
        return [$name, $opts];
    };
  3. 然后,我们确定要反映哪个函数,并提取其参数。在下面的示例中,我们反映 setcookie()

    $test = 'setcookie';
    $ref = new ReflectionFunction($test);
    $parms = $ref->getParameters();
  4. 然后,我们在 ReflectionParameter 实例数组中循环,并产生输出,如下所示:

    $patt = "%18s : %s\n";
    foreach ($parms as $obj)
        vprintf($patt, $func($obj));

下面是在 PHP 7 中运行的输出结果:

root@php8_tips_php7 [ /repo/ch07 ]#
php php8_reflection_parms_defaults.php
Reflecting on setcookie
        Parameter : Default(s)
        ------------ : ------------
        name : No Default
        value : No Default
        expires : No Default
        path : No Default
        domain : No Default
        secure : No Default
        httponly : No Default

结果总是 "无默认值",因为在 PHP 7 及更早版本中,Reflection 扩展无法读取 PHP 内部函数的默认值。而 PHP 8 的输出则准确得多,我们可以在这里看到:

root@php8_tips_php8 [ /repo/ch07 ]#
php php8_reflection_parms_defaults.php
Reflecting on setcookie
Parameter : Default(s)
------------ : ------------
name : No Default
value : ''
expires_or_options : No Default
path : ''
domain : ''
secure : FALSE
httponly : FALSE

从输出结果可以看出,PHP 8 中的 Reflection 扩展能够准确报告内部函数的默认值!

现在让我们看看 Reflection 扩展的其他变化。

其他反射扩展变更

在 PHP 8 之前的 PHP 版本中,ReflectionMethod::isConstructor()ReflectionMethod::isDestructor() 无法反映接口中定义的魔法方法。在 PHP 8 中,这两个方法现在可以返回接口中定义的相应魔法方法的 TRUE

使用 ReflectionClass::getConstants()ReflectionClass::getReflectionConstants() 方法时,新增了 $filter 参数。通过该参数,您可以按可见性级别过滤结果。因此,新参数可以接受以下任何一个新添加的预定义常量:

  • ReflectionClassConstant::IS_PUBLIC

  • ReflectionClassConstant::IS_PROTECTED

  • ReflectionClassConstant::IS_PRIVATE

现在你已经知道如何使用 Reflection 扩展,以及在 PHP 8 迁移后需要注意些什么。现在我们来看看在 PHP 8 中发生变化的其他扩展。