Что такое аннотации (атрибуты) в PHP?

Ответ

Аннотации в PHP (официально называемые атрибутами, attributes) — это форма метаданных, которая добавляется к классам, методам, свойствам или параметрам для предоставления дополнительной информации. Они были введены в PHP 8.0 и представляют собой структурированную альтернативу док-блокам с аннотациями.

Синтаксис и базовое использование:

// Объявление атрибута (класс атрибута)
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Route {
    public function __construct(
        public string $path,
        public array $methods = ['GET']
    ) {}
}

// Применение атрибута к классу и методу
#[Route('/api/users')]
class UserController {
    #[Route('/{id}', methods: ['GET'])]
    public function show(int $id): Response {
        // ...
    }
}

Как я использую атрибуты в реальных проектах:

  1. Маршрутизация в фреймворках:

    // Symfony с атрибутами
    #[Route('/api/products')]
    class ProductController {
        #[Route('/{id}', name: 'product_show', methods: ['GET'])]
        #[IsGranted('ROLE_USER')]
        public function show(Product $product): JsonResponse {
            return $this->json($product);
        }
    
        #[Route('', name: 'product_create', methods: ['POST'])]
        #[IsGranted('ROLE_ADMIN')]
        #[ValidateRequest]
        public function create(Request $request): JsonResponse {
            // ...
        }
    }
  2. Валидация данных:

    class UserDto {
        #[AssertNotBlank]
        #[AssertLength(min: 3, max: 50)]
        public string $username;
    
        #[AssertNotBlank]
        #[AssertEmail]
        public string $email;
    
        #[AssertChoice(['male', 'female', 'other'])]
        public string $gender;
    
        #[AssertRange(min: 18, max: 100)]
        public int $age;
    }
  3. Маппинг Doctrine ORM:

    #[Entity]
    #[Table(name: 'users')]
    class User {
        #[Id]
        #[GeneratedValue]
        #[Column(type: 'integer')]
        private ?int $id = null;
    
        #[Column(type: 'string', length: 180, unique: true)]
        private string $email;
    
        #[OneToMany(mappedBy: 'user', targetEntity: Order::class)]
        private Collection $orders;
    
        #[Column(type: 'datetime')]
        private DateTimeInterface $createdAt;
    
        public function __construct() {
            $this->createdAt = new DateTimeImmutable();
            $this->orders = new ArrayCollection();
        }
    }
  4. Кастомные атрибуты для логики приложения:

    // Атрибут для кэширования результатов метода
    #[Attribute(Attribute::TARGET_METHOD)]
    class CacheResult {
        public function __construct(
            public int $ttl = 3600,
            public ?string $key = null
        ) {}
    }
    
    // Обработчик атрибута
    class CacheResultHandler {
        public function handle(object $object, string $method, array $args): mixed {
            $reflection = new ReflectionMethod($object, $method);
            $attributes = $reflection->getAttributes(CacheResult::class);
    
            if (empty($attributes)) {
                return $object->$method(...$args);
            }
    
            $cacheAttr = $attributes[0]->newInstance();
            $cacheKey = $cacheAttr->key ?? $this->generateKey($object, $method, $args);
    
            if ($cached = Cache::get($cacheKey)) {
                return $cached;
            }
    
            $result = $object->$method(...$args);
            Cache::set($cacheKey, $result, $cacheAttr->ttl);
    
            return $result;
        }
    }

Преимущества перед док-блоками:

  • Выполняемый код: Атрибуты — это реальные классы PHP, которые можно инстанциировать и использовать
  • Проверка типов: IDE и статические анализаторы понимают типы параметров
  • Автодополнение: PhpStorm подсказывает доступные параметры атрибутов
  • Производительность: Не нужно парсить строки, как в случае с аннотациями в док-блоках
  • Структурированные данные: Параметры атрибутов типизированы и валидируются

Чтение атрибутов через Reflection API:

$reflectionClass = new ReflectionClass(UserController::class);
$attributes = $reflectionClass->getAttributes(Route::class);

if (!empty($attributes)) {
    $routeAttribute = $attributes[0]->newInstance();
    echo "Path: " . $routeAttribute->path; // /api/users
}

Атрибуты значительно улучшили опыт работы с метаданными в PHP, сделав код более типобезопасным и удобным для анализа инструментами разработки.

Ответ 18+ 🔞

Ну, вот смотри, объясню тебе про эти ваши атрибуты в PHP, или, как их ещё, анноции ёпта. Это ж, блядь, просто метаданные, которые ты к классам или методам цепляешь, чтобы пояснить, что тут к чему. Раньше в док-блоках писали, а теперь с восьмой версии — отдельная синтаксическая хуйня, прям в коде. Овердохуища удобства, честно.

Как это выглядит, блядь:

// Сначала объявляешь сам атрибут, это обычный класс, ёпта
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Route {
    public function __construct(
        public string $path,
        public array $methods = ['GET']
    ) {}
}

// А потом вешаешь его куда надо
#[Route('/api/users')]
class UserController {
    #[Route('/{id}', methods: ['GET'])]
    public function show(int $id): Response {
        // ...
    }
}

А вот где я их на практике применяю, чтоб ты понимал масштаб:

  1. Роутинг в фреймворках. Это вообще пиздец как удобно стало, не надо в отдельный конфиг лазить.

    // Взял, например, Symfony
    #[Route('/api/products')]
    class ProductController {
        #[Route('/{id}', name: 'product_show', methods: ['GET'])]
        #[IsGranted('ROLE_USER')] // И права проверяй тут же, ебать колотить
        public function show(Product $product): JsonResponse {
            return $this->json($product);
        }
    
        #[Route('', name: 'product_create', methods: ['POST'])]
        #[IsGranted('ROLE_ADMIN')]
        #[ValidateRequest] // И валидацию прикрутил, хитрая жопа
        public function create(Request $request): JsonResponse {
            // ...
        }
    }
  2. Валидация данных. Раньше в триста мест писать надо было, а теперь прям над полями класса.

    class UserDto {
        #[AssertNotBlank] // Не пустое, блядь!
        #[AssertLength(min: 3, max: 50)] // И чтоб не короткое и не длинное
        public string $username;
    
        #[AssertNotBlank]
        #[AssertEmail] // Чтоб почта настоящая, а не хуй с горы
        public string $email;
    
        #[AssertChoice(['male', 'female', 'other'])] // Выбирай из списка, ёпта
        public string $gender;
    
        #[AssertRange(min: 18, max: 100)] // От 18 до 100, ядрёна вошь, логично же
        public int $age;
    }
  3. Doctrine ORM маппинг. Тут вообще красота, вся схема БД прямо в ентити.

    #[Entity]
    #[Table(name: 'users')]
    class User {
        #[Id]
        #[GeneratedValue]
        #[Column(type: 'integer')]
        private ?int $id = null;
    
        #[Column(type: 'string', length: 180, unique: true)]
        private string $email;
    
        #[OneToMany(mappedBy: 'user', targetEntity: Order::class)]
        private Collection $orders;
    
        #[Column(type: 'datetime')]
        private DateTimeInterface $createdAt;
    
        public function __construct() {
            $this->createdAt = new DateTimeImmutable();
            $this->orders = new ArrayCollection();
        }
    }
  4. Свои кастомные атрибуты. Вот тут вообще полёт фантазии, делай что хочешь. Например, кеширование результатов метода.

    // Объявляешь атрибут для кеша
    #[Attribute(Attribute::TARGET_METHOD)]
    class CacheResult {
        public function __construct(
            public int $ttl = 3600,
            public ?string $key = null
        ) {}
    }
    
    // А потом пишешь хендлер, который это всё обрабатывает
    class CacheResultHandler {
        public function handle(object $object, string $method, array $args): mixed {
            $reflection = new ReflectionMethod($object, $method);
            $attributes = $reflection->getAttributes(CacheResult::class);
    
            // Если атрибута нет — просто выполняем метод
            if (empty($attributes)) {
                return $object->$method(...$args);
            }
    
            // Достаём параметры атрибута
            $cacheAttr = $attributes[0]->newInstance();
            $cacheKey = $cacheAttr->key ?? $this->generateKey($object, $method, $args);
    
            // Проверяем кеш
            if ($cached = Cache::get($cacheKey)) {
                return $cached;
            }
    
            // Если нет в кеше — выполняем и сохраняем
            $result = $object->$method(...$args);
            Cache::set($cacheKey, $result, $cacheAttr->ttl);
    
            return $result;
        }
    }

И чем это лучше старых док-блоков, спросишь ты? А вот чем, ёпта:

  • Это реальный код, блядь. Не строки для парсера, а классы, которые можно создать и использовать.
  • Проверка типов. IDE не будет орать, что ты строку туда, где массив нужен.
  • Автодополнение в том же PhpStorm. Начал писать #[Rout — он тебе уже Route подсказывает и его параметры.
  • Быстрее. Никакого парсинга строк — всё нативно.
  • Структурированно. Всё типизировано, никакого «а вдруг разработчик опечатался в названии параметра».

Как их через рефлексию читать, если вдруг понадобится:

$reflectionClass = new ReflectionClass(UserController::class);
$attributes = $reflectionClass->getAttributes(Route::class);

if (!empty($attributes)) {
    $routeAttribute = $attributes[0]->newInstance();
    echo "Path: " . $routeAttribute->path; // Выведет /api/users, всё просто
}

Короче, атрибуты — это мощный инструмент, который делает код чище и умнее. Сначала кажется, что лишняя морока, но потом понимаешь — жить без них уже не можешь, как без этого всего.