PHP: атрибуты vs аннотации: оптимизируем метадату Doctrine
Одним из нововведений PHP 8.0 являются атрибуты. Атрибуты содержат метадату для классов, полей, функций; которая доступна через Reflection API. Казалось бы, то же самое, что и аннотации, тогда зачем обращать внимание на эту фичу?
Все отличие в структурированности. Аннотации являются простой строкой phpDoc, а, следовательно, разработчику нужно использовать какой-то механизм для их обработки, чтобы извлечь нужную информацию.
Появляется необходимость парсинга в рантайме. Насколько это плохо? Рассмотрим, как это влияет на производительность на примере Doctrine.
Doctrine, как любая ORM, широко использует метадату для своей работы. В частности — маппинги бизнес-сущностей на таблицы в БД. Есть разные реализации способа хранения метадаты:
Драйвер XML
Драйвер YAML
Статический PHP драйвер
PHP драйвер
Драйвер аннотаций
Драйвер атрибутов
В тестах рассмотрим аннотации и атрибуты. Дополнительно посмотрим на статический и обычный PHP драйвер, чтобы сравнить, насколько предыдущие способы уступают ручной инициализации метадаты. Не забудем взглянуть на драйвера аннотаций с кэшом на основе APCu и Redis с целью понять, решают ли они возможную проблему производительности.
Пример использования драйвера аннотаций
Пример использования статического PHP драйвера
setPrimaryTable([
'table' => 'item_price',
]);
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
$metadata->mapField([
'fieldName' => 'id',
'type' => 'integer',
'id' => true,
]);
$metadata->mapManyToOne([
'fieldName' => 'item',
'joinColumns' => [
[
'name' => 'item_id',
'referencedColumnName' => 'id',
],
],
'inversedBy' => 'prices',
'targetEntity' => Item::class,
]);
$metadata->mapField([
'fieldName' => 'amount',
'type' => 'string',
]);
$metadata->mapField([
'fieldName' => 'currency',
'type' => 'string',
]);
}
private int $id;
private Item $item;
private string $amount;
private string $currency;
}
В качестве теста я 10 минут 1 активным пользователем выполнял запросы на получение метадаты всех классов. Использовалась связка Nginx + PHP-fpm с включенным opcache. Исходники доступны здесь.
Драйвер | Всего запросов | Медиана | Минимум | 95-перцентиль |
AnnotationDriver | 35044 | 16.81ms | 13.8ms | 19.16ms |
AttributeDriver | 106016 | 5.5ms | 4.02ms | 6.64ms |
StaticPHPDriver | 114089 | 5.08ms | 3.63ms | 6.23ms |
PHPDriver | 98042 | 5.48ms | 3.76ms | 7.47ms |
Redis Cache | 36057 | 16.2ms | 11.67ms | 19.94ms |
APCu Cache | 38943 | 15.06ms | 10.83ms | 18.51ms |
Атрибуты показали неплохую производительность на уровне драйверов, использующих PHP-код. Разницу между AttributeDriver, StaticPHPDriver и PHPDriver, думаю, можно считать погрешностью. Главное, что можно увидеть по результатам тестирования — аннотации в 3 раза медленнее! Интересен так же тот факт, что кэширование не всегда может помочь ускорить приложение — в Doctrine метадата для каждого класса кэшируется отдельной записью. Это приводит к тому, что ее выгрузка и десериализация для каждого класса в отдельности выходит ненамного быстрее.
Если вы ищите способ оптимизировать ваше legacy приложение, и все запросы к БД уже давно проверены, возможно аннотации контроллеров и сущностей ORM — ваша следующая цель.
Тут можно столкнуться с проблемой: legacy — это когда куча кода, а теперь его нужно весь переписывать? Эту проблему можно решить с помощью еще одного варианта, который вы могли видеть в результатах бенчмарка — кодогенерации:
Драйвер | Всего запросов | Медиана | Минимум | 95-перцентиль |
Generated Static | 102124 | 5.59ms | 3.7ms | 7.38ms |
Этот вариант позволяет продолжать использовать аннотации, не переписывая весь проект, при этом получая бонусы производительности — ведь аннотации генерируются в код, а код попадает в opcache!
Резюмируя:
Стоит ли использовать в новых приложениях атрибуты вместо аннотаций везде, где это представляется возможным? Однозначно да.
Нужно ли переписывать аннотации в legacy проекте на атрибуты? Зависит от нефункциональных требований, возможно вам поможет кодогенерация. Например, подобный скрипт мы используем у себя на продакшене. (Еще рабочим вариантом может оказаться автоматический рефакторинг средствами rector)
Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.