Дата обновления перевода: 2022-01-09

События Doctrine

Doctrine, набор PHP библиотек используемый в Symfony для работы с базами данных, предоставляет легковесную систему событий для обновления сущностей во время выполнения приложения. Эти события называются lifecycle events и дают возможность выполнять задачи вроде “обновить свойство createdAt автоматически прямо перед persist сущности данного типа”.

Doctrine запускает события до/после выполнения наиболее частых операций с сущностью (например, prePersist/postPersist, preUpdate/postUpdate) а также при других частых задачах (e.g. loadClassMetadata, onClear).

Есть несколько способов слушать эти события Doctrine:

  • Обратные вызовы жизненного цикла, они определяются как методы в классах сущностей и вызываются, когда срабатывают события;
  • Слушатели и подписчики жизненного цикла, это классы с callback методами для одного или нескольких событий и вызываются для всех сущностей;
  • Слушатели сущностей, они похожи на lifecycle listeners, но они вызываются только для сущностей определённого класса.

У каждого из них есть недостатки и преимущества:

  • У обратных вызовов лучше производительность, потому что они применяются только к сущностям одного класса, но вы не можете переиспользовать логику в разных классах и они не имеют доступа к сервисам Symfony;
  • Слушатели и подписчики жизненного цикла могут переиспользовать логику в разных сущностях и имеют доступ к сервисам Symonfy, но их производительность хуже, так как они вызываются для всех сущностей;
  • Слущатели сущностей имеют те же преимущества, что и lifecycle listeners и у них лучше производительность, потому что они применяются к одному классу сущности.

Эта статья объясняет только основы того, как события Doctrine используются в приложениях Symfony. Прочитайте официальную документацию о событиях Doctrine чтобы узнать о них детальнее.

See also

Эта статья покрывает listeners и subscribers для Doctrine ORM. Если вы используете ODM для MongoDB, прочитайте документацию к DoctrineMongoDBBundle.

Обратные вызовы жизненного цикла Doctrine

Lifecycle callbacks определяются как методы внутри сущности, которую вы хотите изменить. Например, предположим, что вы хотите установить колонку с датой createdAt в текущую дату, то только когда к сущности применяется первый раз persist (то есть, добавление новой записи). Чтобы сделать это, определите callback для события Doctrine prePersist:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // src/Entity/Product.php
    namespace App\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    
    // При использовании аннотаций не забудьте добавить @ORM\HasLifecycleCallbacks()
    // к классу сущности там, где вы определяете обратный вызов
    
    /**
     * @ORM\Entity()
     * @ORM\HasLifecycleCallbacks()
     */
    class Product
    {
        // ...
    
        /**
         * @ORM\PrePersist
         */
        public function setCreatedAtValue(): void
        {
            $this->createdAt = new \DateTimeImmutable();
        }
    }
    
  • YAML
    1
    2
    3
    4
    5
    6
    # config/doctrine/Product.orm.yml
    App\Entity\Product:
        type: entity
        # ...
        lifecycleCallbacks:
            prePersist: ['setCreatedAtValue']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/doctrine/Product.orm.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
            https://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="App\Entity\Product">
            <!-- ... -->
            <lifecycle-callbacks>
                <lifecycle-callback type="prePersist" method="setCreatedAtValue"/>
            </lifecycle-callbacks>
        </entity>
    </doctrine-mapping>
    

Note

Некоторые lifecycle callbacks получают аргумент, который предоставляет доступ к полезной информации, такой как текущий entity manager (например,``preUpdate`` callback получает аргумент PreUpdateEventArgs $event).

Слушатели жизненного цикла Doctrine

Слушатели жизненного цикла определяются как PHP-классы, которые слушают одно событие Doctrine для всех entities приложения. Например, предположим, что вы хотите обновить поисковый индекс, каждый раз, когда новая entity добавляется (persist) в DB. Чтобы сделать это, объявите listener для события Doctrine postPersist:

// src/EventListener/SearchIndexer.php
namespace App\EventListener;

use App\Entity\Product;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;

class SearchIndexer
{
    // методы слушателя получают аргумент, который дает вам доступ и к
    // сущности объекта события, и к сущности самого менеджера
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();

        // если этот слушатель применяется только к определенным типам сущностей,
        // добавьте код для проверки сущности как можно раньше
        if (!$entity instanceof Product) {
            return;
        }

        $entityManager = $args->getObjectManager();
        // ... сделать что-то с сущностью Product
    }
}

Следующий шаг - включить Doctrine listener в приложение Symfony создав новый сервис для него и добавить тег doctrine.event_listener:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\SearchIndexer:
            tags:
                -
                    name: 'doctrine.event_listener'
                    # это единственная обязательная опция тега слушателя жизненного цикла
                    event: 'postPersist'
    
                    # слушатели могут определять свою приоритетность в случае, если несколько слушателей
                    # связаны с одним событием (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
                    priority: 500
    
                    # вы также можете ограничить слушателей по определенному соединению Doctrine
                    connection: 'default'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- config/services.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <!--
                * 'event' это единственная обязательная опция тега слушателя жизненного цикла
                * 'priority': используется, когда несколько слушателей связаны с одним событием
                *             (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
                * 'connection': ограничивает слушателя по определенному соединению Doctrine
            -->
            <service id="App\EventListener\SearchIndexer">
                <tag name="doctrine.event_listener"
                    event="postPersist"
                    priority="500"
                    connection="default"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // config/services.php
    use App\EventListener\SearchIndexer;
    
    // слушатели по умолчанию применяются ко всем соединениям Doctrine
    $container->autowire(SearchIndexer::class)
        ->addTag('doctrine.event_listener', [
            // это единственная обязательная опция тега слушателя жизненного цикла
            'event' => 'postPersist',
    
            // слушатели могут определять свою приоритетность в случае, если несколько слушателей
            // связаны с одним событием (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
            'priority' => 500,
    
            # вы также можете ограничить слушателей по определенному соединению Doctrine
                'connection' => 'default',
            ])
        ;
    };
    

Tip

Symfony загружает (и инициализирует) Doctrine listeners только, когда связанное событие Doctrine действительно вызывается; а Doctrine subscribers всегда загружаются (и инициализируются) Symfony, делая их менее производительными.

Tip

Значение опции connection также может быть параметром конфигурации.

New in version 5.4: Функция, позволяющая использовать параметры конфигурации в connection была представлена в Symfony 5.4.

Слушатели сущностей Doctrine

Слушатели сущностей определяются как PHP-классы, которые слушают одно событие Doctrine для однго класса entity. Например, предположим, что вы хотите отправить несколько уведомлений, когда entity User изменяется в DB. Для этого определите listener для Doctrine события postUpdate:

// src/EventListener/UserChangedNotifier.php
namespace App\EventListener;

use App\Entity\User;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;

class UserChangedNotifier
{
    // методы слушателя сущности получают два аргумента:
    // экземпляр сущности и событие жизненного цикла
    public function postUpdate(User $user, LifecycleEventArgs $event)
    {
        // ... сделайте что-то, чтобы уведомить об изменениях
    }
}

Следующий шаг - включить Doctrine listener в приложении Symfony создав новый сервис и добавить тег doctrine.orm.entity_listener:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\UserChangedNotifier:
            tags:
                -
                    # это опции, необходимые для определения слушателя сущности
                    name: 'doctrine.orm.entity_listener'
                    event: 'postUpdate'
                    entity: 'App\Entity\User'
    
                    # это другие опции, которые вы можете определить при необходимости
    
                    # установите опцию 'lazy' как TRUE, чтобы инстанциировать слушателей только при использовании
                    # lazy: true
    
                    # установите опцию 'entity_manager', если слушатель не ассоциирован с менеджером по умолчанию
                    # entity_manager: 'custom'
    
                    # по умолчанию, Symfony ищет метод, вызываемый после события (например, postUpdate())
                    # если он не существует, она пытается выполнить метод '__invoke()', но вы можете
                    # сконфигурировать пользовательское имя метода с опцией 'method'
                    # method: 'checkUserChanges'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <service id="App\EventListener\UserChangedNotifier">
                <!--
                    * это опции, необходимые для определения слушателя сущности:
                    *   * name
                    *   * event
                    *   * entity
                    *
                    * это другие опции, которые вы можете определить при необходимости:
                    *   * lazy: если TRUE, слушатели инстанциируются только при использовании
                    *   * entity_manager: определите ее, если слушатель не ассоциирован с менеджером по умолчанию
                    *   * method: по умолчанию, Symfony ищет метод, вызываемый после события (например, postUpdate())
                    *           если он не существует, она пытается выполнить метод '__invoke()', но вы можете
                    *           сконфигурировать пользовательское имя метода с опцией 'method'
                -->
                <tag name="doctrine.orm.entity_listener"
                    event="postUpdate"
                    entity="App\Entity\User"
                    lazy="true"
                    entity_manager="custom"
                    method="checkUserChanges"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\Entity\User;
    use App\EventListener\UserChangedNotifier;
    
    return static function (ContainerConfigurator $container) {
        $services = $configurator->services();
    
        $services->set(UserChangedNotifier::class)
            ->tag('doctrine.orm.entity_listener', [
                // это опции, необходимые для определения слушателя сущности:
                'event' => 'postUpdate',
                'entity' => User::class,
    
                // это другие опции, которые вы можете определить при необходимости:
    
                // установите опцию 'lazy' как TRUE, чтобы инстанциировать слушателей только при использовании
                // 'lazy' => true,
    
                // установите опцию 'entity_manager', если слушатель не ассоциирован с менеджером по умолчанию
                // 'entity_manager' => 'custom',
    
                // по умолчанию, Symfony ищет метод, вызываемый после события (например, postUpdate())
                // если он не существует, она пытается выполнить метод '__invoke()', но вы можете
                // сконфигурировать пользовательское имя метода с опцией 'method'
                // 'method' => 'checkUserChanges',
            ])
        ;
    };
    

Подписчики жизненного цикла Doctrine

Lifecycle subscribers определяются как PHP-классы которые реализуют интерфейс Doctrine\Common\EventSubscriber и которые слушают один или несколько событий Doctrine на все entity приложения. Например, предположим вы хотите логировать всю активность DB. Для этого определите subscriber для событий Doctrine postPersist, postRemove и postUpdate:

// src/EventListener/DatabaseActivitySubscriber.php
namespace App\EventListener;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;

class DatabaseActivitySubscriber implements EventSubscriberInterface
{
    // этот метод может вернуть только имена событий; вы не можете определить
    // пользовательское имя метода для выполнения при запуске каждого события
    public function getSubscribedEvents(): array
    {
        return [
            Events::postPersist,
            Events::postRemove,
            Events::postUpdate,
        ];
    }

    // методы обратного вызова должны быть вызваны точно так же, как и события, которые они слушают;
    // они получают аргумент типа LifecycleEventArgs, который дает вам доступ
    // и к объекту сущности события, и к самому менеджеру сущности
    public function postPersist(LifecycleEventArgs $args): void
    {
        $this->logActivity('persist', $args);
    }

    public function postRemove(LifecycleEventArgs $args): void
    {
        $this->logActivity('remove', $args);
    }

    public function postUpdate(LifecycleEventArgs $args): void
    {
        $this->logActivity('update', $args);
    }

    private function logActivity(string $action, LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();

        // если этот подписчик применяется только к определенному типу сущностей,
        // добавьте код, чтобы проверить тип сущности как можно раньше
        if (!$entity instanceof Product) {
            return;
        }

        // ... получите информацию о сущности и запишите ее каким-либо образом
    }
}

Если вы используете конфигурацию services.yaml по умолчанию и DoctrineBundle 2.1 (дата релиза 25 мая, 2020) или новее, этот пример уже будет работать! В других случаях, , создайте сервис для этого подписчика и добавьте к нему тег doctrine.event_subscriber.

Если вам нужно сконфигурировать некоторую опцию подписчика (например, его приоритетность или соединение Doctrine для использования), вы должны сделать это в конфигурации сервиса вручную:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\DatabaseActivitySubscriber:
            tags:
                - name: 'doctrine.event_subscriber'
    
                  # подписчики определяют приоритетность в случае асоциирования нескольких подписчиков или слушателей
                  # с одним и тем же событием (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
                  priority: 500
    
                  # вы также можете ограничить слушателей по определенному соединению Doctrine
                  connection: 'default'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/services.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <!--
                * 'priority': используется, когда несколько подписчиков или слушателей ассоциированы с одним и тем же событием
                *             (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
                * 'connection': ограничивает слушателя по определенному соединению Doctrine
            -->
            <service id="App\EventListener\DatabaseActivitySubscriber">
                <tag name="doctrine.event_subscriber" priority="500" connection="default"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\EventListener\DatabaseActivitySubscriber;
    
    return static function (ContainerConfigurator $container) {
        $services = $configurator->services();
    
        $services->set(DatabaseActivitySubscriber::class)
            ->tag('doctrine.event_subscriber'[
                // подписчики определяют приоритетность в случае асоциирования нескольких подписчиков или слушателей
                // с одним и тем же событием (приоритет по умолчанию = 0; чем больше цифра = тем раньше запускается слушатель)
                'priority' => 500,
    
                // вы также можете ограничить слушателей по определенному соединению Doctrine
                'connection' => 'default',
            ])
        ;
    };
    

New in version 5.3: Приоритетность подписчиков была представлена в Symfony 5.3.

Tip

Symfony загружает (и инстанциирует) Doctrine subscribers каждый раз при запуске приложения; а Doctrine listeners загружаются только тогда, когда связанное событие действительно срабатывает, поэтому они меньше влияют на производительность.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.