Easyadminbundle: ManyToMany relation is not saved to database

Created on 5 Feb 2016  路  18Comments  路  Source: EasyCorp/EasyAdminBundle

I can add category in the post but can't add post in the category.

Post Entity:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Post
 *
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
 */
class Post
{
    const NUM_ITEMS = 10;

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

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

    /**
     * @var string
     *
     * @ORM\Column(name="content", type="text")
     */
    private $content;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="publishedAt", type="datetime")
     */
    private $publishedAt;

    /**
     * @ORM\ManyToMany(targetEntity="Category", inversedBy="posts", cascade={"persist"})
     * @ORM\JoinTable(name="post_category")
     */
    private $categories;

    public function __toString()
    {
        return $this->title;
    }

    /**
     * Post constructor.
     */
    public function __construct()
    {
        $this->publishedAt = new \DateTime();
        $this->categories = new ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set title
     *
     * @param string $title
     * @return Post
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * Get title
     *
     * @return string 
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * Set slug
     *
     * @param string $slug
     * @return Post
     */
    public function setSlug($slug)
    {
        $this->slug = $slug;

        return $this;
    }

    /**
     * Get slug
     *
     * @return string 
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * Set summary
     *
     * @param string $summary
     * @return Post
     */
    public function setSummary($summary)
    {
        $this->summary = $summary;

        return $this;
    }

    /**
     * Get summary
     *
     * @return string 
     */
    public function getSummary()
    {
        return $this->summary;
    }

    /**
     * Set content
     *
     * @param string $content
     * @return Post
     */
    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }

    /**
     * Get content
     *
     * @return string 
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Set publishedAt
     *
     * @param \DateTime $publishedAt
     * @return Post
     */
    public function setPublishedAt($publishedAt)
    {
        $this->publishedAt = $publishedAt;

        return $this;
    }

    /**
     * Get publishedAt
     *
     * @return \DateTime 
     */
    public function getPublishedAt()
    {
        return $this->publishedAt;
    }

    /**
     * Add categories
     *
     * @param \AppBundle\Entity\Category $categories
     * @return Post
     */
    public function addCategory(\AppBundle\Entity\Category $categories)
    {
        $this->categories[] = $categories;

        return $this;
    }

    /**
     * Remove categories
     *
     * @param \AppBundle\Entity\Category $categories
     */
    public function removeCategory(\AppBundle\Entity\Category $categories)
    {
        $this->categories->removeElement($categories);
    }

    /**
     * Get categories
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getCategories()
    {
        return $this->categories;
    }
}

Category Entity:

 <?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * Category
 *
 * @ORM\Table(name="category")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CategoryRepository")
 */
class Category
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

    /**
     * @var string
     *
     * @ORM\Column(name="content", type="text")
     */
    private $content;

    /**
     * @ORM\ManyToMany(targetEntity="Post", mappedBy="categories", cascade={"persist"})
     */
    private $posts;

    public function __toString()
    {
        return $this->name;
    }

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->posts = new ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     * @return Category
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string 
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set slug
     *
     * @param string $slug
     * @return Category
     */
    public function setSlug($slug)
    {
        $this->slug = $slug;

        return $this;
    }

    /**
     * Get slug
     *
     * @return string 
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * Set content
     *
     * @param string $content
     * @return Category
     */
    public function setContent($content)
    {
        $this->content = $content;

        return $this;
    }

    /**
     * Get content
     *
     * @return string 
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Add posts
     *
     * @param \AppBundle\Entity\Post $posts
     * @return Category
     */
    public function addPost(\AppBundle\Entity\Post $posts)
    {
        $this->posts[] = $posts;

        return $this;
    }

    /**
     * Remove posts
     *
     * @param \AppBundle\Entity\Post $posts
     */
    public function removePost(\AppBundle\Entity\Post $posts)
    {
        $this->posts->removeElement($posts);
    }

    /**
     * Get posts
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getPosts()
    {
        return $this->posts;
    }
}

Most helpful comment

You need to set by_reference option to false in collection form in Category:

- { property: 'posts', type_options: { by_reference: false} }

and also as @Pierstoval said modify Category entity:

// Category.php

    /**
     * Add posts
     *
     * @param \AppBundle\Entity\Post $posts
     * @return Category
     */
    public function addPost(\AppBundle\Entity\Post $posts)
    {
        if (!$this->posts->contains($posts)) {
            $this->posts[] = $posts;
            $posts->addCategory($this);
        }

        return $this;
    }

    /**
     * Remove posts
     *
     * @param \AppBundle\Entity\Post $posts
     */
    public function removePost(\AppBundle\Entity\Post $posts)
    {
        $this->posts->removeElement($posts);
        $posts->removeCategory($this);
    }

this is said in documentation http://symfony.com/doc/current/cookbook/form/form_collections.html

A second potential issue deals with the Owning Side and Inverse Side of Doctrine relationships. In this example, if the "owning" side of the relationship is "Task", then persistence will work fine as the tags are properly added to the Task. However, if the owning side is on "Tag", then you'll need to do a little bit more work to ensure that the correct side of the relationship is modified.

The trick is to make sure that the single "Task" is set on each "Tag". One easy way to do this is to add some extra logic to addTag(), which is called by the form type since by_reference is set to false:

All 18 comments

I had same problem few weeks ago. This should be define as bidirectional relation Many2Many.

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Group", inversedBy="users")
     * @ORM\JoinTable(name="zu_user_groups",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")}
     * )
     */
    protected $groups;  
    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\User", mappedBy="groups")
     *
     */
    protected $users;

    public function getUsers()
    {
        return $this->users;
    }

    public function addUser(User $user)
{
    $this->users[] = $user;
    $user->addGroup($this);
    return $this;
}

public function removeUser(User $user)
{
    $this->users->removeElement($user);
    $user->removeGroup($this);
}      

@mkalisz77 the mapping is valid for a bidirectionnal relationship.
The main issue may come from the fact that each entity need the proper getters/setters for ManyToMany relationships, but @slmcncb has not shown any of these so we can't be sure it works.

@Pierstoval I updated the issue.

Your mappings seem correct:

* @ORM\ManyToMany(targetEntity="Post", mappedBy="categories", cascade={"persist"})
* @ORM\ManyToMany(targetEntity="Category", inversedBy="posts", cascade={"persist"})

But I don't know if it's totally working because the targetEntity stores only the class name and not the FQCN... But if you can add one of them and not the other it may come from another issue.

Can you try adding setPosts($posts) and setCategories($categories) in the corresponding classes?

If this does not work, can you show your EasyAdmin configuration?

It doesn't work.

Code:

        $em = $this->getDoctrine()->getManager();

        $c = new Category();
        $c->setName('Category 001');
        $c->setSlug('category-001');
        $c->setContent('category 001');

        $p = new Post();
        $p->setTitle('Post 001');
        $p->setSlug('post-001');
        $p->setSummary('Post 001');
        $p->setContent('Post 001');

        $c->addPost($p);

        $em->persist($c);
        $em->flush();

Result:

Category:
eab_1

Post:
eab_2

config.yml

easy_admin:

    formats:
        date:     'd.m.Y'
        time:     'H:i:s'
        datetime: 'd.m.Y H:i:s'

    list_max_results: 10

    entities:
        User:
            class: AppBundle\Entity\User
        Post:
            class: AppBundle\Entity\Post
        Category:
            class: AppBundle\Entity\Category

I don't know whether it'll solve the issue but I'd suggest changing the addPost and addCategory methods according to this change:

// Category.php

/**
 * @param Post $post
 * @return Category
 */
public function addPost(Post $post)
{
    $this->posts[] = $post;
+   if (!$post->getCategories()->contains($this)) {
+       $post->addCategory($this);
+   }
    return $this;
}
// Post.php

/**
 * @param Category $category
 * @return Post
 */
public function addCategory(Category $category)
{
    $this->categories[] = $category;
+   if (!$category->getPosts()->contains($this)) {
+       $category->addPost($this);
+   }
    return $this;
}

If this really does not work, you could also try to persist both objects instead of just one.

@Pierstoval I've tried.

This code works, but...

        $em = $this->getDoctrine()->getManager();

        $c = new Category();
        $c->setName('Category 001');
        $c->setSlug('category-001');
        $c->setContent('category 001');

        $p = new Post();
        $p->setTitle('Post 001');
        $p->setSlug('post-001');
        $p->setSummary('Post 001');
        $p->setContent('Post 001');

        $c->addPost($p);

        $em->persist($c);
        $em->flush();

Easyadmin does not work. "update query" does not send.

Profiler:

Form Data:
profiler_1

Doctrine Query:
profiler_2

Have you tested persisting such objects _outside_ EasyAdmin? Maybe something else could be debugged from the profiler :confused:

I checked. easy-admin-demo have the same problem. :confused:

I can confirm this before are working and now no

Edit: I am getting this problem with a ManyToOne

So, this occurs always from a select2 multiple

I'm pretty sure this has nothing to do with EasyAdmin itself but the Form component + Doctrine. As my wife gave birth a few days ago I don't have time to "code" to test this, I can just advice you to take a look outside EasyAdmin issue tracker (on StackOverflow for instance) to check what's going on. If you find a solution it could be good for you to say it back here :wink:

@Pierstoval Congratulations :smile:

If I find the solution I share here.

I'm finding the same problem on a M2O relationship.

Hmmm after some investigation, doesn't seem like add is being called on the related entity, leaving the child entity unlinked. I wonder if this is a doctrine issue.

Overriding the admin controller to add a preUpdateEntityNameEntity where I manually link child entity to parent works, like so

    // src/AppBundle/AdminController.php

    /**
     * Ensure relations are saved.
     * 
     * @param PortfolioItem $portfolioItem
     */
    public function preUpdatePortfolioItemsEntity(PortfolioItem $portfolioItem)
    {
        foreach ($portfolioItem->getSlideshowItems() as $slideshowItem) {
            /** @var SlideshowItem $slideshowItem */
            $slideshowItem->setPortfolioItem($portfolioItem);
        }
    }

Without, both entities aren't linked, even though each entity:

    // Entity\PortfolioItem
    /**
     * @param SlideshowItem $slideshowItem
     *
     * @return self
     */
    public function addSlideshowItem(SlideshowItem $slideshowItem) : self
    {
        $this->slideshowItems[] = $slideshowItem;
        $slideshowItem->setPortfolioItem($this);

        return $this;
    }

and

    // Entity\SlideshowItem
    /**
     * @param mixed $portfolioItem
     *
     * @return self
     */
    public function setPortfolioItem(PortfolioItem $portfolioItem) : self
    {
        $this->portfolioItem = $portfolioItem;

        if ($portfolioItem->getSlideshowItems()->contains($this) === false) {
            $portfolioItem->addSlideshowItem($this);
        }

        return $this;
    }

You need to set by_reference option to false in collection form in Category:

- { property: 'posts', type_options: { by_reference: false} }

and also as @Pierstoval said modify Category entity:

// Category.php

    /**
     * Add posts
     *
     * @param \AppBundle\Entity\Post $posts
     * @return Category
     */
    public function addPost(\AppBundle\Entity\Post $posts)
    {
        if (!$this->posts->contains($posts)) {
            $this->posts[] = $posts;
            $posts->addCategory($this);
        }

        return $this;
    }

    /**
     * Remove posts
     *
     * @param \AppBundle\Entity\Post $posts
     */
    public function removePost(\AppBundle\Entity\Post $posts)
    {
        $this->posts->removeElement($posts);
        $posts->removeCategory($this);
    }

this is said in documentation http://symfony.com/doc/current/cookbook/form/form_collections.html

A second potential issue deals with the Owning Side and Inverse Side of Doctrine relationships. In this example, if the "owning" side of the relationship is "Task", then persistence will work fine as the tags are properly added to the Task. However, if the owning side is on "Tag", then you'll need to do a little bit more work to ensure that the correct side of the relationship is modified.

The trick is to make sure that the single "Task" is set on each "Tag". One easy way to do this is to add some extra logic to addTag(), which is called by the form type since by_reference is set to false:

Problem is solved. @javierrodriguezcuevas Thanks :)

Thought I would just add this here, as I was curious about the by_reference type option:

Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.

http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

@javierrodriguezcuevas Thanks! ;)

Was this page helpful?
0 / 5 - 0 ratings