Doctrine ORM的事件监听器(Listener)与订阅者(Subscriber):在数据持久化前后执行业务逻辑

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() 方法,该方法返回一个数组,包含了订阅者所订阅的事件列表。 我们订阅了 preRemoveonFlush 事件。

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, postUpdatepostRemove 事件,并在实体创建、更新或删除时,创建一个 LogEntry 对象,记录操作的实体类型、实体 ID、操作类型、操作时间和操作用户。

结论

事件监听器和订阅者是 Doctrine ORM 中非常强大的工具,它们允许我们在实体持久化过程的关键时刻插入自定义逻辑,实现各种业务需求。 选择使用 Listener 还是 Subscriber 取决于具体的应用场景和代码组织方式。 理解 Doctrine ORM 的事件机制,能够帮助我们更好地利用这些工具,构建更加灵活和可维护的应用程序。

总结:深入理解,灵活运用

事件监听器和订阅者是Doctrine ORM中数据持久化的重要组成部分,通过监听ORM的生命周期事件,允许我们实现各种业务逻辑。理解两者的区别及应用场景,可以帮助我们构建更灵活和可维护的应用程序。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注