Дата оновлення перекладу 2022-05-12

Події Doctrine

Doctrine, набір PHP бібліотек, що використовується в Symfony для роботи з базами даних, надає легковажну систему подій для оновлення сутностей під час виконання додатку. Ці події називаються подіями життєвого циклу та надають можливість виконувати задачі на кшталт “оновити властивість createdAt автоматично прямо перед збереженням сутності даного типу”.

Doctrine оголошує події до/після виконання найчастіших операцій з сутністю (наприклад, prePersist/postPersist, preUpdate/postUpdate), а також при інших частих задачах (наприклад, loadClassMetadata, onClear).

Є декілька способів слухати ці події Doctrine:

  • Зворотні виклики життєвого циклу, вони визначаються як методи в класах сутностей, і викликаються, коли спрацьовують події;
  • Слухачі та підписники життєвого циклу, це класи з методами зворотнього виклику для одного або декілької подій, і викликаються для всіх сутностей;
  • Слухачі сутностей, вони схожі на слухачів життєвого циклу, але вони викликаються лише для сутностей певного класу.

У кожного з них є переваги та недоліки:

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

Ця стаття пояснює лише основи того, як події Doctrine використовуються у додатках Symfony. Прочитайте офіційну документацію про події Doctrine, щоб дізнатися про них детальніше.

See also

Ця стаття охоплює слухачів та підписників для Doctrine ORM. Якщо ви використовуєте ODM для MongoDB, прочитайте документацію DoctrineMongoDBBundle.

Зворотні виклики життєвого циклу Doctrine

Зворотні виклики життєвого циклю визначаються як методи сутності, яку ви хочете змінити. Наприклад, припустимо, що ви хочете встановити колонку з датою createdAt у поточну дату, тільки тоді, коли до сутності буде застосовано збереження в перший раз (тобто, додавання нового запису). Щоб зробити це, визначте зворотній виклик для події 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

Деякі зворотні виклики життєвого циклу отримують аргумент, який надає доступ до корисної інформації, такої як поточний менеджер сутностей (наприклад, зворотній виклик preUpdate отримує аргумент PreUpdateEventArgs $event).

Слухачі життєвого циклу Doctrine

Слухачі життєвого циклу визначаються як PHP-класи, які слухають одну подію Doctrine для всіх сутностей додатку. Наприклад, припустимо, що ви хочете оновлювати пошуковий індекс, кожний раз, коли додається нова сутність в базу даних. Щоб зробити це, оголосіть слухача для події 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 у додатку 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 лише коли пов’язана подія Doctrine дійсно оголошується; а підписники Doctrine завжди завантажуються (та ініціалізуються) Symfony, що робить їх менш продуктивними.

Tip

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

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

Слухачі сутностей Doctrine

Слухачі сутностей визначаються як PHP-класи, які слухають одну подію Doctrine для одного класу сутності. Наприклад, припустимо, що ви хочете відправити декілька повідомлень, коли сутність User змінюється в базі даних. Для цього визначте слухача для події 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 у додатку 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

Підписники життєвого циклу визначаються як PHP-класи, які реалізують інтерфейс Doctrine\Common\EventSubscriber і які слухають одну або декілька подій Doctrine на всі сутності додатку. Наприклад, припустимо, що ви хочете вести логи всієї активності бази даних. Для цього визначте підписаника для подій 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 кожний раз при запуску додатку; а слухачі Doctrine завантажуються лише тоді, коли пов’язана подія дійсно спрацьовує, тому вони менше впливають на продуктивність.

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