为清楚起见,我继续讨论开始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)这似乎有点矫枉过正
谢谢!
答案 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 :使用这些事件将导致第二次数据库事务,这是不可取的。