Атрибути

Метапрограмування без DocBlock-хаків

  • Додано у версії: PHP 8.0
  • Що дає: нативний, типобезпечний механізм додавання метаданих до класів, методів, властивостей та параметрів — без необхідності при їх читанні парсити DocBlock-и та встановлювати сторонні бібліотеки.

Трохи особистого

До PHP 8.0 в мене був один технічний “біль”, про який знають тільки ті, хто коли-небудь пробував будувати API-документацію чи SDK на основі читання коду.

Скажу лише одне: парсинг DocBlock-ів — це дуже невдячна робота.

Атрибути стали тією фічею, після якої мої бібліотеки буквально отримали "вдих свіжого повітря".

Код став чистішим, простішим, а метадані — нарешті передбачуваними.

Навіщо вони взагалі потрібні?

До PHP 8 метадані доводилося писати так:

/**
 * @Route("/users")
 * @SomeFlag(true)
 */
class UserController {}

Це був просто коментар, який не розбирається стандартними засобами PHP такими як рефлексія. Для аналізу потрібна ще одна бібліотека-парсер, яка мала вгадати, що розробник мав на увазі.

Атрибути роблять метакоментарі типізованими, тепер вони обʼєктні:

#[Route('/users')]
#[SomeFlag(true)]
class UserController {}

Це буквально декларація екземплярів класів Route та SomeFlag, і IDE точно знає, що всередині. Це дає:

  • нативне використання;
  • розуміння які параметри треба передати;
  • легке читання коду;
  • легке використання цих даних.

Приклади застосування

1. Doctrine

#[ORM\Entity]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    public int $id;

    #[ORM\Column(length: 255)]
    public string $name;
}

2. Symfony Routing

#[Route('/orders/{id}', methods: ['GET'])]
public function show(int $id) { ... }

3. Валідація

class RegistrationDTO
{
    #[Assert\Email]
    #[Assert\NotBlank]
    public string $email;

    #[Assert\Length(min: 8)]
    public string $password;
}

4. Кастомні атрибути

#[API\Method('user.create')]
class CreateUserRequest
{
    #[API\Param]
    public string $email;

    #[API\Param]
    public string $password;
}

А що можна робити з атрибутами?

Майже все, що роблять Java/Spring- або C#-атрибути:

  • позначати класи й методи;
  • додавати метадані;
  • створювати власні декларативні правила;
  • будувати роутинг;
  • маркувати серіалізацію/десеріалізацію;
  • автоматизувати DI/автовайринг;
  • вказувати, як працює кешування чи логування;
  • створювати декларативні контракти для DTO.

Фактично — інструмент, щоб писати менше імперативного коду та більше декларативного.

Як створити власний атрибут

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Loggable
{
    public function __construct(
        public string $message = 'default',
        public bool $enabled = true,
    ) {}
}

Використання:

#[Loggable(message: 'Auth attempt')]
class AuthService {}

Як прочитати атрибут через Reflection

$ref = new ReflectionClass(AuthService::class);
$attrs = $ref->getAttributes(Loggable::class);

$instance = $attrs[0]->newInstance();
echo $instance->message;

Особливо важливо

  • атрибути — це повноцінний об’єкт, а не просто рядок в коментарі;
  • вони мають простір імен;
  • можна типізувати параметри;
  • можна використовувати передові конструкції (enum, readonly, VO);
  • ідеально комбінуються з DI-контейнерами.

Висновок

Атрибути — це одна з тих фіч, після яких старий PHP здається архаїчним. Типобезпека, декларативність, прозора робота через Reflection, відсутність DocBlock-магії — це все робить їх інструментом, який я обожнюю використовувати у власних бібліотеках. Це настільки природно, що важко повірити, що колись ми писали метадані в коментарях.

← Попередня: readonly + accessors · Уся серія


Вперше опубліковано в Telegram-каналі «Створений щоб помирати» 09.12.2025.