Основы атрибутов — PHP 8.0+
Что такое атрибуты
Атрибуты -- это структурированные метаданные, которые можно прикрепить к классам, методам, свойствам, параметрам, константам и функциям. Они пришли на замену аннотациям в doc-блоках (таким как @Route, @ORM\Column из Doctrine).
До PHP 8.0 -- метаданные через комментарии:
<?php
/**
* @Route("/api/users", methods={"GET"})
* @IsGranted("ROLE_ADMIN")
*/
class UserController
{
/**
* @var UserService
* @Inject
*/
private $service;
}
С PHP 8.0 -- нативные атрибуты:
<?php
declare(strict_types=1);
#[Route('/api/users', methods: ['GET'])]
#[IsGranted('ROLE_ADMIN')]
class UserController
{
#[Inject]
private UserService $service;
}
Ключевые отличия от doc-блоков:
- Нативный синтаксис -- часть языка, не комментарии
- Проверяются при компиляции -- ошибка в имени атрибута вызовет исключение при инстанцировании
- Типизированные аргументы -- конструктор атрибута проверяет типы
- Автодополнение в IDE -- полная поддержка PhpStorm, VS Code
Синтаксис
Атрибуты записываются в #[...] перед целевой конструкцией:
<?php
declare(strict_types=1);
// Атрибут на классе
#[Route('/api/users', methods: ['GET'])]
class UserController
{
// Атрибут на свойстве
#[Inject]
private UserService $service;
// Атрибут на константе класса
#[Deprecated('Use VERSION_2 instead')]
public const VERSION_1 = '1.0';
// Несколько атрибутов на методе
#[Route('/api/users/{id}')]
#[Middleware('auth')]
public function show(
// Атрибут на параметре
#[FromRoute] int $id
): Response {
return new Response($this->service->find($id));
}
}
// Атрибут на функции
#[Pure]
function calculateSum(int $a, int $b): int
{
return $a + $b;
}
Несколько атрибутов можно записать в одних скобках через запятую:
<?php
declare(strict_types=1);
// Два способа -- эквивалентны
#[Route('/users')]
#[Middleware('auth')]
public function index(): Response {}
// Компактный вариант
#[Route('/users'), Middleware('auth')]
public function index(): Response {}
Запомни: Атрибуты -- это просто классы PHP.
#[Route('/path')]эквивалентноnew Route('/path'), только вызовnewInstance()происходит через Reflection.
Цели атрибутов (targets)
При создании атрибута можно ограничить, к чему он применяется. Это делается через флаги Attribute::TARGET_*:
<?php
declare(strict_types=1);
use Attribute;
// Только для методов и функций
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly array $methods = ['GET'],
) {}
}
// Только для свойств
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Column
{
public function __construct(
public readonly string $name,
public readonly string $type = 'string',
public readonly bool $nullable = false,
) {}
}
// Для всего (по умолчанию)
#[Attribute]
final class Description
{
public function __construct(
public readonly string $text,
) {}
}
Полный список целей:
| Флаг | Назначение |
|---|---|
Attribute::TARGET_CLASS |
Классы, интерфейсы, трейты, enums |
Attribute::TARGET_FUNCTION |
Функции |
Attribute::TARGET_METHOD |
Методы классов |
Attribute::TARGET_PROPERTY |
Свойства классов |
Attribute::TARGET_CLASS_CONSTANT |
Константы классов |
Attribute::TARGET_PARAMETER |
Параметры функций/методов |
Attribute::TARGET_ALL |
Все вышеперечисленное (по умолчанию) |
Если атрибут применен не к той цели, при вызове newInstance() будет выброшен Error:
<?php
declare(strict_types=1);
#[Attribute(Attribute::TARGET_METHOD)]
final class OnlyForMethods {}
// Применяем к классу -- ошибки при объявлении НЕ будет
#[OnlyForMethods]
class Foo {}
// Ошибка возникнет только при чтении через Reflection
$ref = new ReflectionClass(Foo::class);
$attrs = $ref->getAttributes(OnlyForMethods::class);
$attrs[0]->newInstance();
// Error: Attribute "OnlyForMethods" cannot target class (is only for methods)
Создание пользовательских атрибутов
Атрибут -- это обычный PHP-класс с атрибутом #[Attribute]:
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly array $methods = ['GET'],
public readonly ?string $name = null,
public readonly array $middleware = [],
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Validate
{
public function __construct(
public readonly string $rule,
public readonly ?string $message = null,
) {}
}
#[Attribute(Attribute::TARGET_CLASS)]
final class Entity
{
public function __construct(
public readonly string $table,
public readonly ?string $repository = null,
) {}
}
Использование:
<?php
declare(strict_types=1);
#[Entity(table: 'users', repository: UserRepository::class)]
final class User
{
#[Validate(rule: 'email', message: 'Invalid email')]
public string $email;
#[Validate(rule: 'min:8')]
public string $password;
}
Чтение атрибутов через Reflection
Атрибуты сами по себе ничего не делают. Они хранятся в метаданных и читаются через Reflection API:
<?php
declare(strict_types=1);
// Чтение атрибутов класса
$ref = new ReflectionClass(User::class);
$attributes = $ref->getAttributes(Entity::class);
foreach ($attributes as $attr) {
echo $attr->getName(); // 'Entity'
echo $attr->getTarget(); // Attribute::TARGET_CLASS
$args = $attr->getArguments(); // ['table' => 'users', ...]
// Создание экземпляра атрибута
$entity = $attr->newInstance();
echo $entity->table; // 'users'
echo $entity->repository; // 'UserRepository'
}
// Чтение атрибутов свойств
foreach ($ref->getProperties() as $prop) {
$attrs = $prop->getAttributes(Validate::class);
foreach ($attrs as $attr) {
$validate = $attr->newInstance();
echo "{$prop->getName()}: {$validate->rule}\n";
}
}
// Чтение атрибутов метода
$method = new ReflectionMethod(UserController::class, 'show');
$routeAttrs = $method->getAttributes(Route::class);
if (count($routeAttrs) > 0) {
$route = $routeAttrs[0]->newInstance();
echo $route->path; // '/api/users/{id}'
echo $route->methods; // ['GET']
}
// Чтение атрибутов параметров
foreach ($method->getParameters() as $param) {
$attrs = $param->getAttributes();
foreach ($attrs as $attr) {
echo "{$param->getName()} has attribute: {$attr->getName()}\n";
}
}
Метод getAttributes() принимает необязательные параметры:
<?php
declare(strict_types=1);
// Все атрибуты
$all = $ref->getAttributes();
// Только конкретный атрибут
$routes = $ref->getAttributes(Route::class);
// Атрибуты по наследованию (для интерфейсов)
$attrs = $ref->getAttributes(
MyInterface::class,
ReflectionAttribute::IS_INSTANCEOF
);
// Найдет атрибуты, которые implements MyInterface
Повторяемые атрибуты (IS_REPEATABLE)
По умолчанию атрибут можно применить к цели только один раз. Флаг IS_REPEATABLE снимает это ограничение:
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Middleware
{
public function __construct(
public readonly string $name,
public readonly int $priority = 0,
) {}
}
class ApiController
{
#[Middleware('auth')]
#[Middleware('throttle', priority: 10)]
#[Middleware('cors', priority: 20)]
#[Middleware('log')]
public function store(): Response
{
// ...
}
}
// Чтение всех middleware
$method = new ReflectionMethod(ApiController::class, 'store');
$middlewares = $method->getAttributes(Middleware::class);
foreach ($middlewares as $attr) {
$mw = $attr->newInstance();
echo "{$mw->name} (priority: {$mw->priority})\n";
}
// auth (priority: 0)
// throttle (priority: 10)
// cors (priority: 20)
// log (priority: 0)
Ловушка: Без
IS_REPEATABLEповторное использование атрибута вызоветErrorприnewInstance(). Не при объявлении, а при чтении!
Вложенные атрибуты
Аргументы атрибутов должны быть константными выражениями. Начиная с PHP 8.1, можно передавать new в аргументах:
<?php
declare(strict_types=1);
use Attribute;
#[Attribute]
final class Validator
{
public function __construct(
public readonly string $field,
public readonly Rule $rule,
) {}
}
#[Attribute]
final class Rule
{
public function __construct(
public readonly string $type,
public readonly int|string|null $value = null,
) {}
}
// PHP 8.1+: вложенные атрибуты через new
#[Validator(
field: 'email',
rule: new Rule(type: 'email')
)]
#[Validator(
field: 'age',
rule: new Rule(type: 'min', value: 18)
)]
class RegistrationForm {}
Допустимые типы аргументов:
- Скалярные:
string,int,float,bool - Массивы скаляров:
['GET', 'POST'] - Константы классов и enums:
Status::Active - Выражения
new(PHP 8.1+) null
Недопустимые аргументы:
- Переменные:
$path-- нельзя - Вызовы функций:
strtolower('GET')-- нельзя - Замыкания:
fn() => 'x'-- нельзя
Атрибуты в аргументах конструктора
Constructor property promotion работает вместе с атрибутами:
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Column
{
public function __construct(
public readonly string $name,
) {}
}
#[Attribute(Attribute::TARGET_PARAMETER)]
final class Autowire
{
public function __construct(
public readonly ?string $service = null,
) {}
}
final class User
{
public function __construct(
#[Column('user_email')]
public readonly string $email,
#[Column('user_name')]
public readonly string $name,
) {}
}
// Атрибут автоматически привязан к СВОЙСТВУ (через promotion)
$ref = new ReflectionClass(User::class);
$prop = $ref->getProperty('email');
$attrs = $prop->getAttributes(Column::class);
echo $attrs[0]->newInstance()->name; // 'user_email'
// Атрибут также доступен на параметре конструктора
$ctor = $ref->getConstructor();
$param = $ctor->getParameters()[0];
$attrs = $param->getAttributes(Column::class);
echo $attrs[0]->newInstance()->name; // 'user_email'
Важно: При constructor property promotion атрибут привязывается и к свойству, и к параметру. Если ваш атрибут нацелен на
TARGET_PROPERTY, он корректно читается черезReflectionProperty. Если наTARGET_PARAMETER-- черезReflectionParameter.
Практический пример: мини-роутер
<?php
declare(strict_types=1);
use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
final class Route
{
public function __construct(
public readonly string $path,
public readonly string $method = 'GET',
) {}
}
class UserController
{
#[Route('/users', method: 'GET')]
public function index(): string { return 'List users'; }
#[Route('/users', method: 'POST')]
public function store(): string { return 'Create user'; }
#[Route('/users/{id}', method: 'GET')]
#[Route('/users/{id}/profile', method: 'GET')]
public function show(int $id): string { return "User {$id}"; }
}
// Сбор маршрутов через Reflection
function collectRoutes(string $controllerClass): array
{
$routes = [];
$ref = new ReflectionClass($controllerClass);
foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attrs = $method->getAttributes(Route::class);
foreach ($attrs as $attr) {
$route = $attr->newInstance();
$routes[] = [
'path' => $route->path,
'method' => $route->method,
'handler' => [$controllerClass, $method->getName()],
];
}
}
return $routes;
}
$routes = collectRoutes(UserController::class);
// [
// ['path' => '/users', 'method' => 'GET', 'handler' => [...]],
// ['path' => '/users', 'method' => 'POST', 'handler' => [...]],
// ['path' => '/users/{id}', 'method' => 'GET', 'handler' => [...]],
// ['path' => '/users/{id}/profile', 'method' => 'GET', 'handler' => [...]],
// ]