<?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)
Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.