Doctrine ORM:事件监听器与订阅者,数据持久化的幕后推手
大家好,今天我们来深入探讨 Doctrine ORM 中两个强大的组件:事件监听器(Listener)和事件订阅者(Subscriber)。它们允许我们在实体持久化过程的关键时刻插入自定义逻辑,实现诸如数据验证、审计日志记录、缓存失效等功能,而无需修改实体本身的代码。
Doctrine ORM 的事件机制
在深入探讨 Listener 和 Subscriber 之前,我们需要了解 Doctrine ORM 的事件机制。 Doctrine ORM 的核心操作(如 persist, merge, remove, flush)会触发一系列事件。这些事件允许我们介入到数据持久化的生命周期中。
常见事件包括:
| 事件名称 | 触发时机 |
|---|---|
prePersist |
在实体 persist() 操作被调用,但实体尚未被插入到数据库之前。 |
postPersist |
在实体被插入到数据库之后。 |
preUpdate |
在实体 flush() 操作期间,如果 Doctrine 发现实体已被修改,但在更新数据库之前。 |
postUpdate |
在实体被更新到数据库之后。 |
preRemove |
在实体 remove() 操作被调用,但实体尚未从数据库中删除之前。 |
postRemove |
在实体从数据库中删除之后。 |
preLoad |
在实体从数据库加载到内存之后,但在任何属性被填充之前。 |
postLoad |
在实体完全从数据库加载到内存之后。 |
preFlush |
在 EntityManager::flush() 操作开始时,但在任何更改被计算或同步到数据库之前。 |
onFlush |
在 EntityManager::flush() 操作期间,在 Doctrine 计算并计划对数据库所做的所有更改之后,但在任何更改被实际同步到数据库之前。这是一个非常重要的事件,可以用来实现复杂的业务逻辑。 |
postFlush |
在 EntityManager::flush() 操作完成并且所有更改都已同步到数据库之后。 |
onClear |
在 EntityManager::clear() 操作期间被触发。 |
onClassMetadataNotFound |
当 Doctrine 尝试加载一个实体的元数据,但找不到时触发。 |
事件监听器 (Listener)
事件监听器是 PHP 类,它包含一个或多个方法,这些方法订阅了 Doctrine ORM 的特定事件。当事件发生时,Doctrine 会调用相应的监听器方法。
配置事件监听器
事件监听器需要在 Doctrine 的配置中进行注册。这通常在 doctrine.yaml 或类似的配置文件中完成。
# config/packages/doctrine.yaml
doctrine:
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'AppEntity'
alias: App
listeners:
entity_listener:
class: AppEventListenerEntityListener
events: [ prePersist, postPersist, preUpdate, postUpdate, preRemove, postRemove ]
在这个配置中,我们注册了一个名为 entity_listener 的监听器,它监听了 prePersist, postPersist, preUpdate, postUpdate, preRemove, postRemove 这六个事件。 class 属性指定了监听器类的完整命名空间。events 属性指定了监听器要监听的事件列表。
创建事件监听器类
接下来,我们需要创建 AppEventListenerEntityListener 类。
<?php
namespace AppEventListener;
use DoctrineORMEventPrePersistEventArgs;
use DoctrineORMEventPostPersistEventArgs;
use DoctrineORMEventPreUpdateEventArgs;
use DoctrineORMEventPostUpdateEventArgs;
use DoctrineORMEventPreRemoveEventArgs;
use DoctrineORMEventPostRemoveEventArgs;
class EntityListener
{
public function prePersist(PrePersistEventArgs $args): void
{
$entity = $args->getObject();
// 在实体持久化之前执行的逻辑
if (method_exists($entity, 'setCreatedAt')) {
$entity->setCreatedAt(new DateTimeImmutable());
}
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt(new DateTimeImmutable());
}
}
public function postPersist(PostPersistEventArgs $args): void
{
$entity = $args->getObject();
// 在实体持久化之后执行的逻辑
// 例如,发送通知邮件
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$entity = $args->getObject();
// 在实体更新之前执行的逻辑
if (method_exists($entity, 'setUpdatedAt')) {
$entity->setUpdatedAt(new DateTimeImmutable());
}
}
public function postUpdate(PostUpdateEventArgs $args): void
{
$entity = $args->getObject();
// 在实体更新之后执行的逻辑
}
public function preRemove(PreRemoveEventArgs $args): void
{
$entity = $args->getObject();
// 在实体删除之前执行的逻辑
}
public function postRemove(PostRemoveEventArgs $args): void
{
$entity = $args->getObject();
// 在实体删除之后执行的逻辑
}
}
在这个例子中,EntityListener 类包含六个方法,分别对应于我们注册的六个事件。每个方法都接收一个事件参数对象(例如 PrePersistEventArgs),该对象包含有关事件的上下文信息,例如受影响的实体和 EntityManager。 getObject() 方法允许我们获取与事件关联的实体。
事件参数对象
不同的事件对应不同的事件参数对象。 常用的事件参数对象包括:
PrePersistEventArgs:getObject()返回将要被持久化的实体对象。PostPersistEventArgs:getObject()返回已经被持久化的实体对象。PreUpdateEventArgs:getObject()返回将要被更新的实体对象。getEntityChangeSet()返回一个关联数组,其中键是已更改的属性的名称,值是一个包含旧值和新值的数组。hasChangedField(string $fieldName)方法检查给定的字段是否已更改。getNewValue(string $fieldName)和getOldValue(string $fieldName)分别获取字段的新值和旧值。PostUpdateEventArgs:getObject()返回已经被更新的实体对象。PreRemoveEventArgs:getObject()返回将要被删除的实体对象。PostRemoveEventArgs:getObject()返回已经被删除的实体对象。PreLoadEventArgs:getObject()返回将要被加载的实体对象。PostLoadEventArgs:getObject()返回已经被加载的实体对象。PreFlushEventArgs: 不包含实体对象。OnFlushEventArgs: 这个事件非常强大,它允许访问UnitOfWork,从而可以检查所有计划的更改,并且可以调度新的实体持久化、更新或删除操作。PostFlushEventArgs: 不包含实体对象。OnClearEventArgs: 不包含实体对象。OnClassMetadataNotFoundEventArgs: 允许你动态地加载类元数据。
事件订阅者 (Subscriber)
事件订阅者与事件监听器类似,都是用于监听 Doctrine ORM 事件的 PHP 类。 主要的区别在于,事件订阅者实现了 EventSubscriber 接口,并且必须显式地定义它所订阅的所有事件。
配置事件订阅者
事件订阅者的配置方式与事件监听器类似,也在 doctrine.yaml 或类似的配置文件中进行注册。
# config/packages/doctrine.yaml
doctrine:
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'AppEntity'
alias: App
subscribers:
soft_delete_subscriber:
class: AppEventSubscriberSoftDeleteSubscriber
在这个配置中,我们注册了一个名为 soft_delete_subscriber 的订阅者,它对应的类是 AppEventSubscriberSoftDeleteSubscriber。
创建事件订阅者类
接下来,我们需要创建 AppEventSubscriberSoftDeleteSubscriber 类,并实现 EventSubscriber 接口。
<?php
namespace AppEventSubscriber;
use DoctrineCommonEventSubscriber;
use DoctrineORMEventLifecycleEventArgs;
use DoctrineORMEvents;
use AppEntitySoftDeletableInterface;
class SoftDeleteSubscriber implements EventSubscriber
{
public function getSubscribedEvents(): array
{
return [
Events::preRemove,
Events::onFlush,
];
}
public function preRemove(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if ($entity instanceof SoftDeletableInterface) {
$entity->setDeletedAt(new DateTimeImmutable());
$em = $args->getEntityManager();
$em->persist($entity);
$em->flush(); // 立即刷新,而不是真正的删除
}
}
public function onFlush(DoctrineORMEventOnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof SoftDeletableInterface) {
$entity->setDeletedAt(new DateTimeImmutable());
$em->persist($entity);
$uow->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity);
}
}
}
}
在这个例子中,SoftDeleteSubscriber 类实现了 EventSubscriber 接口,并实现了 getSubscribedEvents() 方法,该方法返回一个数组,包含了订阅者所订阅的事件列表。 我们订阅了 preRemove 和 onFlush 事件。
preRemove 方法在实体被删除之前被调用。 如果实体实现了 SoftDeletableInterface 接口,我们则设置 deletedAt 属性,并使用 persist 方法将实体重新持久化。 然后,我们调用 flush 方法立即将更改同步到数据库。 这实际上并没有删除实体,而是将其标记为已删除。
onFlush 方法在 EntityManager::flush() 期间被调用。 我们使用 UnitOfWork 来获取所有计划删除的实体。 对于实现了 SoftDeletableInterface 的实体,我们执行与 preRemove 方法相同的操作,并使用 recomputeSingleEntityChangeSet 方法来重新计算实体的更改集,以便 Doctrine 能够正确更新数据库。
何时使用 Listener 和 Subscriber?
- Listener: 当你需要监听实体生命周期中的几个事件,并且逻辑相对简单时,Listener 是一个不错的选择。 Listener 更适合于处理单个实体的事件,例如设置创建时间和更新时间。
- Subscriber: 当你需要监听多个事件,并且这些事件之间存在逻辑关联,或者你需要访问
UnitOfWork对象时,Subscriber 更加合适。 Subscriber 更适合于实现更复杂的业务逻辑,例如软删除、审计日志记录等。
Listener 和 Subscriber 的优缺点
| 特性 | 事件监听器 (Listener) | 事件订阅者 (Subscriber) |
|---|---|---|
| 配置 | 配置文件 | 配置文件 |
| 接口 | 无 | EventSubscriber |
| 事件订阅 | 配置文件 | getSubscribedEvents() |
| 适用场景 | 简单事件处理 | 复杂事件处理 |
| 代码组织 | 单个类可以监听多个事件 | 更清晰的事件订阅 |
实际应用场景
- 审计日志记录: 在实体创建、更新或删除时,记录操作的用户、时间等信息。
- 数据验证: 在实体持久化之前,验证数据的有效性。
- 缓存失效: 在实体更新时,使相关的缓存失效。
- 软删除: 在删除实体时,不真正删除数据,而是将其标记为已删除。
- 自动填充字段: 在实体创建或更新时,自动填充某些字段的值,例如创建时间、更新时间等。
- 发送通知邮件: 在实体创建、更新或者删除时,发送通知邮件。
- 访问控制: 在加载实体时,根据用户的权限过滤数据。
代码示例:审计日志记录
<?php
namespace AppEventSubscriber;
use DoctrineCommonEventSubscriber;
use DoctrineORMEventLifecycleEventArgs;
use DoctrineORMEvents;
use AppEntityLogEntry;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
class AuditLogSubscriber implements EventSubscriber
{
private TokenStorageInterface $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function getSubscribedEvents(): array
{
return [
Events::postPersist,
Events::postUpdate,
Events::postRemove,
];
}
public function postPersist(LifecycleEventArgs $args): void
{
$this->logActivity('create', $args);
}
public function postUpdate(LifecycleEventArgs $args): void
{
$this->logActivity('update', $args);
}
public function postRemove(LifecycleEventArgs $args): void
{
$this->logActivity('delete', $args);
}
private function logActivity(string $action, LifecycleEventArgs $args): void
{
$entity = $args->getObject();
$em = $args->getEntityManager();
$logEntry = new LogEntry();
$logEntry->setEntity(get_class($entity));
$logEntry->setEntityId($em->getClassMetadata(get_class($entity))->getIdentifierValues($entity));
$logEntry->setAction($action);
$logEntry->setCreatedAt(new DateTimeImmutable());
$user = $this->getUser();
if ($user) {
$logEntry->setUser($user->getUserIdentifier());
}
$em->persist($logEntry);
$em->flush();
}
private function getUser(): ?UserInterface
{
$token = $this->tokenStorage->getToken();
if ($token) {
$user = $token->getUser();
if ($user instanceof UserInterface) {
return $user;
}
}
return null;
}
}
在这个例子中,AuditLogSubscriber 类实现了审计日志记录的功能。 它监听了 postPersist, postUpdate 和 postRemove 事件,并在实体创建、更新或删除时,创建一个 LogEntry 对象,记录操作的实体类型、实体 ID、操作类型、操作时间和操作用户。
结论
事件监听器和订阅者是 Doctrine ORM 中非常强大的工具,它们允许我们在实体持久化过程的关键时刻插入自定义逻辑,实现各种业务需求。 选择使用 Listener 还是 Subscriber 取决于具体的应用场景和代码组织方式。 理解 Doctrine ORM 的事件机制,能够帮助我们更好地利用这些工具,构建更加灵活和可维护的应用程序。
总结:深入理解,灵活运用
事件监听器和订阅者是Doctrine ORM中数据持久化的重要组成部分,通过监听ORM的生命周期事件,允许我们实现各种业务逻辑。理解两者的区别及应用场景,可以帮助我们构建更灵活和可维护的应用程序。