Doctrine Mongodb getOriginalDocumentData on embedded document - symfony

In my symfony application i've got my event_subscriber
CoreBundle\EventSubscriber\CloseIssueSubscriber:
tags:
- { name: doctrine_mongodb.odm.event_subscriber, connection: default }
My subscriber simply listen to postPersist and postUpdate events:
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->index($args);
}
In my index function what I need to do is to get if certain field has changed in particular the issue.status field.
public function index(LifecycleEventArgs $args)
{
$document = $args->getEntity();
$originalData = $uow->getOriginalDocumentData($document);
$originalStatus = $originalData && !empty($originalData['issue']) ? $originalData['issue']->getStatus() : null;
var_dump($originalStatus);
var_dump($document->getIssue()->getStatus());die;
}
In my test what I do is change the issue.status field so I expect to receive 2 different values from the var_dump but instead I got the last status from both.
My document is simply something like that:
class Payload
{
/**
* #ODM\Id
*/
private $id;
/**
* #ODM\EmbedOne(targetDocument="CoreBundle\Document\Issue\Issue")
* #Type("CoreBundle\Document\Issue\Issue")
*/
protected $issue;
}
In the embedded issue document status is simply a text field.
I've also try to use the changeset:
$changeset = $uow->getDocumentChangeSet($document);
foreach ($changeset as $fieldName => $change) {
list($old, $new) = $change;
}
var_dump($old->getStatus());
var_dump($new->getStatus());
Also this two var_dumps returns the same status.

By the time of postUpdate changes in the document are already done so originalDocumentData is adjusted and ready for new calculations. Instead you should hook into preUpdate event and use $uow->getDocumentChangeSet($document); there.
I guess that you want to run index once changes have been written to the database, so on preUpdate you can accumulate changes in the listener and additionally hook into postFlush event to re-index documents.

I found the solution to my problem.
What malarzm said in the other answer is correct but not the solution to my problem.
I suppose that I get only one postUpdate/preUpdate postPersist/prePersist just for the Document (Payload) instead I notice that it get called event for the embedded document (don't know why doctrine consider it a persist).
So the main problem is that I'm waiting for a Payload object instead I have to wait for a Issue object.
In other hands I was unable to use the getOriginalDocumentData work right even in the postUpdate and in the preUpdate so I have to use the getDocumentChangeSet().

Related

Why are Drupal Entity Events not firing?

namespace Drupal\ta3mal_utilities\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Entity\EntityTypeEvents;
use Drupal\Core\Entity\EntityTypeEvent;
class EntityEvents implements EventSubscriberInterface {
public function onCreate(EntityTypeEvent $event) {
//Do things on creation
}
public function onUpdate(EntityTypeEvent $event) {
dd('on update now');
}
public function onDelete(EntityTypeEvent $event) {
//Do things on delete
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[EntityTypeEvents::CREATE][] = ['onCreate', 1];
$events[EntityTypeEvents::UPDATE][] = ['onUpdate', 1];
$events[EntityTypeEvents::DELETE][] = ['onDelete', 1];
return $events;
}
}
I have included this in the services file as well, the getSubscribedEvents is called but it doe not reach the updated one, any help ?
Thank you.
EntityTypeEvents::XXX is triggered when entity types are being created/updated/deleted. Looking at your classname, I'm assuming you want the entity events. Currently it's not possible to do this with Drupal Core and the events combo you're trying to implement. Referring to this issue: https://www.drupal.org/project/drupal/issues/2551893 you could also try the contributed module mentioned in the ticket, but I don't have any experience with that module, so mileage may vary.
If you want entity events you will need to stick with the old hook style system.
The hooks you're looking for are:
hook_entity_update
hook_entity_delete
hook_entity_create / hook_entity_insert (depending on what you want to do)
Full reference here in the Drupal docs: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/8.8.x
Another thing that I noticed that might help you in the future when implementing event listeners is that you declared all your subscribe events with priority 1. It could be that you're putting the priority too high up in the hierarchy. If you don't need to expliytidly execute your code before any other Drupal Modules or core functionality, I would omit the priority and just leave it empty so the default value (0) will be used, this will make sure your event is added to the end of the list. The order of execution could mean you're breaking other modules/core functionality, so always be careful with that.
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents() {
$events[EntityTypeEvents::CREATE][] = ['onCreate'];
$events[EntityTypeEvents::UPDATE][] = ['onUpdate'];
$events[EntityTypeEvents::DELETE][] = ['onDelete'];
return $events;
}
Smalle code nit, you can also omit $events = []; since it's not needed to declare an empty array.

Doctrine: How to suppress further lifecycle events when updating entities inside entity's listener?

I am working on an app in Symfony 3 using Doctrine.
Whenever I persist or update a specific entity, I need to query for a subset of these entities (same class) and update a property for each.
Currently I have an entity listener class with postPersist() and postUpdate() handlers which call a function performing the aforementioned behaviour.
However, I am finding that when persisting changes for these other entities, it is triggering events leading to additional lifecycle callbacks to postUpdate() on the same listener (as somewhat expected, but undesired).
I wanted to ask if anyone would know of a way to suppress these additional events or possibly have any ideas on alternative solutions to this scenario.
Update: Providing sample code to help illustrate the issue
// Subscribed Entity Listener for Foo
class FooListener
{
...
public function postPersist(Foo $foo, LifecycleEventArgs $event)
{
...
$this->updateFlaggedFoos($event);
}
public function postUpdate(Foo $foo, LifecycleEventArgs $event)
{
...
$this->updateFlaggedFoos($event);
}
private function updateFlaggedFoos(LifecycleEventArgs $event)
{
$user = $this->tokenStorage->getToken()->getUser();
$entityManager = $event->getEntityManager();
$userFoos = $entityManager->getRepository('MyApp:Foo')->getUserFoos($user);
foreach($userFoos as $foo) {
if ($this->determineIfFlagValueNeedsToChange($foo)) {
$foo->setFlagValue( ! $foo->isFlagged());
$entityManager->persist($foo); // Causes postUpdate() to be called again.
}
}
}
...
}

preUpdate() siblings manage into tree: how to break ->persist() recursion?

Let's say I've got an entity like this
class FooEntity
{
$id;
//foreign key with FooEntity itself
$parent_id;
//if no parent level =1, if have a parent without parent itself = 2 and so on...
$level;
//sorting index is relative to level
$sorting_index
}
Now I would like on delete and on edit to change level and sorting_index of this entity.
So I've decided to take advantage of Doctrine2 EntityListeners and I've done something similar to
class FooListener
{
public function preUpdate(Foo $entity, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$this->handleEntityOrdering($entity, $em);
}
public function preRemove(Foo $entity, LifecycleEventArgs $args)
{
$level = $entity->getLevel();
$cur_sorting_index = $entity->getSortingIndex();
$em = $args->getEntityManager();
$this->handleSiblingOrdering($level, $cur_sorting_index, $em);
}
private function handleEntityOrdering($entity, $em)
{
error_log('entity to_update_category stop flag: '.$entity->getStopEventPropagationStatus());
error_log('entity splobj: '.spl_object_hash($entity));
//code to calculate new sorting_index and level for this entity (omitted)
$this->handleSiblingOrdering($old_level, $old_sorting_index, $em);
}
}
private function handleSiblingOrdering($level, $cur_sorting_index, $em)
{
$to_update_foos = //retrieve from db all siblings that needs an update
//some code to update sibling ordering (omitted)
foreach ($to_update_foos as $to_update_foo)
{
$em->persist($to_update_foo);
}
$em->flush();
}
}
The problem here is pretty clear: if I persist a Foo entity, preUpdate() (into handleSiblingOrdering function) trigger is raised and this cause an infinite loop.
My first idea was to insert a special variable inside my entity to prevent this loop: when I started a sibling update, that variable is setted and before executing the update code is checked. This works like a charm for preRemove() but not for preUpdate().
If you notice I'm logging spl_obj_hash to understand this behaviour. With a big surprise I can see that obj passed to preUpdate() after a preRemove() is the same (so setting a "status flag" is a fine) but the object passed to preUpdate() after a preUpdate() isn't the same.
So ...
First question
Someone could point me in the right direction to manage this situation?
Second question
Why doctrine needs to generate different objects if two similar events are raised?
I've founded a workaround
Best approach to this problem seem to create a custom EventSubscriber with a custom Event dispatched programmatically into controller update action.
That way I can "break" the loop and having a working code.
Just to make this answer complete I will report some snippet of code just to clarify che concept
Create custom events for your bundle
//src/path/to/your/bundle/YourBundleNameEvents.php
final class YourBundleNameEvents
{
const FOO_EVENT_UPDATE = 'bundle_name.foo.update';
}
this is a special class that will not do anything but provide some custom events for our bundle
Create a custom event for foo update
//src/path/to/your/bundle/Event/FooUpdateEvent
class FooUpdateEvent
{
//this is the class that will be dispatched so add properties useful for your own logic. In my example two properties could be $level and $sorting_index. This values are setted BEFORE dispatch the event
}
Create a custom event subscriber
//src/path/to/your/bundle/EventListener/FooSubscriber
class FooSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(YourBundleNameEvents::FooUpdate => 'handleSiblingsOrdering');
}
public function handleSiblingsOrdering(FooUpdateEvent $event)
{
//I can retrieve there, from $event, all data I setted into event itself. Now I can run all my own logic code to re-order siblings
}
}
Register your Subscriber as a service
//app/config/config.yml
services:
your_bundlename.foo_listener:
class: Your\Bundle\Name\EventListener\FooListener
tags:
- { name: kernel.event_subscriber }
Create and dispatch events into controller
//src/path/to/your/bundle/Controller/FooController
class FooController extends Controller
{
public function updateAction()
{
//some code here
$dispatcher = $this->get('event_dispatcher');
$foo_event = new FooEvent();
$foo_event->setLevel($level); //just an example
$foo_event->setOrderingIndex($ordering_index); //just an examle
$dispatcher->dispatch(YourBundleNameEvents::FooUpdate, $foo_event);
}
}
Alternative solution
Of course above solution is the best one but, if you have a property mapped into db that could be used as a flag, you could access it directly from LifecycleEventArgs of preUpdate() event by calling
$event->getNewValue('flag_name'); //$event is an object of LifecycleEventArgs type
By using that flag we could check for changes and stop the propagation
You are doing wrong approach by calling $em->flush() inside preUpdate, I even can say restricted by Doctrine action: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#reference-events-implementing-listeners
9.6.6. preUpdate
PreUpdate is the most restrictive to use event, since it is called
right before an update statement is called for an entity inside the
EntityManager#flush() method.
Changes to associations of the updated entity are never allowed in
this event, since Doctrine cannot guarantee to correctly handle
referential integrity at this point of the flush operation.

Symfony2 - postPersist does not happen after persist

got another Symfony2 issue, more specifically postPersist does not seem to execute after the persist.
In my controller I have a createAction (removed some code)
public function createAction(Request $request)
{
try {
$em = $this->getDoctrine()->getManager();
$alert = new AvailabilityAlert();
$alert->setLastUpdated();
$alert->setIsDeleted(0);
$alert->setAlertStatus('Active');
$em->persist($alert);
$em->flush();
return new JsonResponse('Success');
}catch (Exception $e) {
}
}
To this, I have an EventListener in place
<?php
namespace Nick\AlertBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Nick\AlertBundle\Entity\AvailabilityAlert;
use Nick\AlertBundle\Service\UapiService;
class AvailabilityAlertListener
{
protected $api_service;
public function __construct(UapiService $api_service)
{
$this->api_service = $api_service;
}
public function postPersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof AvailabilityAlert) {
$this->api_service->addFlightsAction($entity);
}
}
}
From what I understand, this postPersist should execute after the AvailabilityAlert is persisted to the database?
Inside my listener I call a function addFlightsAction. This function does a database query. This query essentially selects the data I need where the id is equal to the id I pass it (the one created by new AvailabilityAlert() ). To get this id, I use $entity->getId().
I have outputted the id and it is correct. However, when I output the result from the database query I receive an empty array. If however in my database query I do
$id = $entity->getId();
... query code
->setParameter('id', $id-1)
So take 1 away from the current id, the query returns the data from the last alert I inserted. So this tells me that when I give it the alert id of the current alert and I get no data from this, the current alert has not been persisted to the database at the time this is happening.
Is there any way to resolve this?
p.s. My service
doctrine.availability_alert_listener:
class: Nick\AlertBundle\EventListener\AvailabilityAlertListener
arguments: [#alert_bundle.api_service]
tags:
- { name: doctrine.event_listener, event: postPersist }
Thanks
Since the inserts are happening in a transaction, which hasn't been committed yet, the records that you have just written may not yet be available to read back in.
If all the data you need to do the call to addFlightsAction is in the entity, you have the data available there. Alternatively, you may be able to gather the rest of the data that has just been written via $args->getEntityManager()->getUnitOfWork();
Otherwise, you could always just run the addFlight code after the flush, when the transaction has been committed.
Fire your query in the postFlush event instead of the postPersist event, this should let you do $id-1 as you described above.
Keep in mind that postFlush args are a instance of PostFlushEventArgs, not LifecycleEventArgs

ReferenceRepository strips related entities when calling getReference()

I am writing a Symfony 2 unit test that relies heavily on data fixtures. As a shortcut, I wired up a method that will give me access to the fixture loader's ReferenceRepository so that I can access shared entities in my tests.
However, when I pull an object out of the ReferenceRepository, it has no relations, even though I persist them in the data fixture.
The weird part is, there is some code in ReferenceRepository that appears to be stripping those relations out, and I don't understand why it is doing this (let alone how to prevent it).
As an example, here is what a data fixture looks like:
public function load(ObjectManager $manager)
{
$project = new Project();
// ... populate fields ...
/* Add one detail field to the Project. */
$detail = new ProjectDetail();
// ... populate fields ...
$project->addDetail($detail);
$manager->persist($project);
$manager->flush();
$this->addReference('project-onedetail', $project);
}
In my test case, I am doing something (more or less) like this:
$project =
$this->fixtureLoader->getReferenceRepository()
->getReference('project-onedetail');
When I call the method in the test case to grab this Project object, I notice some weird behavior:
From Doctrine\Common\DataFixtures\ReferenceRepository (comments added):
public function getReference($name)
{
$reference = $this->references[$name];
// At this point, $reference contains the Project object with related ProjectDetail.
// It would be awesome if the method would just return $reference...
$meta = $this->manager->getClassMetadata(get_class($reference));
$uow = $this->manager->getUnitOfWork();
if (!$uow->isInIdentityMap($reference) && isset($this->identities[$name])) {
// ... but instead it goes into this conditional....
$reference = $this->manager->getReference(
$meta->name,
$this->identities[$name]
);
// ... and now $reference->getDetails() is empty! What just happened??
$this->references[$name] = $reference; // already in identity map
}
return $reference;
}
What's going on in ReferenceRepository->getReference()? Why are the related objects getting removed from $reference, and how do I prevent that?
What's Going On
After the fixture loader runs, it clears out the UnitOfWork's identity map.
See \Doctrine\Common\DataFixtures\Executor\AbstractExecutor:
public function load(ObjectManager $manager, FixtureInterface $fixture)
{
...
$fixture->load($manager);
$manager->clear();
}
As a result, the condition !$uow->isInIdentityMap($reference) in ReferenceRepository->getReference() will always evaluate to false after the fixture loader has finished.
The Workaround
You can work around this by clearing out ReferenceRepository->$identities. Unfortunately, you don't have direct access to this array, so you'll need to do something slightly kludgy like:
/* #kludge The fixture loader clears out its UnitOfWork object after
* loading each fixture, so we also need to clear the
* ReferenceRepository's identity map.
*/
$repository = $this->fixtureLoader->getReferenceRepository();
$identities = array_keys($repository->getIdentities());
foreach($identities as $key)
{
$repository->setReferenceIdentity($key, null);
}
However, if you do that, you may run into some nasty ORMInvalidArgumentExceptions if you set related objects in your test fixtures:
Doctrine\ORM\ORMInvalidArgumentException: A new entity was found through the relationship '...' that was not configured to cascade persist operations for entity: url. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
The Solution
Ultimately, if you want this to work properly, you'll need to change the behavior of the fixture executor that you use in your test cases so that it does not clear the manager after loading fixtures:
/** Executes data fixtures for unit tests.
*/
class TestExecutor extends ORMExecutor
{
/** Load a fixture with the given persistence manager.
*
* #param ObjectManager|EntityManager $manager
* #param FixtureInterface $fixture
*/
public function load(ObjectManager $manager, FixtureInterface $fixture)
{
/** #kludge Unfortunately, we have to copy-paste a bit of code.
*
* The only difference between this method and AbstractExecutor->load()
* is that we don't call $manager->clear() when we're done loading.
*/
if($this->logger)
{
$prefix = '';
if($fixture instanceof OrderedFixtureInterface)
{
$prefix = sprintf('[%d] ', $fixture->getOrder());
}
$this->log('loading ' . $prefix . get_class($fixture));
}
// additionally pass the instance of reference repository to shared fixtures
if($fixture instanceof SharedFixtureInterface)
{
$fixture->setReferenceRepository($this->referenceRepository);
}
$fixture->load($manager);
/* Do NOT clear the unit of work; we will keep managed entities so that
* they are available to tests.
*/
}
}

Resources