Что такое attributes

Attributes в PHP — это структурированные метаданные прямо в коде. Они появились в PHP 8.0 и записываются синтаксисом #[...] над классом, методом, функцией, свойством, параметром, class constant или, в актуальных версиях PHP, обычной константой. В отличие от комментария, attribute разбирается PHP-парсером и доступен через Reflection API.

Проще всего думать о них как о маленькой декларативной конфигурации рядом с тем местом, к которому она относится. Например: «этот метод — HTTP route», «это поле нужно сериализовать под таким именем», «этот параметр нельзя показывать в stack trace». Сам PHP не знает бизнес-смысл большинства ваших attributes: он хранит метаданные, а приложение, библиотека или фреймворк читают их через Reflection и решают, что делать.

Это продолжает тему из Классы, объекты и видимость: attribute — тоже class-like сущность. Его имя подчиняется тем же правилам namespace и use, что и класс, enum или interface из Неймспейсы и use, а в проекте с Composer обычно автозагружается через Autoloading и PSR-4.

Быстрое повторение
Почему attribute в PHP можно читать надёжнее, чем комментарий PHPDoc, если код должен принять runtime-решение?

Attribute как класс

Чтобы объявить свой attribute, создают обычный класс и помечают его встроенным #[Attribute]. Аргументы, которые вы пишете в #[Route('GET', '/users')], позже попадут в конструктор attribute-класса.

<?php

declare(strict_types=1);

namespace App\Http;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Route
{
    public function __construct(
        public string $method,
        public string $path,
    ) {}
}

Здесь Route можно ставить только на методы, и один метод может иметь несколько таких attributes. Без Attribute::IS_REPEATABLE PHP считает attribute одноразовым для конкретного объявления.

Аргументы attribute должны быть literal values или constant expressions: строки, числа, массивы, class constants, именованные аргументы, выражения вроде 100 + 200. Нельзя передать результат функции, сервис из контейнера или значение из базы. Это важное ограничение: attribute описывает код, а не выполняет runtime-логику.

Быстрое повторение
Как объявить собственный attribute-класс в PHP?

Targets и repeatable rules

Флаги Attribute::TARGET_* задают, где attribute разрешён. Частые варианты: TARGET_CLASS, TARGET_METHOD, TARGET_PROPERTY, TARGET_PARAMETER, TARGET_FUNCTION, TARGET_CLASS_CONSTANT, TARGET_ALL. В PHP 8.5 добавлен TARGET_CONSTANT для обычных констант; если библиотека должна поддерживать PHP 8.2–8.4, на него нельзя опираться без проверки минимальной версии.

#[Attribute(Attribute::TARGET_PROPERTY)]
final class Column
{
    public function __construct(public string $name) {}
}

final class User
{
    #[Column('email_address')]
    public string $email;
}

Тонкий момент: часть проверок откладывается до Reflection. Если attribute применён не туда, ошибка может проявиться не в момент загрузки файла, а когда код вызовет ReflectionAttribute::newInstance(). Поэтому инфраструктурный код, который сканирует attributes, обычно запускают в тестах, при прогреве кеша или на старте приложения, чтобы проблемы не вылезали случайно в середине запроса.

flowchart TD A[Код с #[Attribute]] --> B[PHP парсер хранит metadata] B --> C[ReflectionClass / ReflectionMethod / ReflectionProperty] C --> D[getAttributes() возвращает ReflectionAttribute] D --> E{Нужен объект attribute?} E -- нет --> F[getName() / getArguments()] E -- да --> G[newInstance() вызывает конструктор] G --> H[Фреймворк строит routes, validators, mappings или кеш]
flowchart TD
    A[Код с #[Attribute]] --> B[PHP парсер хранит metadata]
    B --> C[ReflectionClass / ReflectionMethod / ReflectionProperty]
    C --> D[getAttributes() возвращает ReflectionAttribute]
    D --> E{Нужен объект attribute?}
    E -- нет --> F[getName() / getArguments()]
    E -- да --> G[newInstance() вызывает конструктор]
    G --> H[Фреймворк строит routes, validators, mappings или кеш]
Как attribute превращается из синтаксиса в данные, которые читает инфраструктурный код.
Быстрое повторение
Что может случиться, если attribute применили не к тому target, например property-only attribute поставили на метод?

Чтение через Reflection

Reflection API позволяет смотреть на структуру программы как на данные: классы, методы, свойства, параметры, типы, комментарии и attributes. Для attributes есть общий паттерн: взять ReflectionClass, ReflectionMethod, ReflectionProperty или другой reflection-объект, вызвать getAttributes(), затем при необходимости создать экземпляр через newInstance().

<?php

declare(strict_types=1);

use App\Http\Route;

final class UserController
{
    #[Route('GET', '/users')]
    #[Route('POST', '/users')]
    public function collection(): void
    {
    }
}

$method = new ReflectionMethod(UserController::class, 'collection');

foreach ($method->getAttributes(Route::class) as $attribute) {
    $route = $attribute->newInstance();
    echo $route->method . ' ' . $route->path . PHP_EOL;
}

Вывод будет таким:

GET /users
POST /users

getAttributes() возвращает не сами ваши Route, а объекты ReflectionAttribute. У них можно спросить getName(), getArguments(), getTarget(), isRepeated() и только потом вызвать newInstance(). Это сделано намеренно: можно сначала отфильтровать метаданные, залогировать имя неизвестного attribute или обработать ошибку аргументов аккуратнее.

Если нужна выборка по базовому классу или интерфейсу attribute, используют флаг ReflectionAttribute::IS_INSTANCEOF:

$attributes = $method->getAttributes(
    MiddlewareAttribute::class,
    ReflectionAttribute::IS_INSTANCEOF,
);

Это полезно, когда несколько разных attributes реализуют общий контракт. Здесь видна связь с Наследование, интерфейсы и трейты: обычная объектная модель работает и для metadata-классов.

Metadata-driven code

Attributes чаще всего встречаются там, где код хочет быть декларативным. HTTP-routing может читать #[Route]; serializer — #[Column] или #[SerializedName]; валидатор — #[NotBlank]; DI-контейнер — #[Autowire]; тестовый фреймворк — #[Test]. В таких местах attribute заменяет отдельный YAML/XML/PHP config или договорённость в названии метода.

Это не значит, что всё нужно переносить в attributes. Хороший attribute короткий, стабильный и близкий к объявлению. Плохой attribute превращается в спрятанный язык программирования:

#[Route('GET', '/users')]
#[RequiresRole('admin')]
#[Cache(ttl: 60)]
public function index(): Response
{
    // Реальная логика остаётся в методе и сервисах.
}

Здесь metadata описывает входные правила. Но если в attribute начинают класть сложные условия, SQL, callback-цепочки и зависимости от текущего пользователя, код становится сложнее читать и тестировать. Для PSR-7, middleware и HTTP-клиенты attribute может быть удобной точкой объявления route, но сам request/response pipeline всё равно лучше держать в явных объектах и middleware.

Attributes и PHPDoc

Attributes не заменяют PHPDoc полностью. Они решают другую задачу. Attribute — машинно-читаемая runtime-метаинформация с классом, конструктором и ограничениями target. PHPDoc — комментарий для людей, IDE и статических анализаторов. Он по-прежнему нужен для generics, shape-типов, template-параметров, сложных @var, @param, @return и других вещей, которых нет в системе типов PHP.

Например, #[Route] хорошо говорит рантайму, какой URL связан с методом. А @return list<User> пока остаётся зоной PHPDoc, generics и статический анализ, потому что PHP не имеет встроенного синтаксиса generic-типов для массивов.

Практическое правило: если информацию должен читать ваш код во время выполнения, рассмотрите attribute. Если информация нужна анализатору, IDE или человеку и не влияет на runtime-поведение, часто достаточно PHPDoc. Иногда они стоят рядом, и это нормально.

Ограничения и осторожность

Reflection удобен, но это интроспекция, а не обычный вызов метода. В больших приложениях сканирование всех классов на каждый HTTP-запрос может стать дорогим. Поэтому фреймворки часто кешируют результат: один раз читают attributes, строят таблицу routes, validators или mappings, а потом используют уже готовую структуру.

Ещё один риск — магия. Когда поведение приложения определяется metadata, часть причинно-следственной связи уходит из обычного call stack. Это приемлемо для инфраструктуры, но плохо для доменной логики. Не прячьте бизнес-решения в attributes только потому, что синтаксис выглядит аккуратно.

И наконец, attributes завязаны на имена классов. Переименование namespace, ошибка в use или несовместимость версии PHP проявятся при Reflection. Здесь помогает та же дисциплина, что и в остальной объектной модели: PSR-4 autoloading, статический анализ, тесты инфраструктурного слоя и аккуратные минимальные контракты.

См. также

Источники

  1. PHP Manual: Attributes
  2. PHP Manual: Attribute syntax
  3. PHP Manual: Reading Attributes with the Reflection API
  4. PHP Manual: Declaring Attribute Classes
  5. PHP Manual: The Attribute attribute
  6. PHP Manual: Reflection
  7. PHP Manual: ReflectionAttribute
  8. PHP Manual: ReflectionClass