Как стать автором
Обновить

PHP: атрибуты vs аннотации: оптимизируем метадату Doctrine

Время на прочтение4 мин
Количество просмотров10K
<?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-перцентиль

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)

Надеюсь, статья была полезна и познакомила вас с ещё одним преимуществом атрибутов перед аннотациями.

Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+16
Комментарии5

Публикации

Истории

Работа

PHP программист
87 вакансий

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань