Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ManyToMany relation is not saved to database #860

Closed
phpdev opened this issue Feb 5, 2016 · 19 comments
Closed

ManyToMany relation is not saved to database #860

phpdev opened this issue Feb 5, 2016 · 19 comments

Comments

@phpdev
Copy link
Contributor

phpdev commented Feb 5, 2016

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;
    }
}
@mkalisz77
Copy link
Contributor

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);
}      

@Pierstoval
Copy link
Contributor

@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.

@phpdev
Copy link
Contributor Author

phpdev commented Feb 6, 2016

@Pierstoval I updated the issue.

@Pierstoval
Copy link
Contributor

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?

@phpdev
Copy link
Contributor Author

phpdev commented Feb 6, 2016

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

@Pierstoval
Copy link
Contributor

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.

@phpdev
Copy link
Contributor Author

phpdev commented Feb 6, 2016

@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

@phpdev phpdev changed the title ManyToMany Problem ManyToMany relation is not saved to database Feb 6, 2016
@Pierstoval
Copy link
Contributor

Have you tested persisting such objects outside EasyAdmin? Maybe something else could be debugged from the profiler 😕

@phpdev
Copy link
Contributor Author

phpdev commented Feb 8, 2016

I checked. easy-admin-demo have the same problem. 😕

@rubengc
Copy link
Contributor

rubengc commented Feb 11, 2016

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

@Pierstoval
Copy link
Contributor

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 😉

@phpdev
Copy link
Contributor Author

phpdev commented Feb 12, 2016

@Pierstoval Congratulations 😄

If I find the solution I share here.

@luispabon
Copy link

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

@luispabon
Copy link

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;
    }

@zisato
Copy link
Contributor

zisato commented Mar 5, 2016

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:

@phpdev
Copy link
Contributor Author

phpdev commented Mar 5, 2016

Problem is solved. @javierrodriguezcuevas Thanks :)

@ghostal
Copy link

ghostal commented Nov 30, 2016

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

@Glancu
Copy link

Glancu commented Jan 18, 2019

@javierrodriguezcuevas Thanks! ;)

@apphancer
Copy link

I seem to have solved a similar issue that I was having by:

  • Making sure the inversedBy and mappedBy is set up correctly
  • the addCollection() method was present in both Entities (this needs to reflect the field name, e.g. addUser, addCategory etc...)
  • the field is declared in EasyAdmin Controller using the AssociationField
  • the field in the Controller has the by_reference property set to false using setFormTypeOptionIfNotSet('by_reference', false)

This last point is only needed if trying to persist changes when working on an Entity that is the reverse side.

    public function configureFields(string $pageName) : iterable
    {
        return [
            IdField::new('id')
                ->hideOnForm(),
            AssociationField::new('categories')
                ->setFormTypeOptionIfNotSet('by_reference', false)
                ->hideOnIndex(),
        ];
    }
    ````

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants