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

События Doctrine

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

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

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

  • Lifecycle callbacks, они определяются как методы в классах сущностей и вызываются, когда срабатывают события;
  • Lifecycle listeners and subscribers, это классы с callback методами для одного или нескольких событий и вызываются для всех сущностей;
  • Entity listeners, они похожи на lifecycle listeners, но они вызываются только для сущностей определённого класса.

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

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

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

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

Doctrine Lifecycle Callbacks

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
    // src/Entity/Product.php
    use Doctrine\ORM\Mapping as ORM;
    
    // When using annotations, don't forget to add @ORM\HasLifecycleCallbacks()
    // to the class of the entity where you define the callback
    
    /**
     * @ORM\Entity()
     * @ORM\HasLifecycleCallbacks()
     */
    class Product
    {
        // ...
    
        /**
         * @ORM\PrePersist
         */
        public function setCreatedAtValue()
        {
            $this->createdAt = new \DateTime();
        }
    }
    
  • 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 Lifecycle Listeners

Lifecycle listeners определяются как PHP-классы, которые слушают одно событие Doctrine для всех entities приложения. Например, предположим, что вы хотите обновить поисковый индекс, каждый раз, когда новая 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\Common\Persistence\Event\LifecycleEventArgs;

class SearchIndexer
{
    // the listener methods receive an argument which gives you access to
    // both the entity object of the event and the entity manager itself
    public function postPersist(LifecycleEventArgs $args)
    {
        $entity = $args->getObject();

        // if this listener only applies to certain entity types,
        // add some code to check the entity type as early as possible
        if (!$entity instanceof Product) {
            return;
        }

        $entityManager = $args->getObjectManager();
        // ... do something with the Product entity
    }
}

Следующий шаг - включить 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'
                    # this is the only required option for the lifecycle listener tag
                    event: 'postPersist'
    
                    # listeners can define their priority in case multiple listeners are associated
                    # to the same event (default priority = 0; higher numbers = listener is run earlier)
                    priority: 500
    
                    # you can also restrict listeners to a specific Doctrine connection
                    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' is the only required option that defines the lifecycle listener
                * 'priority': used when multiple listeners are associated to the same event
                *             (default priority = 0; higher numbers = listener is run earlier)
                * 'connection': restricts the listener to a specific Doctrine connection
            -->
            <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
    // config/services.php
    use App\EventListener\SearchIndexer;
    
    // listeners are applied by default to all Doctrine connections
    $container->autowire(SearchIndexer::class)
        ->addTag('doctrine.event_listener', [
            // this is the only required option for the lifecycle listener tag
            'event' => 'postPersist',
    
            // listeners can define their priority in case multiple listeners are associated
            // to the same event (default priority = 0; higher numbers = listener is run earlier)
            'priority' => 500,
    
            # you can also restrict listeners to a specific Doctrine connection
            'connection' => 'default',
        ])
    ;
    

Tip

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

Doctrine Entity Listeners

Entity listeners определяются как 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\Common\Persistence\Event\LifecycleEventArgs;

class UserChangedNotifier
{
    // the entity listener methods receive two arguments:
    // the entity instance and the lifecycle event
    public function postUpdate(User $user, LifecycleEventArgs $event)
    {
        // ... do something to notify the changes
    }
}

Следующий шаг - включить 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
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\UserChangedNotifier:
            tags:
                -
                    # these are the basic options that define the entity listener
                    name: 'doctrine.orm.entity_listener'
                    event: 'postUpdate'
                    entity: 'App\Entity\User'
    
                    # set the 'lazy' option to TRUE to only instantiate listeners when they are used
                    lazy: true
    
                    # you can also associate an entity listener to a specific entity manager
                    entity_manager: 'custom'
    
                    # by default, Symfony looks for a method called after the event (e.g. postUpdate())
                    # if it doesn't exist, it tries to execute the '__invoke()' method, but you can
                    # configure a custom method name with the 'method' option
                    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
    <!-- config/services.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <service id="App\EventListener\UserChangedNotifier">
                <!--
                    * 'event' and 'entity' are the basic options that define the entity listener
                    * 'lazy': if TRUE, listeners are only instantiated when they are used
                    * 'entity_manager': define it if the listener is not associated to the
                    *                   default entity manager
                    * 'method': by default, Symfony looks for a method called after the event (e.g. postUpdate())
                    *           if it doesn't exist, it tries to execute the '__invoke()' method, but
                    *           you can configure a custom method name with the 'method' option
                -->
                <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
    // config/services.php
    use App\Entity\User;
    use App\EventListener\UserChangedNotifier;
    
    $container->autowire(UserChangedNotifier::class)
        ->addTag('doctrine.orm.event_listener', [
            // these are the basic options that define the entity listener
            'event' => 'postUpdate',
            'entity' => User::class,
    
            // set the 'lazy' option to TRUE to only instantiate listeners when they are used
            'lazy' => true,
    
            // you can also associate an entity listener to a specific entity manager
            'entity_manager' => 'custom',
    
            // by default, Symfony looks for a method called after the event (e.g. postUpdate())
            // if it doesn't exist, it tries to execute the '__invoke()' method, but you can
            // configure a custom method name with the 'method' option
            'method' => 'checkUserChanges',
        ])
    ;
    

Doctrine Lifecycle Subscribers

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\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;

class DatabaseActivitySubscriber implements EventSubscriber
{
    // this method can only return the event names; you cannot define a
    // custom method name to execute when each event triggers
    public function getSubscribedEvents()
    {
        return [
            Events::postPersist,
            Events::postRemove,
            Events::postUpdate,
        ];
    }

    // callback methods must be called exactly like the events they listen to;
    // they receive an argument of type LifecycleEventArgs, which gives you access
    // to both the entity object of the event and the entity manager itself
    public function postPersist(LifecycleEventArgs $args)
    {
        $this->logActivity('persist', $args);
    }

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

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

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

        // if this subscriber only applies to certain entity types,
        // add some code to check the entity type as early as possible
        if (!$entity instanceof Product) {
            return;
        }

        // ... get the entity information and log it somehow
    }
}

Затем включите Doctrine subscriber в приложение Symfony создав новый сервис для него и добавив тег doctrine.event_subscriber:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\DatabaseActivitySubscriber:
            tags:
                - { name: 'doctrine.event_subscriber' }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- config/services.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <service id="App\EventListener\DatabaseActivitySubscriber">
                <tag name="doctrine.event_subscriber"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // config/services.php
    use App\EventListener\DatabaseActivitySubscriber;
    
    $container->autowire(DatabaseActivitySubscriber::class)
        ->addTag('doctrine.event_subscriber')
    ;
    

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/services.yaml
    services:
        # ...
    
        App\EventListener\DatabaseActivitySubscriber:
            tags:
                - { name: 'doctrine.event_subscriber', connection: 'default' }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- config/services.xml -->
    <?xml version="1.0" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:doctrine="http://symfony.com/schema/dic/doctrine">
        <services>
            <!-- ... -->
    
            <service id="App\EventListener\DatabaseActivitySubscriber">
                <tag name="doctrine.event_subscriber" connection="default"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // config/services.php
    use App\EventListener\DatabaseActivitySubscriber;
    
    $container->autowire(DatabaseActivitySubscriber::class)
        ->addTag('doctrine.event_subscriber', ['connection' => 'default'])
    ;
    

Tip

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

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