Чистое API для чтения PHP-атрибутов: как упростить работу с Reflection
PHP 8.0 представил атрибуты — отличный способ добавлять структурированные метаданные прямо в код. Концепция элегантна, но вот Reflection API, с помощью которого эти атрибуты нужно читать, удивительно многословен и излишне детализирован. Возникает разрыв между простотой идеи и сложностью её реализации.
Spatie выпустили пакет spatie/php-attribute-reader, который устраняет этот разрыв, предлагая чистое и интуитивное API. Давайте посмотрим, что он умеет.
Использование Attribute Reader
Чтение одного PHP атрибута
Представьте, что у контроллера есть атрибут Route, и вам нужно получить его экземпляр. С нативным PHP это выглядит так:
$reflection = new ReflectionClass(MyController::class);
$attributes = $reflection->getAttributes(Route::class, ReflectionAttribute::IS_INSTANCEOF);
$route = null;
if (count($attributes) > 0) {
$route = $attributes[0]->newInstance();
}Пять строк только для одного PHP атрибута. С пакетом от Spatie всё становится проще:
use Spatie\Attributes\Attributes;
$route = Attributes::get(MyController::class, Route::class);Одна строка. Если атрибута нет — вернётся null. Никаких лишних проверок.
Для разных целей (методы, свойства, константы) есть отдельные методы:
Attributes::onMethod(MyController::class, 'index', Route::class);
Attributes::onProperty(User::class, 'email', Column::class);
Attributes::onConstant(Status::class, 'ACTIVE', Label::class);
Attributes::onParameter(MyController::class, 'show', 'id', FromRoute::class);Проверка наличия PHP атрибута
Если нужно просто проверить, есть ли атрибут, без получения его экземпляра, используйте has():
if (Attributes::has(MyController::class, Route::class)) {
// атрибут существует
}Это работает для всех типов целей — классов, методов, свойств и т.д.
Повторяющиеся PHP атрибуты
Некоторые PHP атрибуты могут быть повторяющимися (IS_REPEATABLE). Например, middleware в контроллере:
#[Route('/users')]
#[Middleware('auth')]
#[Middleware('verified')]
public function index() {}Для получения всех экземпляров используйте getAllOnMethod:
$middlewares = Attributes::getAllOnMethod(
UserController::class,
'index',
Middleware::class
);
foreach ($middlewares as $middleware) {
echo $middleware->name; // 'auth', 'verified'
}Аналогичные методы есть для свойств, констант, параметров и функций.
Поиск всех PHP атрибутов
Но нативное Reflection API становится по-настоящему громоздким, когда нужно найти все вхождения определённого атрибута в классе. Представьте класс формы, где несколько свойств используют PHP атрибут Validate для правил валидации:
#[Attribute]
class Validate
{
public function __construct(public string $rule = 'required') {}
}
#[Validate('exists:forms')]
class ContactForm
{
#[Validate('string|max:255')]
public string $name;
#[Validate('email')]
public string $email;
public function submit(#[Validate('array')] array $data) {}
}Теперь нам нужно найти все места в классе ContactForm, где используется атрибут Validate. Вот упрощённый пример того, что пришлось бы писать на нативном PHP:
// Лишь малая часть того, что вам реально понадобится:
$results = [];
$class = new ReflectionClass(ContactForm::class);
// Проверяем свойства...
foreach ($class->getProperties() as $property) {
foreach ($property->getAttributes(Validate::class) as $attr) {
// Здесь мы теряем контекст - к какому свойству относится атрибут
$results[] = $attr->newInstance();
}
}
// ...и так нужно повторить для методов, параметров и констант.
// Огромное количество повторяющегося кода, в котором легко ошибиться.Сравните с тем, как ту же задачу решает пакет:
use Spatie\Attributes\Attributes;
$results = Attributes::find(ContactForm::class, Validate::class);Вместо того чтобы писать парсер для свойств, потом для методов, а потом для параметров — вы просто говорите «найди всё». Это и есть чистое API.
Работа с результатами поиска
Каждый результат — это объект AttributeTarget с тремя полезными свойствами:
use Spatie\Attributes\AttributeTarget;
foreach ($results as $result) {
$result->attribute; // Готовый экземпляр атрибута
$result->target; // Объект Reflection (свойство, метод и т.д.)
$result->name; // Например, 'email' или 'submit.data'
}Для параметров имя формируется как метод.параметр. В примере с классом ContactForm и атрибутом Validate результаты будут выглядеть так:
| name | target type | attribute->rule |
|---|---|---|
| ContactForm | ReflectionClass | exists:forms |
| name | ReflectionProperty | string|max:255 |
| ReflectionProperty | ||
| submit.data | ReflectionParameter | array |
Получение атрибутов в виде массива
Метод toArray() преобразует все свойства PHP атрибута в ассоциативный массив. Это удобно, когда структура атрибута неизвестна заранее:
foreach ($results as $result) {
$config = $result->toArray();
// ['rule' => 'exists:forms'] для класса
// ['rule' => 'email'] для свойства email
}Доступ к reflection-объекту
Свойство target даёт полный доступ к нижележащему reflection-объекту. Можно получить дополнительную информацию о цели:
foreach (Attributes::find(ContactForm::class, Validate::class) as $result) {
if ($result->target instanceof ReflectionProperty) {
$type = $result->target->getType(); // тип свойства
}
}Один метод find заменяет десятки строк кода по всему проекту.
Работа с иерархией PHP атрибутов
Все методы пакета по умолчанию используют ReflectionAttribute::IS_INSTANCEOF. Это значит, что если вы ищете родительский класс атрибута, будут найдены и его наследники.
#[Attribute]
class CacheStrategy
{
public function __construct(public int $ttl = 3600) {}
}
#[Attribute]
class AggressiveCache extends CacheStrategy
{
public function __construct()
{
parent::__construct(ttl: 86400);
}
}
#[AggressiveCache]
class ProductCatalog {}Теперь при поиске CacheStrategy будет найден AggressiveCache:
$cache = Attributes::get(ProductCatalog::class, CacheStrategy::class);
$cache instanceof AggressiveCache; // true
$cache->ttl; // 86400Это работает для всех методов: get, has, find, onMethod, onProperty и других.
В заключение
Пакет уже используется в нескольких проектах, включая laravel-responsecache, laravel-event-sourcing и laravel-markdown. В каждом случае он помог убрать десятки строк шаблонного reflection-кода, сделав кодовую базу чище, удобнее для тестирования и понимания.
Полная документация доступна на сайте Spatie, а исходный код — на GitHub. Это один из многих пакетов, созданных командой.