Skip to content

PHP 属性挂钩(Property Hooks)

概述

属性挂钩(Property Hooks),在其他语言中也称为"属性访问器"(Property Accessors),是 PHP 8.4 引入的重要特性。它允许拦截和覆盖属性的读写行为,使得传统的 getter/setter 方法变得不再必要。

版本要求

  • PHP 8.4+:属性挂钩(Property Hooks)

基础概念

什么是属性挂钩

属性挂钩为属性提供 getset 两个钩子,分别控制属性的读取和写入行为。它的两大用途:

  1. 替代 getter/setter 方法:直接使用属性,保留未来添加额外行为的可能性
  2. 虚拟属性:属性不直接存储值,而是通过计算派生

Backed 属性与虚拟属性

  • Backed 属性:有后备存储,挂钩中引用 $this->propertyName,属性在内存中实际存储值
  • 虚拟属性:挂钩中不引用属性本身,不占用内存空间,值完全由挂钩计算

语法与代码

基本语法

属性以 {} 结尾(而非分号)表示存在挂钩。

php
<?php
declare(strict_types=1);

class Example
{
    private bool $modified = false;

    public string $foo = 'default value'
    {
        get {
            if ($this->modified) {
                return $this->foo . ' (modified)';
            }
            return $this->foo;
        }
        set(string $value) {
            $this->foo = strtolower($value);
            $this->modified = true;
        }
    }
}

$example = new Example();
$example->foo = 'changed';
echo $example->foo; // changed (modified)

简写语法

get 箭头表达式:当 get 挂钩是单个表达式时,可省略 {}

php
<?php
declare(strict_types=1);

class Example
{
    public string $foo = 'default value'
    {
        get => $this->foo . ($this->modified ? ' (modified)' : '');
        set(string $value) {
            $this->foo = strtolower($value);
            $this->modified = true;
        }
    }
}

set 默认值:当 set 挂钩参数类型与属性类型相同时,可省略类型声明,值自动命名为 $value

php
<?php
declare(strict_types=1);

class Example
{
    public string $foo = 'default value'
    {
        get => $this->foo . ($this->modified ? ' (modified)' : '');
        set {
            $this->foo = strtolower($value);
            $this->modified = true;
        }
    }
}

set 箭头表达式set 挂钩仅做简单转换时。

php
<?php
declare(strict_types=1);

class Example
{
    public string $foo = 'default value'
    {
        get => $this->foo . ($this->modified ? ' (modified)' : '');
        set => strtolower($value);
    }
}

虚拟属性

虚拟属性没有后备值,不占用内存。适合派生属性场景。

php
<?php
declare(strict_types=1);

class Rectangle
{
    public int $area
    {
        get => $this->h * $this->w;
    }

    public function __construct(
        public int $h,
        public int $w,
    ) {}
}

$rect = new Rectangle(4, 5);
echo $rect->area; // 20

// Error: 没有定义 set 操作
// $rect->area = 30;

详细说明

set 挂钩的类型放宽

set 挂钩的参数类型可以比属性类型更宽泛(逆变)。例如 string 属性可以接受 string|Stringable 的 set 值。

php
<?php
declare(strict_types=1);

class Label
{
    public string $text
    {
        set(string|\Stringable $value) {
            $this->text = (string) $value;
        }
    }
}

$label = new Label();
$label->text = 'Hello';          // string 直接赋值
$label->text = new class implements \Stringable {
    public function __toString(): string
    {
        return 'World';
    }
}; // Stringable 对象赋值
echo $label->text; // World

挂钩中调用方法

挂钩在对象作用域内运行,可以访问所有属性和方法。

php
<?php
declare(strict_types=1);

class Person
{
    public string $phone
    {
        set => $this->sanitizePhone($value);
    }

    private function sanitizePhone(string $value): string
    {
        $value = ltrim($value, '+');
        $value = ltrim($value, '1');

        if (!preg_match('/\d{3}-\d{3}-\d{4}/', $value)) {
            throw new \InvalidArgumentException("Invalid phone: {$value}");
        }

        return $value;
    }
}

继承中的挂钩

子类可以通过重新声明属性来覆盖挂钩,使用 parent::$prop::get() 访问父类挂钩。

php
<?php
declare(strict_types=1);

class Point
{
    public int $x;
    public int $y;
}

class PositivePoint extends Point
{
    public int $x
    {
        set {
            if ($value < 0) {
                throw new \InvalidArgumentException('Too small');
            }
            parent::$x::set($value);
        }
    }
}

Final 挂钩

挂钩可声明为 final,防止子类覆盖。

php
<?php
declare(strict_types=1);

class User
{
    public string $username
    {
        final set => strtolower($value);
    }
}

class Manager extends User
{
    public string $username
    {
        // 允许覆盖 get
        get => strtoupper($this->username);
        // Error: Cannot override final set hook
        // set => strtoupper($value);
    }
}

属性挂钩与 readonly

属性挂钩与 readonly 不兼容。如果需要控制读写权限,使用不对称属性可见性

php
<?php
declare(strict_types=1);

// 推荐替代方案:不对称可见性
class Book
{
    public function __construct(
        public private(set) string $title,
    ) {}
}

$book = new Book('PHP 8.4');
echo $book->title;    // 可读
// $book->title = '';  // Error: Cannot modify private(set)

实战示例

带验证的实体类

php
<?php
declare(strict_types=1);

class UserProfile
{
    public string $email
    {
        set {
            if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
                throw new \InvalidArgumentException("Invalid email: {$value}");
            }
            $this->email = strtolower($value);
        }
    }

    public int $age
    {
        set {
            if ($value < 0 || $value > 150) {
                throw new \InvalidArgumentException("Invalid age: {$value}");
            }
            $this->age = $value;
        }
    }

    public string $fullName
    {
        get => "{$this->firstName} {$this->lastName}";
    }

    public function __construct(
        public string $firstName = '',
        public string $lastName = '',
        string $email = '',
        int $age = 0,
    ) {
        $this->email = $email;
        $this->age = $age;
    }
}

$profile = new UserProfile(email: 'test@example.com', age: 25);
$profile->firstName = 'John';
$profile->lastName = 'Doe';
echo $profile->fullName; // John Doe

注意事项

  1. 属性挂钩与 readonly 不兼容:需要控制读写权限时使用不对称可见性
  2. set 挂钩与构造器属性提升:提升时使用属性类型作为构造函数参数类型
  3. 虚拟属性不能写入:未定义 set 的虚拟属性赋值会报错
  4. 数组属性的引用问题:写入数组元素涉及隐式引用,需要特殊处理

最佳实践

  • 新项目优先使用属性挂钩替代传统的 getter/setter 方法
  • 验证逻辑放在 set 挂钩中,确保属性始终保持有效状态
  • 派生属性使用虚拟属性,避免冗余数据
  • 使用 final 防止关键挂钩被覆盖,保证业务逻辑一致性
  • 序列化时注意行为差异var_dump/serialize 使用原始值,json_encode 使用 get 挂钩

进阶用法

调试与测试技巧

php
<?php
declare(strict_types=1);

// 单元测试辅助函数
function createTestResource(): mixed
{
    return match (true) {
        default => new stdClass(),
    };
}

// 调试输出函数
function debugOutput(mixed , string  = ''): void
{
     =  ? ": " : '';
     .= print_r(, true);
    fwrite(STDERR,  . "\n");
}

// 性能基准测试
function benchmark(callable , int  = 1000): float
{
     = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e9;
}

日志记录实践

php
<?php
declare(strict_types=1);

/**
 * 简易日志记录器
 */
class SimpleLogger
{
    private string $logFile;
    private string $level = 'INFO';

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('WARNING', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    private function log(string $level, string $message, array $context): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

配置与环境检测

php
<?php
declare(strict_types=1);

// 环境检测工具
class EnvironmentChecker
{
    public static function checkRequirements(array $requirements): array
    {
        $results = [];
        foreach ($requirements as $name => $check) {
            $results[$name] = is_callable($check) ? $check() : false;
        }
        return $results;
    }

    public static function getSystemInfo(): array
    {
        return [
            'php_version' => PHP_VERSION,
            'os' => PHP_OS,
            'sapi' => PHP_SAPI,
            'memory_limit' => ini_get('memory_limit'),
            'max_execution_time' => ini_get('max_execution_time'),
            'loaded_extensions' => get_loaded_extensions(),
        ];
    }
}

常见问题排查

问题可能原因解决方案
连接超时网络问题/配置错误检查配置,增加超时时间
权限不足文件/目录权限使用 chmod/chown 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 8.0__construct(public $x)
php
<?php
declare(strict_types=1);

// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
    if (version_compare(PHP_VERSION, $minVersion, '<')) {
        throw new RuntimeException(
            sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
        );
    }
}

ensureVersion('8.1.0');

参考链接