Дата обновления перевода: 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.