Что такое 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 как класс
Чтобы объявить свой 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-логику.
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 или кеш]Чтение через 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 /usersgetAttributes() возвращает не сами ваши 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, статический анализ, тесты инфраструктурного слоя и аккуратные минимальные контракты.
См. также
- Классы, объекты и видимость — attribute-классы используют обычные свойства, конструкторы и visibility.
- Наследование, интерфейсы и трейты — как строить общий контракт для группы attributes.
- Неймспейсы и use — как PHP разрешает имена attribute-классов.
- Autoloading и PSR-4 — почему attribute должен быть доступен автозагрузчику при
newInstance(). - Enum в PHP — enum cases тоже можно исследовать через Reflection API.
- PHPDoc, generics и статический анализ — где PHPDoc остаётся незаменимым рядом с attributes.
- PSR-7, middleware и HTTP-клиенты — частый контекст для route и middleware metadata.