Kodokon kodokon.com

进阶 PHP 8:特性(Attributes)、readonly、可调用值

深入探究特性(Attributes)、readonly、一等可调用值以及 PHP 8 严格类型的内部机理。

10 分钟 · 3 题

在 Kodokon 中打开本课

特性(Attributes)用真正的、由引擎校验的类取代了 docblock 注解。一个特性不过是一份沉睡的元数据:写下 #[Route('/users')]不会创建任何对象。只有当某段代码通过反射查询它并调用 newInstance() 时,它才会苏醒过来 - 这正是框架在启动时构建路由表所做的事。它所允许的目标(TARGET_METHODTARGET_CLASS……)本身也是由特性类上的一个特性来声明的。

PHP
<?php
declare(strict_types=1);

#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
    public function __construct(
        public readonly string $path,
        public readonly string $method = 'GET'
    ) {}
}

final class UserController
{
    #[Route('/users', method: 'GET')]
    public function index(): array
    {
        return ['users' => []];
    }
}

$ref = new ReflectionMethod(
    UserController::class,
    'index'
);

foreach ($ref->getAttributes(Route::class) as $attr) {
    $route = $attr->newInstance();
    echo $route->method, ' ', $route->path, PHP_EOL;
}
特性只有在调用 newInstance() 的那一刻才会被实例化。

readonly - 8.1 中作用于属性,8.2 中作用于整个类 - 保证一个属性只被写入一次,而且只能在类的作用域内写入。它是构建值对象的利器:任何"修改"都变成了构造一个新对象,这消除了一整类共享状态的 bug。一个 readonly 类要求它的所有属性都带类型且非静态。

PHP
<?php
declare(strict_types=1);

final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency
    ) {}

    public function add(Money $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException(
                'Currency mismatch'
            );
        }
        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }
}

$total = new Money(500, 'EUR');
$total = $total->add(new Money(250, 'EUR'));
echo $total->amount;
一个值对象:相加会创建一个新实例。

一等可调用语法 strtoupper(...)(PHP 8.1)会从任意函数或方法创建一个 Closure。不同于 'strtoupper' 这样的字符串或 [$obj, 'method'] 这样的数组,这个符号会在创建时就被校验,静态分析工具也能从头到尾地追踪类型。对于实例方法,闭包会捕获对象;对于静态方法,则只捕获类。

PHP
<?php
declare(strict_types=1);

$words = ['php', 'rust', 'go'];

$upper = strtoupper(...);
var_dump($upper('php'));

print_r(array_map(strtoupper(...), $words));

final class Mailer
{
    public function send(string $to): bool
    {
        return $to !== '';
    }

    public static function ping(): string
    {
        return 'pong';
    }
}

$mailer = new Mailer();
$send = $mailer->send(...);
$ping = Mailer::ping(...);

var_dump($send('dev@example.org'), $ping());
每一个 f(...) 表达式都会产生一个在创建时就经过校验的 Closure。

知识检测

确认你已牢记本课的重点内容。

  1. 一个 #[Route(...)] 特性的类是在什么时候被实例化的?
    • 在承载它的文件被编译时
    • 在被它装饰的方法一被调用时
    • 只有当某段代码通过反射查询它并调用 newInstance() 时
  2. 一个 readonly 属性究竟保证了什么?
    • 它所引用的对象会变得完全不可变
    • 它只在类的作用域内被写入一次;而被引用的对象本身仍然是可变的
    • 它只能从声明它的那个类中被读取
  3. declare(strict_types=1) 的作用范围是什么?
    • 只要有一个文件声明了它,整个项目就都生效
    • 写在包含该声明的文件里的那些调用
    • 只作用于在那个文件里定义的函数,无论它们在哪里被调用