0

I had a big time trying to figure out how to setup a ManyToOne -> OneToMany relationship with Doctrine 2 and it still not working...

Here is the application behaviour:

  1. A site has Pages
  2. A User can write Comment on a Page

Here are my Entities (simplified):

Comment Entity:

**
 * @ORM\Entity
 * @ORM\Table(name="comment")
 */
class Comment {
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
     protected $id;

    /**
     * Many Comments have One User
     *
     * @ORM\ManyToOne(targetEntity="\Acme\UserBundle\Entity\User", inversedBy="comments")
     */
    protected $user;

    /**
     * Many Comments have One Page
     *
     * @ORM\ManyToOne(targetEntity="\Acme\PageBundle\Entity\Page", inversedBy="comments")
     */
    protected $page;

    ...

    /**
     * Set user
     *
     * @param \Acme\UserBundle\Entity\User $user
     * @return Comment
     */
    public function setUser(\Acme\UserBundle\Entity\User $user)
    {
        $this->user = $user;
        return $this;
    }

    /**
     * Set page
     *
     * @param \Acme\PageBundle\Entity\Page $page
     * @return Comment
     */
    public function setPage(\Acme\PageBundle\Entity\Page $page)
    {
        $this->page = $page;
        return $this;
    }

User Entity:

/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * The User create the Comment so he's supposed to be the owner of this relationship
     * However, Doctrine doc says: "The many side of OneToMany/ManyToOne bidirectional relationships must be the owning
     * side", so Comment is the owner
     *
     * One User can write Many Comments
     *
     * @ORM\OneToMany(targetEntity="Acme\CommentBundle\Entity\Comment", mappedBy="user")
     */
    protected $comments;

    ...

    /**
     * Get Comments
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getComments() {
        return $this->comments ?: $this->comments = new ArrayCollection();
    } 

Page Entity:

/**
 * @ORM\Entity
 * @ORM\Table(name="page")
 */
class Page
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * One Page can have Many Comments
     * Owner is Comment
     *
     * @ORM\OneToMany(targetEntity="\Acme\CommentBundle\Entity\Comment", mappedBy="page")
     */
    protected $comments;

    ...

    /**
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getComments(){
        return $this->comments ?: $this->comments = new ArrayCollection();
    }

I want a bidirectional relationship to be able to get the collection of Comments from the Page or from the User (using getComments()).

My problem is that when I try to save a new Comment, I get an error saying that doctrine is not able to create a Page entity. I guess this is happening because it's not finding the Page (but it should) so it's trying to create a new Page entity to later link it to the Comment entity that I'm trying to create.

Here is the method from my controller to create a Comment:

public function createAction()
    {
        $user = $this->getUser();
        $page = $this->getPage();

        $comment = new EntityComment();
        $form = $this->createForm(new CommentType(), $comment);

        if ($this->getRequest()->getMethod() === 'POST') {
            $form->bind($this->getRequest());
            if ($form->isValid()) {
                $em = $this->getDoctrine()->getManager();

                $comment->setPage($page);
                $comment->setUser($user);

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

                return $this->redirect($this->generateUrl('acme_comment_listing'));
            }
        }

        return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
            'form' => $form->createView()
        ));
    }

I don't understand why this is happening. I've checked my Page object in this controller (returned by $this->getPage() - which return the object stored in session) and it's a valid Page entity that exists (I've checked in the DB too).

I don't know what to do now and I can't find anyone having the same problem :(

This is the exact error message I have:

A new entity was found through the relationship 'Acme\CommentBundle\Entity\Comment#page' that was not configured to cascade persist operations for entity: Acme\PageBundle\Entity\Page@000000005d8a1f2000000000753399d4. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Acme\PageBundle\Entity\Page#__toString()' to get a clue.

But I don't want to add cascade={"persist"} because I don't want to create the page on cascade, but just link the existing one.

UPDATE1:

If I fetch the page before to set it, it's working. But I still don't know why I should.

public function createAction()
        {
            $user = $this->getUser();
            $page = $this->getPage();

            // Fetch the page from the repository
            $page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
               'id' => $page->getId()
            ));

            $comment = new EntityComment();

            // Set the relation ManyToOne
            $comment->setPage($page);
            $comment->setUser($user);

            $form = $this->createForm(new CommentType(), $comment);

            if ($this->getRequest()->getMethod() === 'POST') {
                $form->bind($this->getRequest());
                if ($form->isValid()) {
                    $em = $this->getDoctrine()->getManager();

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

                    return $this->redirect($this->generateUrl('acme_comment_listing'));
                }
            }

            return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
                'form' => $form->createView()
            ));
        }

UPDATE2:

I've ended up storing the page_id in the session (instead of the full object) which I think is a better idea considering the fact that I won't have a use session to store but just the id. I'm also expecting Doctrine to cache the query when retrieving the Page Entity.

But can someone explain why I could not use the Page entity from the session? This is how I was setting the session:

$pages = $site->getPages(); // return doctrine collection
if (!$pages->isEmpty()) {         
    // Set the first page of the collection in session
    $session = $request->getSession();
    $session->set('page', $pages->first());
}
maxwell2022
  • 2,818
  • 5
  • 41
  • 60

1 Answers1

0

Actually, your Page object is not known by the entity manager, the object come from the session. (The correct term is "detached" from the entity manager.) That's why it tries to create a new one.

When you get an object from different source, you have to use merge function. (from the session, from an unserialize function, etc...)

Instead of

// Fetch the page from the repository
            $page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
               'id' => $page->getId()
            ));

You can simply use :

$page = $em->merge($page);

It will help you if you want to work with object in your session.

More information on merging entities here

Pierrickouw
  • 4,644
  • 1
  • 30
  • 29
  • Thanks, I'll have a look tomorrow. But I'm not sure I want to store the full `Page` entity in the session. Never mind, I guess I can use the annotation too? I've seen `cascade={"merge"}`, is that the same? – maxwell2022 Feb 13 '13 at 12:57
  • 1
    So your solution is working, the annotation is not! That's not really handy :( – maxwell2022 Feb 13 '13 at 22:26