【PHP小课堂】一起学习PHP中的反射(一)

时间:2024-10-16 19:09:48

一起学习PHP中的反射(一)

反射这个名词相信大家不会陌生,但反过来说,这个反射到底是一个什么概念呢?其实反射,就是通过一些方法函数,来获得一个类或者一个实例化对象中的一些信息。当然,更重要的是,它可以是在运行时来动态获取这些信息的。这样的话,有很多功能就可以通过反射来实现了。比如说 Java 中的注解,在一些 PHP 的框架中,要实现类似的注解功能,就是通过反射来获得注释中的信息来实现的。大家可以参考 Hyperf 框架中的注解功能。

在学习的过程中,我们还是一步一步的来看看这些反射相关的类及方法是如何使用的。反射也是进阶高级程序员所必备的能力,同时,这个能力也是大部分语言都支持的,不仅限于 PHP ,在 Java 中,反射也是非常常用的特性之一。

获得一个反射类的全部信息

首先,我们来学习的是反射一个类中的信息。注意,类指的是模板,不是一个实例化的对象。所以我们反射出来的内容都是固定写死在类中的,这个我们后面还会讲到,反射类和反射对象是有不同的。

class A{

    /**
     * This is ONE DOC.
     */
    const ONE = 'number 1';
    
    // This is TWO DOC.
    private const TWO = 'number 2';

    /**
     * This is a DOC.
     */
    private $a = '1';
    
    // This is b DOC.
    protected $b = '2';

    /* This is c DOC. */
    public $c = '3';

    public $d;

    static $e = '5';
    static $f;


    private function testA(){
        echo 'This is class A, function testA', PHP_EOL;
    }

    protected function testB(){
        echo 'This is class A, function testB', PHP_EOL;
    }

    public function testC(){
        echo 'This is class A, function testC', PHP_EOL;
    }
}

首先,我们要定义一个用于测试的类。当然,反射相关的操作是可以对系统环境中的全部类和对象都起作用的。不过为了便于测试,我们还是通过一个自定义的类来进行演示。在这个类中,我们有一些属性和方法,有不同的访问修饰符,也有一些不同的注释。今天我们主要研究的是属性方面的操作,所以方法这块就是简单地定义了三个不同访问修饰符的方法。等到后面的文章需要深入地学习方法相关的反射操作时,再增加这个类中关于方法及其它方面的内容。

然后我们先看一下一次性地反射整个类中的信息的方法。

ReflectionClass::export(new ReflectionClass('A'));
// Class [ <internal:Reflection> class ReflectionClass implements Reflector ] {

//     - Constants [3] {
//       Constant [ public int IS_IMPLICIT_ABSTRACT ] { 16 }
//       Constant [ public int IS_EXPLICIT_ABSTRACT ] { 32 }
//       Constant [ public int IS_FINAL ] { 4 }
//     }
  
//     - Static properties [0] {
//     }
  
//     - Static methods [1] {
//       Method [ <internal:Reflection, prototype Reflector> static public method export ] {
  
//         - Parameters [2] {
//           Parameter #0 [ <required> $argument ]
//           Parameter #1 [ <optional> $return ]
//         }
//       }
//     }
  
//     - Properties [1] {
//       Property [ <default> public $name ]
//     }

//  ……………………………………
//  ……………………………………
//  ……………………………………
//  ……………………………………
  
//       Method [ <internal:Reflection> public method getExtension ] {
  
//         - Parameters [0] {
//         }
//       }
  
//       Method [ <internal:Reflection> public method getExtensionName ] {
  
//         - Parameters [0] {
//         }
//       }
  
//       Method [ <internal:Reflection> public method inNamespace ] {
  
//         - Parameters [0] {
//         }
//       }
  
//       Method [ <internal:Reflection> public method getNamespaceName ] {
  
//         - Parameters [0] {
//         }
//       }
  
//       Method [ <internal:Reflection> public method getShortName ] {
  
//         - Parameters [0] {
//         }
//       }
//     }
//   }

ReflectionClass 类就是用于反射一个类的相关信息的类模板,它的这个静态的 export() 方法就是用于报告这个类的全部信息。不过这个方法已经在 PHP7.4 中被标记为过时的,并且在 PHP8 中被移除了。其实对于所有反射相关的类来说,包括我们下面要学习的 ReflectionClassConstant 和 ReflectionProperty 对象,在之前都会有这个 export() 方法,但是现在它们都实现了 __toString() 方法,也就是说,直接打印它们就可以代替原来的 export() 方法了。

$obj = new ReflectionClass('A');
echo $obj;
// Class [ <user> class A ] {
//     @@ /Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/05/source/1.一起学习PHP中的反射(一).php 3-41
  
//     - Constants [2] {
//       Constant [ public string ONE ] { number 1 }
//       Constant [ private string TWO ] { number 2 }
//     }
  
//     - Static properties [2] {
//       Property [ public static $e ]
//       Property [ public static $f ]
//     }
  
//     - Static methods [0] {
//     }
  
//     - Properties [4] {
//       Property [ <default> private $a ]
//       Property [ <default> protected $b ]
//       Property [ <default> public $c ]
//       Property [ <default> public $d ]
//     }
  
//     - Methods [3] {
//       Method [ <user> private method testA ] {
//         @@ /Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/05/source/1.一起学习PHP中的反射(一).php 30 - 32
//       }
  
//       Method [ <user> protected method testB ] {
//         @@ /Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/05/source/1.一起学习PHP中的反射(一).php 34 - 36
//       }
  
//       Method [ <user> public method testC ] {
//         @@ /Users/zhangyue/MyDoc/博客文章/dev-blog/php/2021/05/source/1.一起学习PHP中的反射(一).php 38 - 40
//       }
//     }
//   }

直接打印出来的内容是不是更清晰明了。我们可以看到在类中定义的常量、属性、方法,但是需要注意的是,静态属性在这里是不会打印出来的哦。ReflectionClass 类的构造函数需要传递一个类的名称过来,这样它就可以返回一个针对指定的这个类的反射类对象。注意,现在我们获得的这个 obj 对象就是针对 A 类的一个反射类了。后面学习的操作都是通过这个 obj 对象来实现的。

反射类中的常量信息

既然有了全局的输出,那么我们再来看看单独的一些操作。首先就是看一下获取常量相关信息的操作。

print_r($obj->getConstants());
// Array
// (
//     [ONE] => number 1
//     [TWO] => number 2
// )

echo $obj->getConstant('ONE'), PHP_EOL;// number 1

var_dump($obj->hasConstant('TWO')); // bool(true)
var_dump($obj->hasConstant('THREE')); // bool(false)

可以通过 getConstants() 方法获得全部的常量信息数据,也可以通过 getConstant() 方法获得指定的常量值,也可以通过 hasConstant() 方法判断指定的某个常量是否存在。另外,也可以获得常量对象信息,也就是我们下面要讲的 ReflectionClassConstant 对象。

ReflectionClassConstant

var_dump($obj->getReflectionConstants());
// array(2) {
//     [0]=>
//     object(ReflectionClassConstant)#2 (2) {
//       ["name"]=>
//       string(3) "ONE"
//       ["class"]=>
//       string(1) "A"
//     }
//     [1]=>
//     object(ReflectionClassConstant)#3 (2) {
//       ["name"]=>
//       string(3) "TWO"
//       ["class"]=>
//       string(1) "A"
//     }
//   }

var_dump($obj->getReflectionConstant('ONE'));
// object(ReflectionClassConstant)#3 (2) {
//     ["name"]=>
//     string(3) "ONE"
//     ["class"]=>
//     string(1) "A"
//   }

getReflectionConstants() 用于获得常量反射对象数组,也就是我们的反射对象中全部的常量信息。getReflectionConstant() 获得指定的单个常量反射对象。接下来我们就看看这个 getReflectionConstants 对象中都有什么操作。

$objContant1 = $obj->getReflectionConstant('ONE');
$objContant2 = $obj->getReflectionConstant('TWO');

var_dump($objContant1->getName()); // string(3) "ONE"
var_dump($objContant2->getName()); // string(3) "TWO"

var_dump($objContant1->getValue()); // string(8) "number 1"
var_dump($objContant2->getValue()); // string(8) "number 2"

var_dump($objContant1->getDocComment());
// string(35) "/**
// * This is ONE DOC.
// */"

var_dump($objContant2->getDocComment()); // bool(false)

var_dump($objContant1->getDeclaringClass());
// object(ReflectionClass)#4 (1) {
//     ["name"]=>
//     string(1) "A"
//   }

我们可以通过 getName() 获得这个常量反射对象对应的常量键名,也可以通过 getValue() 获得它的值。

当然,这里更重要的一点是通过 getDocComment() 获得这个常量反射对象的注释信息。从测试代码中可以看出,我们能获取到的是标准的 DOC 格式的注释内容,普通的 // 以及 /* */ 这样的注释内容是无法获取到的。这一点在最前面关于反射的说明中也说过,注解功能正是通过反射这些注释中的内容实现的。DOC 格式的注释中是可以有 @ 修饰符号的,很多内容就可以通过这个 @ 修饰符号来进行,这个我们后面再说。现在的这个方法只是获取到注释里面的文字内容。

接下来就是一个 getDeclaringClass() 方法,返回的就是这个常量反射对象所对应的类模板是哪个,在这里我们可以看到输出的结果是对应的反射类 A 。注意,它返回的不是那个真实的 A 类,而是我们 obj 对应的那个 A 类的反射对象。

var_dump($objContant1->getModifiers()); // int(256)
var_dump($objContant2->getModifiers()); // int(1024)

var_dump($objContant1->isPrivate()); // bool(false)
var_dump($objContant1->isProtected()); // bool(false)
var_dump($objContant1->isPublic()); // bool(true)

最后,我们看到的是关于这个常量反射对象对应的常量的访问修饰符的信息获取操作,通过 getModifiers() 方法,可以获得这个常量对应的访问修饰符,它返回的是一个数字类型,实际上对应的是几个常量,256 对应的是 IS_PUBLIC, 1024 对应的是 IS_PRIVATE ,另外还有一个 512 对应的是 IS_PROTECTED 。当然,我们还有更简单的方法,就是通过下面的 isPrivate() 、isProtectd() 和 isPublic() 方法来直接获得判断结果。

反射类中的属性信息

类中的属性信息其实和常量信息的内容是差不多的,只是它多了针对静态属性相关的操作而已。

var_dump($obj->getDefaultProperties());
// array(6) {
//     ["e"]=>
//     string(1) "5"
//     ["f"]=>
//     NULL
//     ["a"]=>
//     string(1) "1"
//     ["b"]=>
//     string(1) "2"
//     ["c"]=>
//     string(1) "3"
//     ["d"]=>
//     NULL
//   }

getDefaultProperties() 方法用于获取反射类对象中的所有默认属性,其实就是我们定义在类中的属性,它只是通过键值的形式返回属性信息,没有别的内容。真正对应属性的操作,还是集中于属性相关的属性反射对象,也就是 ReflectionProperty 这个对象。

ReflectionProperty

var_dump($obj->getProperties());
// array(6) {
//     [0]=>
//     object(ReflectionProperty)#2 (2) {
//       ["name"]=>
//       string(1) "a"
//       ["class"]=>
//       string(1) "A"
//     }
//     [1]=>
//     object(ReflectionProperty)#3 (2) {
//       ["name"]=>
//       string(1) "b"
//       ["class"]=>
//       string(1) "A"
//     }
//     [2]=>
//     object(ReflectionProperty)#4 (2) {
//       ["name"]=>
//       string(1) "c"
//       ["class"]=>
//       string(1) "A"
//     }
//     [3]=>
//     object(ReflectionProperty)#5 (2) {
//       ["name"]=>
//       string(1) "d"
//       ["class"]=>
//       string(1) "A"
//     }
//     [4]=>
//     object(ReflectionProperty)#6 (2) {
//       ["name"]=>
//       string(1) "e"
//       ["class"]=>
//       string(1) "A"
//     }
//     [5]=>
//     object(ReflectionProperty)#7 (2) {
//       ["name"]=>
//       string(1) "f"
//       ["class"]=>
//       string(1) "A"
//     }
//   }

var_dump($obj->getProperty('a'));
// object(ReflectionProperty)#7 (2) {
//     ["name"]=>
//     string(1) "a"
//     ["class"]=>
//     string(1) "A"
//   }

getProperties() 用于获取反射属性对象列表,getProperty() 用于获取指定的反射属性对象。

$objPro1 = $obj->getProperty('a');
$objPro2 = $obj->getProperty('b');
$objPro3 = new ReflectionProperty('A', 'c');
$objPro4 = $obj->getProperty('d');
$objPro5 = $obj->getProperty('e');

// $objPro111 = $obj->getProperty('aaa');  // PHP Fatal error:  Uncaught ReflectionException: Property aaa does not exist

我们获取类中全部需要测试的属性,在这里,我们使用了另一种通过实例化 ReflectionProperty 类来获取指定类中的反射属性的方式来获得这个对象。这种形式其实上面的 ReflectionClassConstant 也是支持的。另外,如果指定的属性不存在的话,会报直接中断脚本运行的错误信息。

var_dump($objPro1->getName()); // string(1) "a"
// var_dump($objPro1->getValue(new A)); // PHP Fatal error:  Uncaught ReflectionException: Cannot access non-public member A::$a 

var_dump($objPro3->getValue(new A)); // string(1) "3"

var_dump($objPro1->getDocComment());
// string(33) "/**
//      * This is a DOC.
//      */"
var_dump($objPro2->getDocComment()); // bool(false)
var_dump($objPro3->getDocComment()); // bool(false)

var_dump($objPro1->getDeclaringClass());
// object(ReflectionClass)#4 (1) {
//     ["name"]=>
//     string(1) "A"
//   }

var_dump($obj->hasProperty('a')); // bool(true)
var_dump($obj->hasProperty('aa')); // bool(false)

这几个方法,getName()、getValue()、getDocComment()、getDeclaringClass() 和上面学习过的常量当中的方法是没有什么区别的。不过要注意的是,我们反射出来的内容,只能是 Public 的哦,如果反射能够直接反射获得私有和受保护的属性值的话,那么封装的意义就不存在了,不过我们也可以通过另外一个方法来获得这些属性的值。

$objPro1->setAccessible(true);
var_dump($objPro1->getValue(new A)); // string(1) "1"

嗯,说好封装还是被我们给破坏了,说实话,在使用反射的时候对于封装的保护是一个重要的议题,这也是我们在业务开发中需要重点关注的内容。

还有就是 getValue() 需要传入一个实例化对象,这又是为什么呢?常量那边不需要是因为常量是固定的,不可改变的,而普通属性是可以改变的,也就是说,即使给了默认值,在运行时这个属性的值也是不一定的,不能直接将类模板中的属性值反射回来,而必须要根据具体的实例化对象来确定这个属性的值。

既然属性的值有这样的问题,那么我们能不能通过反射来修改属性的值呢?当然可以啦,在属性这边是有一个对应的 setValue() 方法的。

$classA = new A;
$objPro4->setValue($classA, 'This is d value.');
var_dump($classA->d); // string(16) "This is d value."

关于访问修饰符的内容,属性和常量的方法也是完全一样的,这里就只展示出来不做过多的解释了。

var_dump($objPro1->getModifiers()); // int(1024)
var_dump($objPro2->getModifiers()); // int(512)
var_dump($objPro3->getModifiers()); // int(256)

var_dump($objPro1->isPrivate()); // bool(true)
var_dump($objPro1->isProtected()); // bool(false)
var_dump($objPro1->isPublic()); // bool(false)

var_dump($objPro1->isDefault()); // bool(true)

var_dump($objPro1->isStatic()); // bool(false)
var_dump($objPro5->isStatic()); // bool(true)

在这里,我们还多了两个方法,一个是 isDefault() 用于判断这个属性是否是默认属性,也就是是否是定义在这个类模板中的。而另一个 isStatic() 方法,则用于判断这个属性反射对象是否是静态属性。

另外,还有一些方法是在 PHP7.4 以后才提供的,我这里是 PHP7.3 ,没有进行过测试,只是列出来供大家参考。

// PHP8
// var_dump($objPro1->getDefaultValue());
// var_dump($objPro1->hasDefaultValue());
// PHP7.4
// var_dump($objPro3->isInitialized());
// var_dump($objPro3->getType());
// var_dump($objPro3->hasType());

静态属性列表

最后,对应静态属性来说,还有两个特殊的方法是专门用于静态属性的列表及单个信息获取的。

var_dump($obj->getStaticProperties());
// array(2) {
//     ["e"]=>
//     string(1) "5"
//     ["f"]=>
//     NULL
//   }

var_dump($obj->getStaticPropertyValue('e')); // string(1) "5"

总结

今天一开篇的内容就不少吧,毕竟反射是一个非常大的功能集,也是我们向更高层次迈进的一个重要的知识点。所以后面我们的学习也会更加的艰苦,大家做好心理准备了吗?

测试代码:

https://github.com/zhangyue0503/dev-blog/blob/master/php/2021/05/source/1.%E4%B8%80%E8%B5%B7%E5%AD%A6%E4%B9%A0PHP%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%B0%84%EF%BC%88%E4%B8%80%EF%BC%89.php

参考文档:

https://www.php.net/manual/zh/book.reflection.php