在Doctrine Entity Listener

时间:2015-06-09 14:21:30

标签: symfony doctrine-orm

为清楚起见,我继续讨论开始here

Doctrine Entity Listener内,在preUpdate方法中(我可以访问实体任何字段的旧值和新值)我试图保持与焦点实体无关的实体。

基本上我有实体A,当我在一个我要写的字段中更改一个值时,在project_notification表中,字段为oldValue,newValue和其他字段。

如果我没有在preUpdate方法中刷新,则新通知实体不会存储在DB中。如果我冲洗它,我进入一个无限循环。

这是preUpdate方法:

public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
{
    if ($event->hasChangedField('riskToleranceFlag')) {
    $project = $tolerances->getProject();                
    $em = $event->getEntityManager();
    $notification = new ProjectNotification();
    $notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
    $notification->setValueTo($event->getNewValue('riskToleranceFlag'));
    $notification->setEntity('Entity'); //TODO substitute with the real one
    $notification->setField('riskToleranceFlag');
    $notification->setProject($project);
    $em->persist($notification);


    // $em->flush(); // gives infinite loop
    }
}

谷歌搜索了一下我发现你无法在听众中调用同花,而here建议将这些东西存放在数组中,以便稍后在onFlush中进行刷新。尽管如此它不起作用(并且它可能不起作用,因为在调用preUpdate之后,侦听器类的实例会被破坏,因此当您稍后调用onFlush时,无论您在类级别作为受保护属性存储的内容都会丢失,或者我错过了什么?)。

以下是监听器的更新版本:

class ProjectTolerancesListener
{
    protected $toBePersisted = [];

    public function preUpdate(ProjectTolerances $tolerances, PreUpdateEventArgs $event)
    {
        $uow = $event->getEntityManager()->getUnitOfWork();
//        $hasChanged = false;

        if ($event->hasChangedField('riskToleranceFlag')) {
        $project = $tolerances->getProject();                
        $notification = new ProjectNotification();
        $notification->setValueFrom($event->getOldValue('riskToleranceFlag'));
        $notification->setValueTo($event->getNewValue('riskToleranceFlag'));
        $notification->setEntity('Entity'); //TODO substitute with the real one
        $notification->setField('riskToleranceFlag');
        $notification->setProject($project);

        if(!empty($this->toBePersisted))
            {
            array_push($toBePersisted, $notification);
            }
        else
            {
            $toBePersisted[0] = $notification;
            }
        }
    }

    public function postFlush(LifecycleEventArgs $event)
    {
        if(!empty($this->toBePersisted)) {

            $em = $event->getEntityManager();

            foreach ($this->toBePersisted as $element) {

                $em->persist($element);
            }

            $this->toBePersisted = [];
            $em->flush();
        }
    }
}

也许我可以通过从侦听器内部触发事件来解决这个问题,并在刷新后执行我的日志记录操作...但是:

1)我不知道我是否可以这样做

2)这似乎有点矫枉过正

谢谢!

4 个答案:

答案 0 :(得分:29)

我把理查德的所有学分都指向了正确的方向,所以我接受了他的回答。不过,我也会用未来访客的完整代码发布我的答案。

class ProjectEntitySubscriber implements EventSubscriber
{
    public function getSubscribedEvents()
    {
        return array(
            'onFlush',
        );
    }

    public function onFlush(OnFlushEventArgs  $args)
    {
        $em = $args->getEntityManager();
        $uow = $em->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $keyEntity => $entity) {
            if ($entity instanceof ProjectTolerances) {
                foreach ($uow->getEntityChangeSet($entity) as $keyField => $field) {
                    $notification = new ProjectNotification();
                    // place here all the setters
                    $em->persist($notification);
                    $classMetadata = $em->getClassMetadata('AppBundle\Entity\ProjectNotification');
                    $uow->computeChangeSet($classMetadata, $notification);
                }
            }
        }
    }
}

答案 1 :(得分:19)

不要使用preUpdate,使用onFlush - 这允许您访问UnitOfWork API&然后你可以坚持实体。

E.g。 (这是我在2.3中的做法,可能会在较新版本中更改)

    $this->getEntityManager()->persist($entity);
    $metaData = $this->getEntityManager()->getClassMetadata($className);
    $this->getUnitOfWork()->computeChangeSet($metaData, $entity);

答案 2 :(得分:2)

正如David Baucum所说,最初的问题是关于“教义实体监听器”的,但作为解决方案,操作最终使用了事件监听器。

由于无限循环问题,我敢肯定会有更多人迷失在这个话题上。 对于那些采用可接受答案的用户,请注意onFlush事件(当使用上述事件监听器时)与可能在更新队列中的所有实体一起执行,而实体监听器仅在使用“分配”到的实体。

我使用symfony 4.4和API平台设置了自定义审核系统,并且仅使用Entity Listener就设法达到了预期的结果。

注意:然而,经过测试并可以正常使用的名称空间和功能已经过修改,这纯粹是为了演示如何在Doctrine Entity Listener中操纵另一个实体。

// this goes into the main entity
/**
* @ORM\EntityListeners({"App\Doctrine\MyEntityListener"})
*/
<?
// App\Doctrine\MyEntityListener.php

namespace App\Doctrine;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Security;

// whenever an Employee record is inserted/updated
// log changes to EmployeeAudit
use App\Entity\Employee;
use App\Entity\EmployeeAudit;

private $security;
private $currentUser;
private $em;
private $audit;

public function __construct(Security $security, EntityManagerInterface $em) {
    $this->security = $security;
    $this->currentUser = $security->getUser();
    $this->em = $em;
}

// HANDLING NEW RECORDS

/**
 * since prePersist is called only when inserting a new record, the only purpose of this method
 * is to mark our object as a new entry
 * this method might not be necessary, but for some reason, if we set something like
 * $this->isNewEntry = true, the postPersist handler will not pick up on that
 * might be just me doing something wrong
 *
 * @param Employee $obj
 * @ORM\PrePersist()
 */
public function prePersist(Employee $obj){
    if(!($obj instanceof Employee)){
        return;
    }
    $isNewEntry = !$obj->getId();
    $obj->markAsNewEntry($isNewEntry);// custom Employee method (just sets an internal var to true or false, which can later be retrieved)
}

/**
 * @param Employee $obj
 * @ORM\PostPersist()
 */
public function postPersist(Employee $obj){
    // in this case, we can flush our EmployeeAudit object safely
    $this->prepareAuditEntry($obj);
}

// END OF NEW RECORDS HANDLING

// HANDLING UPDATES

/**
 * @see {https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/events.html}
 * @param Employee $obj
 * @param PreUpdateEventArgs $args
 * @ORM\PreUpdate()
 */
public function preUpdate(Employee $obj, PreUpdateEventArgs $args){
    $entity = $args->getEntity();
    $changeset = $args->getEntityChangeSet();

    // we just prepare our EmployeeAudit obj but don't flush anything
    $this->audit = $this->prepareAuditEntry($obj, $changeset, $flush = false);
}

/**
 * @ORM\PostUpdate()
 */
public function postUpdate(){
    // if the preUpdate handler was called, $this->audit should exist
    // NOTE: the preUpdate handler DOES NOT get called, if nothing changed
    if($this->audit){
        $this->em->persist($this->audit);
        $this->em->flush();
    }
    // don't forget to unset this
    $this->audit = null;
}

// END OF HANDLING UPDATES

// AUDITOR

private function prepareAuditEntry(Employee $obj, $changeset = [], $flush = true){
    if(!($obj instanceof Employee) || !$obj->getId()){
        // at this point, we need a DB id
        return;
    }

    $audit = new EmployeeAudit();
    // this part was cut out, since it is custom
    // here you would set things to your EmployeeAudit object
    // either get them from $obj, compare with the changeset, etc...

    // setting some custom fields
    // in case it is a new insert, the changedAt datetime will be identical to the createdAt datetime
    $changedAt = $obj->isNewInsert() ? $obj->getCreatedAt() : new \DateTime('@'.strtotime('now'));
    $changedFields = array_keys($changeset);
    $changedCount = count($changedFields);
    $changedBy = $this->currentUser->getId();
    $entryId = $obj->getId();

    $audit->setEntryId($entryId);
    $audit->setChangedFields($changedFields);
    $audit->setChangedCount($changedCount);
    $audit->setChangedBy($changedBy);
    $audit->setChangedAt($changedAt);

    if(!$flush){
        return $audit;
    }
    else{
        $this->em->persist($audit);
        $this->em->flush();
    }
}

这个想法是不保留/刷新preUpdate中的任何内容(准备数据除外,因为您可以访问变更集和内容),并在更新时执行postUpdate,在新插入时执行postPersist。

答案 3 :(得分:0)

在这种情况下,使用Lifecycle Listener代替EntityListener可能更合适(我发现symfony docs提供了关于不同选项的更好概述)。这归因于onFlush,这是一个非常强大的事件,不适用于EntityListeners。在计算所有变更集之后和执行数据库操作之前,将调用此事件。

在这个答案中,我使用Entity Listener 探索选项。

使用preUpdate :此事件提供了一个PreUpdateEventArgs,可让您轻松找到所有将要更改的值。但是,在处理了插入之后,将在UnitOfWork#commit中触发此事件。因此,现在无法添加要保留在当前交易中的新实体。

使用preFlush :此事件在flush操作开始时发生。变更集可能尚不可用,但是我们可以将原始值与当前值进行比较。当需要进行许多更改时,此方法可能不合适。这是一个示例实现:

    public function preFlush(Order $order, PreFlushEventArgs $eventArgs)
    {
        // Create a log entry when the state was changed
        $entityManager = $eventArgs->getEntityManager();
        $unitOfWork = $entityManager->getUnitOfWork();
        $originalEntityData = $unitOfWork->getOriginalEntityData($order);
        $newState = $order->getState();
        if (empty($originalEntityData)) {
            // We're dealing with a new order
            $oldState = "";
        } else {
            $stateProperty = 'state';
            $oldState = $originalEntityData[$stateProperty];
            // Same behavior as in \Doctrine\ORM\UnitOfWork:720: Existing
            // changeset is ignored when the property was changed
            $entityChangeSet = $unitOfWork->getEntityChangeSet($order);
            $stateChanges = $entityChangeSet[$stateProperty] ?? [];
            if ($oldState == $newState && $stateChanges) {
                $oldState = $stateChanges[0] ?? "";
                $newState = $stateChanges[1] ?? "";
            }
        }
        if ($oldState != $newState) {
            $statusLog = $this->createOrderStatusLog($order, $oldState, $newState);
            $unitOfWork->scheduleForInsert($statusLog);
            $unitOfWork->computeChangeSet($entityManager->getClassMetadata('App\Entity\OrderStatusLog'), $statusLog);
        }
    }

使用postFlush / postUpdate :使用这些事件将导致第二次数据库事务,这是不可取的。