Quentin Brosse
Quentin Brosse

Reputation: 191

Create a bidirectional many-to-many association with an extra field on doctrine

I work with symfony 3 and doctrine and this is my problem:
I want to create dishes composed of ingredients of different quantity.

The database will look like this:

[ Dish ] <===> [ IngredientDish ] <===> [ Ingredient ]
[------]       [----------------]       [------------]
[- name]       [- Dish          ]       [-name       ]
[      ]       [- Ingredient    ]       [            ]
[      ]       [- quantity      ]       [            ]
[------]       [----------------]       [------------]

This is my code :

Dish.php

/**
 * @ORM\Table(name="dish")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\DishRepository")
 */
class Dish
{

     /**
      * @ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
      *         mappedBy="dish")
      */
     private $ingredientsDish;

     [...]

     public function addIngredientDish(IngredientDish $ingredient)
     {
         $this->ingredientsDish[] = $ingredient;

         return $this;
     }

     public function getIngredientsDish()
     {
         return $this->ingredientsDish;
     }

 }

Ingredient.php

/**
 * @ORM\Table(name="ingredient")
 *    @ORM\Entity(repositoryClass="AppBundle\Repository\IngredientRepository")
 */
 class Ingredient
 {

     /**
      * @ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
      *     mappedBy="ingredient")
      * @Assert\Type(type="AppBundle\Entity\IngredientDish")
      */
     private $ingredientsDish;

     [...]

     public function addIngredientDish(IngredientDish $ingredientDish)
     {
         $this->ingredientDish[] = $ingredientDish;

         return $this;
      }

     public function getingredientsDish()
     {
         return $this->ingredients;
     }

 }

IngredientDish.php

/**
 * @ORM\Table(name="ingredient_dish")
 *    @ORM\Entity(repositoryClass="AppBundle\Repository\IngredientDishRepository")
 */
class IngredientDish
{

    /**
     * @ORM\Column(name="quantity", type="smallint")
     * @Assert\NotBlank()
     * @Assert\Length(min=1)
     */
    private $quantity = 1;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Ingredient",
     *      inversedBy="ingredientsDish")
     * @Assert\Type(type="AppBundle\Entity\Ingredient")
     */
    private $ingredient;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Dish",
     *      inversedBy="ingredientsDish")
     * @ORM\JoinColumn()
     * @Assert\Type(type="AppBundle\Entity\Dish")
     */
    private $dish;

    public function __construct(Ingredient $ingredient, Dish $dish, $quantity = 1)
    {
        $this->setIngredient($ingredient);
        $this->setDish($dish);
        $this->setQuantity($quantity);
    }

    public function setQuantity($quantity)
    {
        $this->quantity = $quantity;

        return $this;
    }

    public function getQuantity()
    {
        return $this->quantity;
    }

    public function setIngredient(Ingredient $ingredient)
    {
        $this->ingredient = $ingredient;
        return $this;
    }

    public function getIngredient()
    {  
        return $this->ingredient;
    }

    public function setDish(Dish $dish)
    {
        $this->dish = $dish;
        return $this;
    }

    public function getDish()
    {
        return $this->dish;
    }

}

My test code

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

//Get an apple pie
$dish = $em->getRepository('AppBundle:Dish')->find(6);

//Get an apple
$ingredient = $em->getRepository('AppBundle:Ingredient')->find(11);

$quantityApple = 5;

$ingredientDish = new IngredientDish($ingredient, $dish, $quantityApple);

$ingredient->addIngredientDish($ingredientDish);

$dish->addIngredientDish($ingredientDish);

$em->persist($ingredientDish);
$em->persist($dish);
$em->flush();

After execution, i have an interesting entry:

mysql> select * from ingredient_dish;
+----+---------------+----------+---------+
| id | ingredient_id | quantity | dish_id |
+----+---------------+----------+---------+
| 1  |            11 |        5 |       6 |
+----+---------------+----------+---------+

But after, if I try to get my dish:

$dish = $em->getRepository('AppBundle:Dish')->find(6);
dump($dish->getIngredientsDish());

It has no ingredients :

PersistentCollection {#1180 ▼
    -snapshot: []
    -owner: Dish {#1146 ▶}
    -association: array:15 [ …15]
    -em: EntityManager {#1075 …11}
    -backRefFieldName: "dish"
    -typeClass: ClassMetadata {#1157 …}
    -isDirty: false
    #collection: ArrayCollection {#1181 ▼
        -elements: [] <<<<<<<<<<<<<<<<<<<<< EMPTY
    }
    #initialized: false
}

The database is not empty after the execution of my test code, so I think there is an error of getter. Can you help me, do you see something false ?

Thanks you for your help ! :)

Upvotes: 2

Views: 90

Answers (2)

Jakub Matczak
Jakub Matczak

Reputation: 15686

I think that everything is fine, but you got mislead by lazy loading which is apparently smarter than you think it is. ;-)

When you do

$dish->getIngredientsDish();

you receive PersistentCollection which extends AbstractLazyCollection.

But the collection is still not fetched from DB(!)

Take a look closer into your var_dump result

PersistentCollection {#1180 ▼
    -snapshot: []
    -owner: Dish {#1146 ▶}
    -association: array:15 [ …15]
    -em: EntityManager {#1075 …11}
    -backRefFieldName: "dish"
    -typeClass: ClassMetadata {#1157 …}
    -isDirty: false
    #collection: ArrayCollection {#1181 ▼
        -elements: [] <<<<<<<<<<<<<<<<<<<<< EMPTY //<= yeah empty, but...
    }
    #initialized: false // <= it's still not initialized!
}

As you can see there's initialized property which says that the collection is still not initialized (not fetched form DB).

Just try to use it. It will fetch the collection on first usage.

Upvotes: 1

Alvin Bunk
Alvin Bunk

Reputation: 7764

First, there are some problems with you annotations mappings. Make the follow changes:

/**
 * @ORM\Table(name="dish")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\DishRepository")
 */
class Dish
{
    /**
     * @ORM\Column(name="dish_id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $dish_id;

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\IngredientDish",
     *         mappedBy="dish")
     */
    private $ingredientsDish = null;

    public function __construct() {
         $this->ingredientsDish = new ArrayCollection();

    }
    ...
}

/**
 * @ORM\Table(name="ingredient_dish")
 *    @ORM\Entity(repositoryClass="AppBundle\Repository\IngredientDishRepository")
 */
class IngredientDish
{
    ...

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\Dish",
     *      inversedBy="ingredientsDish")
     * @ORM\JoinColumn(name="dish_id", referencedColumnName="country_id")
     * @Assert\Type(type="AppBundle\Entity\Dish")
     */
    private $dish;
    ...
}

In the Dish entity you need to have a constructor for the ArrayCollection, and set it to null.

You can also take a look at this other post of mine (on stackoverflow for a reference:

Doctrine Entities Relations confusing

Upvotes: 0

Related Questions