My friends and family are under attack in Ukraine. Donate to protect them directly or help international organizations.

Doctrine not saving ManyToMany

March 26th, 2015

Say you have a ManyToMany relationship, like Post <—> Tag.

// AcmeBundle/Entity/Post.php
/**
 * @ORM\ManyToMany(targetEntity="Tag", inversedBy="posts", cascade={"persist","remove"})
 */
 protected $tags;

// AcmeBundle/Entity/Tag.php
/**
 * @ORM\ManyToMany(targetEntity="Post", mappedBy="tags", cascade={"persist","remove"})
 */
 protected $posts;

When you add tags to a Post and the save the Post, everything is great. But when you add posts to a Tag and then save the Tag, the posts are not saved. Why? Because Doctrine persists changes only on the owning side of a relation. The owning side is the one that has the inversedBy.

How to fix this? Simple. If you're accessing the Entity directly (without a Symfony form), then there is only 1 step.

1. On the inverse side (Tag), you will need to edit the following methods: addPost and removePost.

public function addPost(Post $post)
{
    $this->posts[] = $post;
    $post->addTag($this);
    return $this;
}

public function removePost(Post $post)
{
    $this->posts->removeElement($post);
    $post->removeTag($this);
}

This will keep the owning side in sync, so that persisting either the Post or the Tag will save the association. At this point, you can go ahead and clean up the cascade in the Tag entity, as it's no longer necessary.

@ORM\ManyToMany(targetEntity="Post", mappedBy="tags")

2. If you're using a Symfony form with a collection field to associate the posts, you need to add an extra option in your form builder:

->add('posts', 'collection', array(
    'by_reference' => false,
    //...
))

This is only needed in the Tag form (the inverse side of the relationship). If you omit this option, it will default to true and will set posts by reference. It will go fetch the ArrayCollection of posts and manipulate it directly, therefore not calling addPost/removePost that you need to keep things in sync. Setting this option to false will force the form to call these methods.

Happy coding!

Previous: How skiing made me a better developer Next: Conference Travel Expenses