<?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)

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

Комментарии (3)


  1. gruzoveek
    07.09.2022 08:45
    +3

    Думаю стоит переводить проекты с аннотаций на атрибуты. Сам для этого юзал https://github.com/rectorphp/rector


  1. northmule
    07.09.2022 10:08

    На совсем легасси, зачасту, используется PHPDriver, такой легасси с php 5.x до 8.x лучше наверное не переводить (если это вообщем возможно)


    1. aspirin4k Автор
      07.09.2022 13:54
      +1

      Если legacy времен php 5 и никакого дальнейшего развития от него не требуется (хотя если в принципе от приложения требуется только поддержка), то да, переводить особо смысла нет )

      В наше время уже php 7 считается legacy. В том время пользовались популярностью аннотации. Например, наше приложение стартовало в 2018 с php 7.2, обросло большим количеством кода, а требования с т.з. бизнеса продолжали расти (в том числе по нагрузке). Поэтому оптимизация метадаты оказалась полезным решением - ускорение запросов на 30%, если судить по метрикам. Правда, ускорили мы не за счет атрибутов, а кодогенерации, которую я в статье упомянул. Но думаю в конечном итоге перейдем на атрибуты, чтобы не поддерживать кастом )