dunglas / doctrine-json-odm

An object document mapper for Doctrine ORM using JSON types of modern RDBMS.

Home Page:https://dunglas.fr

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

POPOs data is not being persisted

opened this issue · comments

Thanks for your nice bundle. I got the issue: if I change the json_document related plain php object property, it won't persisted by default.

...
$foo->misc[0]->setName('New Name');
$em->persist($foo);
$em->flush(); //nothing to update by uow
commented

I have resolved this issue with DoctrineEventSubscriber

<?php

use Doctrine\Common\Inflector\Inflector;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Common\EventSubscriber;

class JsonDocumentSubscriber implements EventSubscriber
{
    /**
     * @return array
     */
    public function getSubscribedEvents(): array
    {
        return [
            Events::preUpdate
        ];
    }

    /**
     * @param PreUpdateEventArgs $event
     */
    public function preUpdate(PreUpdateEventArgs $event): void
    {
        $subject = $event->getObject();

        $em   = $event->getEntityManager();
        $uow  = $em->getUnitOfWork();
        $meta = $em->getClassMetadata(get_class($subject));

        $jsonObjects = $this->filterJsonObjects($meta);

        if($this->forceUpdateJsonDocument($jsonObjects, $subject)) {
            $uow->recomputeSingleEntityChangeSet($meta, $subject);
        }
    }

    /**
     * @param ClassMetadata $meta
     *
     * @return array
     */
    private function filterJsonObjects(ClassMetadata $meta): array 
    {
        $jsonObjects = [];
        $fieldMappings = $meta->fieldMappings;

        if(is_array($fieldMappings)) {
            foreach($fieldMappings as $filed => $options) {
                if($options['type'] === 'json_document') {
                    $jsonObjects[] = $filed;
                }
            }
        }

        return $jsonObjects;
    }

    /**
     * @param array $fields
     * @param       $object
     *
     * @return bool
     */
    private function forceUpdateJsonDocument(array $fields, $object): bool
    {
        $hasUpdates = false;

        if(!empty($fields)) {
            foreach($fields as $fieldName) {
                $methodGet = 'get'.Inflector::camelize($fieldName);
                $methodSet = 'set'.Inflector::camelize($fieldName);

                $newValue = null;
                $actualValue = $object->{$methodGet}();
                if(is_array($actualValue)) {
                    $newValue = [];
                    foreach ($actualValue as $k => $v) {
                        $newValue[$k] = clone $v;
                    }
                }

                if(is_object($actualValue)) {
                    $newValue =  clone $actualValue;
                }

                if(null !== $newValue) {
                    $object->{$methodSet}($newValue);
                    $hasUpdates = true;
                }
            }
        }

        return $hasUpdates;
    }
}

@Dragonqos How are your entities mapped? I can't get this to work - preUpdate event isn't fired. Or maybe you have other fields which are changed too?

commented

@thatside-zaraffa Yes, I am updating "updated_at" field manually when needed, to fire event.

@Dragonqos Seems it is the reason of your subscriber working :)
In my case I do not update any field except json_document one so preUpdate isn't called.
I'll write down the solution here when I'll find one.

Actually I've come to same solution - added updatedAt field and update it manually. Not the greatest way to work...

The problem is with the way UOW compares objects - it is simple "equals" comparison ===. I've researched this a little more and found out that it is enough to clone the embedded document to change it's link.
So you need to do this:

$data = clone $foo->misc;
$data[0]->setName('New Name');
$foo->misc = $data;
$em->persist($foo);
$em->flush(); 

My code is slightly different (simple object instead of array as misc field) but this should work.

Would you be able to open a PR with a failing test?

So regarding to what @thatside-zaraffa said, there's no work for this bundle? Or is there any listener that could get all POPOs and clone them if changed?

Any news about this issue ?

Maybe I can give some background here. So the issue is that Doctrine does not blindly execute UPDATE queries but instead it calculates which properties have changed. If you want to have a look at where it does this, checkout UnitOfWork::computeChangeSet().
Specifically what happens is this here: https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/UnitOfWork.php#L607

So if your object stays the same (PHP compares the object reference when you do ===), Doctrine will not notice that a nested entity or property changed. So this is actually not really an issue related to this type but a general Doctrine issue or limitation.
An easy fix would be to clone the object before you set it. That way === would always return false.

Does this help?

Thank you @Toflar , I knew that Doctrine checked the status of the changes before saving but I did not think the object appear as the same even if a nested object was changed :).
I'll simply clone the object before set it.

Is there a way to make the embedded objects behave like associations so that doctrine checks them too (instead of just the parent object) with something like an implicit cascade="persist" ?

Imho there's not.
Folks, I'm closing this issue as there's nothing we can do about it and I would like to clean up the issues a little bit :)