Skip to content

ReflectionProperty

概述

ReflectionProperty 是 PHP 反射 API 中用于检查类属性(成员变量)的类。它提供了属性的名称、类型、修饰符、默认值、是否已初始化等信息,并且可以在运行时读取和修改 private/protected 属性的值。

PHP 8.1+

PHP 8.1+ 中,所有 ReflectionProperty 默认可以访问 private 和 protected 属性,无需调用 setAccessible(true)

基础概念

获取属性信息

通过 ReflectionClass::getProperty()ReflectionClass::getProperties() 获取 ReflectionProperty 实例。

修饰符检查

检查属性的访问控制级别(public/protected/private)、是否静态、是否只读等。

访问和修改属性值

通过 getValue()setValue() 在运行时读取和修改属性值。

语法与代码

获取属性的基本信息

php
<?php
declare(strict_types=1);

class UserProfile
{
    public string $name = '';
    protected int $age = 0;
    private string $email = '';
    public static int $totalUsers = 0;
    public readonly string $role = 'user';
}

$refClass = new \ReflectionClass(UserProfile::class);

foreach ($refClass->getProperties() as $prop) {
    $modifiers = \Reflection::getModifierNames($prop->getModifiers());
    $type = $prop->getType()?->getName() ?? 'mixed';
    echo implode(' ', $modifiers) . " {$type} \${$prop->getName()}\n";
}

获取特定属性

php
<?php
declare(strict_types=1);

$prop = new \ReflectionProperty(UserProfile::class, 'name');
echo "属性名: " . $prop->getName() . "\n";
echo "类型: " . $prop->getType()?->getName() . "\n";
echo "是否 public: " . ($prop->isPublic() ? 'yes' : 'no') . "\n";
echo "是否 static: " . ($prop->isStatic() ? 'yes' : 'no') . "\n";
echo "是否只读: " . ($prop->isReadOnly() ? 'yes' : 'no') . "\n";

访问和修改属性值

php
<?php
declare(strict_types=1);

class Secret
{
    private string $password = 'hidden';
}

$secret = new Secret();
$refProp = new \ReflectionProperty(Secret::class, 'password');

echo $refProp->getValue($secret) . "\n"; // hidden

$refProp->setValue($secret, 'new_password');
echo $refProp->getValue($secret) . "\n"; // new_password

获取默认值

php
<?php
declare(strict_types=1);

$prop = new \ReflectionProperty(UserProfile::class, 'age');
echo "默认值: " . var_export($prop->getDefaultValue(), true) . "\n"; // 0
echo $prop->hasDefaultValue() ? 'yes' : 'no'; // yes

详细说明

ReflectionProperty 常用方法

方法说明
getName()属性名
getType()类型(ReflectionType)
getModifiers()修饰符位掩码
getDefaultValue()默认值
getValue($object)获取属性值
setValue($object, $value)设置属性值
isPublic() / isProtected() / isPrivate()访问级别
isStatic()是否静态
isReadOnly()是否只读(PHP 8.1+)
isInitialized($object)是否已初始化(PHP 8.0+)

只读属性不能被 setValue

PHP 8.1+ 的 readonly 属性只能在构造函数中初始化,setValue() 会抛出异常。

实战示例

实战:对象属性复制器

php
<?php
declare(strict_types=1);

class ObjectCloner
{
    public static function cloneProperties(object $source, object $target): void
    {
        $refSource = new \ReflectionClass($source);
        $refTarget = new \ReflectionClass($target);

        foreach ($refSource->getProperties() as $prop) {
            $propName = $prop->getName();
            if ($refTarget->hasProperty($propName)) {
                $targetProp = $refTarget->getProperty($propName);
                $targetProp->setValue($target, $prop->getValue($source));
            }
        }
    }
}

注意事项

静态属性的 getValue

静态属性不需要传入对象实例,传入 null 即可。

php
<?php
declare(strict_types=1);

$prop = new \ReflectionProperty(UserProfile::class, 'totalUsers');
echo $prop->getValue(null); // 0
$prop->setValue(null, 100);

最佳实践

  1. 缓存 ReflectionProperty 实例
  2. 谨慎修改私有属性:仅在测试或框架代码中使用。
php
<?php
declare(strict_types=1);

function getProperty(object $object, string $name): mixed
{
    return (new \ReflectionClass($object))
        ->getProperty($name)
        ->getValue($object);
}

ReflectionProperty 高级用法

批量获取和设置属性

php
<?php
declare(strict_types=1);

class Product
{
    public string $name = '';
    public float $price = 0.0;
    public int $stock = 0;
    private string $internalCode = '';
    protected bool $isActive = true;
}

$product = new Product();

// 批量设置公共属性
$refClass = new \ReflectionClass(Product::class);

foreach ($refClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
    $name = $prop->getName();
    $prop->setValue($product, match ($name) {
        'name' => 'Widget Pro',
        'price' => 29.99,
        'stock' => 100,
        default => null,
    });
}

// 批量读取所有属性
$allValues = [];
foreach ($refClass->getProperties() as $prop) {
    $allValues[$prop->getName()] = $prop->getValue($product);
}
print_r($allValues);

属性类型分析

php
<?php
declare(strict_types=1);

class TypedProperties
{
    public string $name = '';
    public ?int $age = null;
    public array $tags = [];
    public readonly float $score = 0.0;
    public static string $global = 'value';
}

function analyzeProperties(string $className): array
{
    $refClass = new \ReflectionClass($className);
    $analysis = [];

    foreach ($refClass->getProperties() as $prop) {
        $type = $prop->getType();
        $typeName = $type?->getName() ?? 'mixed';

        $analysis[$prop->getName()] = [
            'type' => $typeName,
            'allowsNull' => $type?->allowsNull() ?? true,
            'isBuiltin' => ($type instanceof \ReflectionNamedType) ? $type->isBuiltin() : false,
            'isReadOnly' => $prop->isReadOnly(),
            'isStatic' => $prop->isStatic(),
            'hasDefault' => $prop->hasDefaultValue(),
            'defaultValue' => $prop->hasDefaultValue()
                ? var_export($prop->getDefaultValue(), true)
                : null,
            'modifiers' => \Reflection::getModifierNames($prop->getModifiers()),
            'isInitialized' => $prop->isInitialized(new $className()),
        ];
    }

    return $analysis;
}

print_r(analyzeProperties(TypedProperties::class));

反射属性的 HasDefault 陷阱

php
<?php
declare(strict_types=1);

class DefaultValueTest
{
    public string $a = 'default';   // hasDefaultValue: true
    public string $b;               // hasDefaultValue: false(未初始化的 typed property)
    public $c;                      // hasDefaultValue: true(值为 null)
}

$refClass = new \ReflectionClass(DefaultValueTest::class);

foreach ($refClass->getProperties() as $prop) {
    echo "\${$prop->getName()}: ";
    echo $prop->hasDefaultValue() ? 'has default' : 'no default';
    echo " (value: " . var_export($prop->getDefaultValue() ?? 'no value', true) . ")\n";
}

PHP 8.0 Typed Properties

PHP 8.0 引入了类型属性。未初始化的 typed property 的 hasDefaultValue() 返回 false。访问未初始化的 typed property 会抛出 Error

实战:ORM 风格的实体映射器

php
<?php
declare(strict_types=1);

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
readonly class ColumnMapping
{
    public function __construct(
        public string $columnName,
        public string $type = 'string',
    ) {}
}

class UserEntity
{
    #[ColumnMapping(columnName: 'user_id', type: 'integer')]
    private int $id = 0;

    #[ColumnMapping(columnName: 'user_name', type: 'string')]
    private string $username = '';

    #[ColumnMapping(columnName: 'email_addr', type: 'string')]
    private string $email = '';

    #[ColumnMapping(columnName: 'is_active', type: 'boolean')]
    private bool $active = true;
}

function entityToRow(object $entity): array
{
    $refClass = new \ReflectionClass($entity);
    $row = [];

    foreach ($refClass->getProperties() as $prop) {
        $attrs = $prop->getAttributes(ColumnMapping::class);
        if (!empty($attrs)) {
            $mapping = $attrs[0]->newInstance();
            $row[$mapping->columnName] = $prop->getValue($entity);
        }
    }

    return $row;
}

function rowToEntity(string $className, array $row): object
{
    $entity = new $className();
    $refClass = new \ReflectionClass($className);

    foreach ($refClass->getProperties() as $prop) {
        $attrs = $prop->getAttributes(ColumnMapping::class);
        if (!empty($attrs)) {
            $mapping = $attrs[0]->newInstance();
            if (array_key_exists($mapping->columnName, $row)) {
                $prop->setValue($entity, $row[$mapping->columnName]);
            }
        }
    }

    return $entity;
}

// 测试
$user = new UserEntity();
$user->id = 1;
$user->username = 'alice';
$user->email = 'alice@example.com';

$row = entityToRow($user);
print_r($row);
// ['user_id' => 1, 'user_name' => 'alice', 'email_addr' => 'alice@example.com', 'is_active' => true]

$restored = rowToEntity(UserEntity::class, $row);

常见误区与 FAQ

可以通过反射给 readonly 属性赋值吗?

PHP 8.1+ 的 readonly 属性只能在构造函数中初始化一次。通过反射的 setValue() 可以绕过这个限制(仅限未初始化时),但这是不推荐的做法。

isInitialized 对未声明类型的属性

未声明类型的属性(如 public $x;)始终被视为已初始化(值为 null)。只有 typed properties 才有"未初始化"状态。

如何判断属性是否被声明为 promoted constructor parameter?

php
<?php
declare(strict_types=1);

class Example
{
    public function __construct(public readonly string $name = '') {}
}

$prop = new \ReflectionProperty(Example::class, 'name');
echo $prop->isPromoted() ? 'promoted' : 'not promoted'; // promoted (PHP 8.1+)

属性反射的高级模式

属性分组与过滤

php
<?php
declare(strict_types=1);

class Configuration
{
    // 系统配置
    private string $systemName = 'Default';
    private int $systemTimeout = 30;
    private bool $systemDebug = false;

    // 数据库配置
    private string $dbHost = 'localhost';
    private int $dbPort = 3306;
    private string $dbName = 'app';

    // 缓存配置
    private string $cacheDriver = 'redis';
    private int $cacheTtl = 3600;
}

function getPropertiesByPrefix(string $className, string $prefix): array
{
    $refClass = new \ReflectionClass($className);
    $result = [];

    foreach ($refClass->getProperties(\ReflectionProperty::IS_PRIVATE) as $prop) {
        if (str_starts_with($prop->getName(), $prefix)) {
            $result[$prop->getName()] = $prop;
        }
    }

    return $result;
}

// 获取所有 db 开头的属性
$dbProps = getPropertiesByPrefix(Configuration::class, 'db');
echo "数据库相关属性:\n";
foreach ($dbProps as $name => $prop) {
    echo "  - \${$name}: " . ($prop->getType()?->getName() ?? 'mixed') . "\n";
}

属性可见性过滤

php
<?php
declare(strict_types=1);

$refClass = new \ReflectionClass(Configuration::class);

$publicProps = $refClass->getProperties(\ReflectionProperty::IS_PUBLIC);
$protectedProps = $refClass->getProperties(\ReflectionProperty::IS_PROTECTED);
$privateProps = $refClass->getProperties(\ReflectionProperty::IS_PRIVATE);
$staticProps = $refClass->getProperties(\ReflectionProperty::IS_STATIC);

echo "Public 属性: " . count($publicProps) . "\n";
echo "Protected 属性: " . count($protectedProps) . "\n";
echo "Private 属性: " . count($privateProps) . "\n";
echo "Static 属性: " . count($staticProps) . "\n";

// 组合过滤:获取所有 public static 属性
$publicStatic = $refClass->getProperties(
    \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_STATIC
);

属性初始化状态检查

php
<?php
declare(strict_types=1);

class PartialUser
{
    public string $name;         // 未初始化 typed property
    public string $email = '';  // 有默认值
    public int $age;            // 未初始化
}

$user = new PartialUser();
$user->email = 'test@test.com';

$refClass = new \ReflectionClass(PartialUser::class);

foreach ($refClass->getProperties() as $prop) {
    echo "\${$prop->getName()}: ";
    echo "初始化=" . ($prop->isInitialized($user) ? 'yes' : 'no') . " ";
    echo "有默认值=" . ($prop->hasDefaultValue() ? 'yes' : 'no') . "\n";
}
// $name: 初始化=no 有默认值=no
// $email: 初始化=yes 有默认值=yes
// $age: 初始化=no 有默认值=no

未初始化的 typed property

访问未初始化的 typed property 会抛出 Error。务必在访问前用 isInitialized() 检查。

常见误区

clone 对象时属性反射的行为

php
<?php
declare(strict_types=1);

$refProp = new \ReflectionProperty(PartialUser::class, 'name');
$original = new PartialUser();
$original->name = 'Alice';

$cloned = clone $original;
echo $refProp->getValue($cloned); // 'Alice' — clone 会复制属性值

反射属性与 __debugInfo 的关系

ReflectionProperty 不受 __debugInfo() 魔术方法影响。反射直接访问对象的真实属性,不会调用任何魔术方法。

参考链接