<?php #[ORM\Entity, ORM\Table(name: 'item_price')] class ItemPrice { #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue] private int $id; #[ ORM\ManyToOne(targetEntity: Item::class, inversedBy: 'prices'), ORM\JoinColumn(name: 'item_id', referencedColumnName: 'id') ] private Item $item; #[ORM\Column(type: 'string')] private string $amount; #[ORM\Column(type: 'string')] private string $currency; }
Одним из нововведений PHP 8.0 являются атрибуты. Атрибуты содержат метадату для классов, полей, функций; которая доступна через Reflection API. Казалось бы, то же самое, что и аннотации, тогда зачем обращать внимание на эту фичу?
Все отличие в структурированности. Аннотации являются простой строкой phpDoc, а, следовательно, разработчику нужно использовать какой-то механизм для их обработки, чтобы извлечь нужную информацию.
Появляется необходимость парсинга в рантайме. Насколько это плохо? Рассмотрим, как это влияет на производительность на примере Doctrine.
Doctrine, как любая ORM, широко использует метадату для своей работы. В частности - маппинги бизнес-сущностей на таблицы в БД. Есть разные реализации способа хранения метадаты:
Драйвер XML
Драйвер YAML
Статический PHP драйвер
PHP драйвер
Драйвер аннотаций
Драйвер атрибутов
В тестах рассмотрим аннотации и атрибуты. Дополнительно посмотрим на статический и обычный PHP драйвер, чтобы сравнить, насколько предыдущие способы уступают ручной инициализации метадаты. Не забудем взглянуть на драйвера аннотаций с кэшом на основе APCu и Redis с целью понять, решают ли они возможную проблему производительности.
Пример использования драйвера аннотаций
<?php /** * @ORM\Entity * @ORM\Table(name="item_price") */ class ItemPrice { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue */ private int $id; /** * @ORM\ManyToOne(targetEntity="Item", inversedBy="prices") * @ORM\JoinColumn(name="item_id", referencedColumnName="id") */ private Item $item; /** * @ORM\Column(type="string") */ private string $amount; /** * @ORM\Column(type="string") */ private string $currency; }
Пример использования статического PHP драйвера
<?php class ItemPrice { public static function loadMetadata(ClassMetadata $metadata): void { $metadata->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-перцентиль |
35044 | 16.81ms | 13.8ms | 19.16ms | |
106016 | 5.5ms | 4.02ms | 6.64ms | |
114089 | 5.08ms | 3.63ms | 6.23ms | |
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-перцентиль |
102124 | 5.59ms | 3.7ms | 7.38ms |
Этот вариант позволяет продолжать использовать аннотации, не переписывая весь проект, при этом получая бонусы производительности - ведь аннотации генерируются в код, а код попадает в opcache!
Резюмируя:
Стоит ли использовать в новых приложениях атрибуты вместо аннотаций везде, где это представляется возможным? Однозначно да.
Нужно ли переписывать аннотации в legacy проекте на атрибуты? Зависит от нефункциональных требований, возможно вам поможет кодогенерация. Например, подобный скрипт мы используем у себя на продакшене. (Еще рабочим вариантом может оказаться автоматический рефакторинг средствами rector)
Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.
