2

Spoiler: I think I found the answer but I'm not 100% sure ;)

I've been looking at this question for a while but I cannot manage to make it work. So I've create dummies Entities to test the relation and here they are:

  • A Product can be in many Cart
  • A Cart can contains several Product
  • The Product in a Cart are order by a position

Product

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

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

    /**
     * @ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="product", cascade={"all"})
     */
    protected $productCarts;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->productCarts = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

    /**
     * Add productCarts
     *
     * @param \Acme\DemoBundle\Entity\CartHasProduct $productCarts
     * @return Product
     */
    public function addProductCart(\Acme\DemoBundle\Entity\CartHasProduct $productCarts)
    {
        $this->productCarts[] = $productCarts;

        return $this;
    }

    /**
     * Remove productCarts
     *
     * @param \Acme\DemoBundle\Entity\CartHasProduct $productCarts
     */
    public function removeProductCart(\Acme\DemoBundle\Entity\CartHasProduct $productCarts)
    {
        $this->productCarts->removeElement($productCarts);
    }

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

Cart

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

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

    /**
     * @ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="cart", cascade={"all"})
     */
    protected $cartProducts;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->cartProducts = new \Doctrine\Common\Collections\ArrayCollection();
    }

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

    /**
     * Add cartProducts
     *
     * @param \Acme\DemoBundle\Entity\CartHasProduct $cartProducts
     * @return Cart
     */
    public function addCartProduct(\Acme\DemoBundle\Entity\CartHasProduct $cartProducts)
    {
        $this->cartProducts[] = $cartProducts;

        return $this;
    }

    /**
     * Remove cartProducts
     *
     * @param \Acme\DemoBundle\Entity\CartHasProduct $cartProducts
     */
    public function removeCartProduct(\Acme\DemoBundle\Entity\CartHasProduct $cartProducts)
    {
        $this->cartProducts->removeElement($cartProducts);
    }

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

and finally CartHasProduct reference table

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

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

    /**
     * @ORM\ManyToOne(targetEntity="Cart", inversedBy="productCarts")
     */
    protected $cart;

    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="cartProducts")
     */
    protected $product;

    /**
     * @ORM\Column(type="integer")
     */
    protected $position;

    public function __construct(Cart $cart, Product $product, $position=0) {
        $this->cart = $cart;
        $this->product = $product;
        $this->setPosition($position);
    }

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

    /**
     * Set position
     *
     * @param integer $position
     * @return CartHasProduct
     */
    public function setPosition($position)
    {
        $this->position = $position;

        return $this;
    }

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

    /**
     * Set cart
     *
     * @param \Acme\DemoBundle\Entity\Cart $cart
     * @return CartHasProduct
     */
    public function setCart(\Acme\DemoBundle\Entity\Cart $cart = null)
    {
        $this->cart = $cart;

        return $this;
    }

    /**
     * Get cart
     *
     * @return \Acme\DemoBundle\Entity\Cart 
     */
    public function getCart()
    {
        return $this->cart;
    }

    /**
     * Set product
     *
     * @param \Acme\DemoBundle\Entity\Product $product
     * @return CartHasProduct
     */
    public function setProduct(\Acme\DemoBundle\Entity\Product $product = null)
    {
        $this->product = $product;

        return $this;
    }

    /**
     * Get product
     *
     * @return \Acme\DemoBundle\Entity\Product 
     */
    public function getProduct()
    {
        return $this->product;
    }
}

I've created the Entities manually, adding the @ORM annotations to setup the relationship and then I've used app/console generate:doctrine:entities AcmeDemoBundle to populate the getter, setter and __construct

Now I a controller I have to following code:

<?php

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class WelcomeController extends Controller
{
    public function indexAction()
    {
        // Create a Cart Entity
        $cart = new \Acme\DemoBundle\Entity\Cart();

        // Create a Product Entity
        $product = new \Acme\DemoBundle\Entity\Product();

        // Add the Product into the Cart
        $cart->getCartProducts()->add($product);

        // Save the Cart
        $em = $this->getDoctrine()->getManager();
        $em->persist($cart);
        $em->flush();

        return $this->render('AcmeDemoBundle:Welcome:index.html.twig');
    }
}

Doing so I have the following error coming up:

Found entity of type Acme\DemoBundle\Entity\Product on association Acme\DemoBundle\Entity\Cart#cartProducts, but expecting Acme\DemoBundle\Entity\CartHasProduct

So my question is how to add a Product into a Cart? Do I need to create the relation Object manually (CartHasProduct)? I would think Doctrine would have done it. I looked everywhere on Doctrine documentation and I could not find an exemple of relationship with extra field.

I've also looked into the tests in the vendor, there is plenty of model (very interesting) but nothing with extra field in relationship.

I was thinking the create my own method in Cart like this:

public function addProduct(Product $product, $position=0) {
    $relation = new CartHasProduct($this, $product, $position);
    if (!$this->cartProducts->contains($relation)) {
        $this->cartProducts->add($relation);
    }
}

But I'd like to know if I need to implement it or if it's meant to be handled automatically?

#### UPDATE 1 ####

So I ended up adding this method addProduct. The problem is that contains() is not working as expected. So I tried to delete all Product from the Cart and add a new one.

Here is my function to delete the products:

/**
 * Reset the product for the cart
 *
 * @return bool
 */
public function resetCart() {
    foreach ($this->getCartProducts() as $relation) {
        $relation->getProduct()->removeProductCart($relation);
        $this->removeCartProducts($relation);
    }
}

and here is how I call it:

    $em = $this->getDoctrine()->getManager();
    $cart->resetCart();
    $em->persist($cart);
    $em->flush();

But the records are not deleted in CartHasProduct table.

UPDATE 2

I found what was the problem, you need to add orphanRemoval=true in the OneTwoMany relation (on both side) if you want to delete the relationship between the 2 main Entity (Cart and Product):

/**
 * @ORM\Entity
 * @ORM\Table(name="demo_product")
 */
class Product
{
...

    /**
     * @ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="product", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    protected $productCarts;

And

/**
 * @ORM\Entity
 * @ORM\Table(name="demo_cart")
 */
class Cart
{
    ...

    /**
     * @ORM\OneToMany(targetEntity="CartHasProduct", mappedBy="cart", cascade={"persist", "remove"}, orphanRemoval=true)
     */
    protected $cartProducts;

    ...

    /**
     * Reset the product for the cart
     */
    public function resetCart() {
        $this->getCartProducts()->clear();
    }

Cheers,

Maxime

Community
  • 1
  • 1
maxwell2022
  • 2,818
  • 5
  • 41
  • 60

1 Answers1

2

Well many to many association with extra parameters can be implemented by using a third intermediate entity. You have the right approach, but wrong associations defined. Here's how it should be.

Taking your 3 entities Product, Cart, CartProducts

Cart should have a one-to-many relationship with CartProducts

CartProducts should have many-to-one relationship to Product and Many-to-one association with Cart

So you first initialize a Cart, and add products to Cart like this:

For every Product

  • initialize a CartProduct with the Product and Cart and other extra parameters you need.
  • Add it to the Cart
Broncha
  • 3,794
  • 1
  • 24
  • 34