События Doctrine

Дата обновления перевода 2023-01-18

События 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:

  • Attributes
  • YAML
  • XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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();
    }
}

Note

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

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

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

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/EventListener/SearchIndexer.php
namespace App\EventListener;

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

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

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

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

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

  • YAML
  • XML
  • PHP
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'

Tip

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

Tip

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/EventListener/UserChangedNotifier.php
namespace App\EventListener;

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

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

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

  • YAML
  • XML
  • 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
# 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'

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

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

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 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
  • XML
  • PHP
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'

Tip

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