在Doctrine Entity Listener的preUpdate中保留其他实体

Ser*_*gri 18 symfony doctrine-orm

为了清楚我在这里继续讨论开始在这里.

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

1)我不知道我是否能做到

2)这似乎有点矫枉过正

谢谢!

Ser*_*gri 35

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

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);
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我花了一些时间才意识到这一点,但是在这个答案中,您离开了Doctrine Entity Listener,并实现了Doctrine Event Listener。实体侦听器没有onFlush或postFlush事件。 (3认同)

Ric*_*ard 22

不要使用preUpdate,使用onFlush - 这允许您访问UnitOfWork API,然后您可以保留实体.

例如(我在2.3中这样做,可能会在较新版本中更改)

    $this->getEntityManager()->persist($entity);
    $metaData = $this->getEntityManager()->getClassMetadata($className);
    $this->getUnitOfWork()->computeChangeSet($metaData, $entity);
Run Code Online (Sandbox Code Playgroud)

  • 当然.UnitOfWork有getEntityChangeSet - http://www.doctrine-project.org/api/orm/2.0/class-Doctrine.ORM.UnitOfWork.html (3认同)
  • 您可以在onFlush中访问oldValue和newValue吗? (2认同)

man*_*man 6

正如 David Baucum 所说,最初的问题涉及 Doctrine 实体监听器,但作为解决方案,该操作最终使用了事件监听器。

我确信由于无限循环问题,更多人会偶然发现这个主题。对于那些采用已接受答案的人,请注意 onFlush 事件(当使用如上所述的事件侦听器时)是与可能在更新队列中的所有实体一起执行的,而实体侦听器仅在对它被“分配”到的实体。

我使用 symfony 4.4 和 API 平台设置了一个自定义审核系统,并且仅使用实体监听器就达到了预期的结果。

注意:然而,经过测试和工作,命名空间和函数已被修改,这纯粹是为了演示如何在 Doctrine 实体监听器中操作另一个实体。

// this goes into the main entity
/**
* @ORM\EntityListeners({"App\Doctrine\MyEntityListener"})
*/
Run Code Online (Sandbox Code Playgroud)
<?
// 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();
    }
}

Run Code Online (Sandbox Code Playgroud)

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