Kodokon kodokon.com

Продвинутый PHP 8: атрибуты, readonly, callable

Изучи внутреннее устройство атрибутов, readonly, callable как объектов первого класса и строгой типизации PHP 8.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Атрибуты заменяют аннотации в докблоках настоящими классами, которые проверяет движок. Атрибут - это всего лишь спящие метаданные: запись #[Route('/users')] не создаёт никакого объекта. Он оживает только тогда, когда какой-то код запрашивает его через рефлексию и вызывает newInstance() - именно это фреймворки делают при старте, чтобы построить свои таблицы маршрутов. Разрешённая цель (TARGET_METHOD, TARGET_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 - гарантирует, что свойство записывается только один раз и только изнутри области видимости класса. Это инструмент для объектов-значений: любое «изменение» превращается в создание нового объекта, что убирает целое семейство багов с общим состоянием. Класс 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;
Объект-значение: сложение создаёт новый экземпляр.

Синтаксис callable первого класса 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)?
    • Весь проект, как только хотя бы один файл его объявляет
    • Вызовы, написанные в том файле, где находится объявление
    • Только функции, определённые в этом файле, где бы их ни вызывали