<?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)
Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.
Комментарии (3)
northmule
07.09.2022 10:08На совсем легасси, зачасту, используется PHPDriver, такой легасси с php 5.x до 8.x лучше наверное не переводить (если это вообщем возможно)
aspirin4k Автор
07.09.2022 13:54+1Если legacy времен php 5 и никакого дальнейшего развития от него не требуется (хотя если в принципе от приложения требуется только поддержка), то да, переводить особо смысла нет )
В наше время уже php 7 считается legacy. В том время пользовались популярностью аннотации. Например, наше приложение стартовало в 2018 с php 7.2, обросло большим количеством кода, а требования с т.з. бизнеса продолжали расти (в том числе по нагрузке). Поэтому оптимизация метадаты оказалась полезным решением - ускорение запросов на 30%, если судить по метрикам. Правда, ускорили мы не за счет атрибутов, а кодогенерации, которую я в статье упомянул. Но думаю в конечном итоге перейдем на атрибуты, чтобы не поддерживать кастом )
gruzoveek
Думаю стоит переводить проекты с аннотаций на атрибуты. Сам для этого юзал https://github.com/rectorphp/rector