Search code examples
phpsymfonydoctrineentitysymfony4

Symfony 4 Serializer: deserializing request and merging it with entity including relation that is not completely passed by client


Let's say I have a entity called User:

class User implements UserInterface
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
     private $id;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
     private $username;

     /**
     * @ORM\OneToOne(targetEntity="App\Entity\Address", cascade={"persist", "remove"})
     * @ORM\JoinColumn(nullable=false)
     */
    private $address;

The address field is a OneToOne relation to the address entity:

class Address
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $street;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

And I have a Controller for updating the user and his address:

...
 public function putUserSelf(Request $request)
    {
        $em = $this->getDoctrine()->getManager();
        $user = $this->getUser();

        $encoders = array(new JsonEncoder());
        $normalizers = array(new ObjectNormalizer(null, null, null, new ReflectionExtractor()));

        $serializer = new Serializer($normalizers, $encoders);
        $user = $serializer->deserialize($request->getContent(), User::class, "json", ['deep_object_to_populate' => $user]);
        $em->persist($user);
        $em->flush();

In theory I should now be possible to pass a json like this:

{
    "username": "foo",
    "address": {"name": "bar"}
}

to update my entity. But the problem is, that I get a sql error:

An exception occurred while executing 'INSERT INTO address (street, name) VALUES (?, ?)' with params [null, "bar"]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'street' cannot be null

It seems like the entity merge did not work.


Solution

  • according to the docs,

    When the AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE option is set to true, existing children of the root OBJECT_TO_POPULATE are updated from the normalized data, instead of the denormalizer re-creating them. [...] (emphasis mine)

    so you have to set an extra option, so your line should be:

        $user = $serializer->deserialize($request->getContent(), User::class, "json", [
            'object_to_populate' => $user, // this still needs to be set, without the "deep_"
            'deep_object_to_populate' => true,
        ]);
    

    there is some additional comment in the source code of the specific component:

    /**
     * Flag to tell the denormalizer to also populate existing objects on
     * attributes of the main object.
     *
     * Setting this to true is only useful if you also specify the root object
     * in OBJECT_TO_POPULATE.
     */
    public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
    

    source: https://github.com/symfony/symfony/blob/d8a026bcadb46c9955beb25fc68080c54f2cbe1a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php#L82