Managing Tags in ManyToMany association with Symfony
Symfony: v3.1.3
Doctrine: v2.5.4
One of the things I found challenging when first working with Symfony was form collections. The Symfony documentation on how to embed a collection of forms as well as how to work with Doctrine associations / relations is a great place to get started. However, checking out other use cases people shared online really helped as well, so I’m going to share another example here which I hope might help others.
For the following example, I will be setting up a Product entity, and a Tag entity. One Product can have multiple Tags, and one Tag can have multiple Products (Many To Many). Here’s how I want it to work:
- New Tags can be added when adding/editing a Product.
- No duplicate Tags should be added to the tag table.
- If this Product is deleted later, any Tag which was added with it should remain in the database.
- Use the jQuery Tag-it plugin to add the tags to our form for submission.
Ok, let’s get started with our Product entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"}) */ protected $tags; } |
Then, the Tag entity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// src/AppBundle/Entity/Tag.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Tag { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="tags") */ protected $products; } |
A quick note about this line in my Product.php file:
@ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"})
I decided to pick Product as the owning side in this association, as Tags will only be created when a Product is added or edited. As you can see in the Doctrine documentation you can choose the owning side in a ManyToMany association. You do so by setting “inversedBy” to it, while setting “mappedBy” to the inverse side.
targetEntity is the class which is your target: Tag
inversedBy is the property used in the Tag entity for Products (line 27 in Tag.php): products
cascade={"persist"} will automatically persist any Tags associated to this Product.
As far as the Tag.php file goes, you set Product as its targetEntity , and since Tag will be the inverse side in this association, you set mappedBy to the property in the Product entity used for Tags (line 27 in Product.php): tags
With that setup, you can run this to generate your getters and setters:
$ php bin/console doctrine:generate:entities AppBundle
The final result of both entities should look like this:
Product.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
// src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Product { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Tag", inversedBy="products", cascade={"persist"}) */ protected $tags; /** * Constructor */ public function __construct() { $this->tags = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Product */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Add tag * * @param \AppBundle\Entity\Tag $tag * * @return Product */ public function addTag(\AppBundle\Entity\Tag $tag) { $this->tags[] = $tag; return $this; } /** * Remove tag * * @param \AppBundle\Entity\Tag $tag */ public function removeTag(\AppBundle\Entity\Tag $tag) { $this->tags->removeElement($tag); } /** * Get tags * * @return \Doctrine\Common\Collections\Collection */ public function getTags() { return $this->tags; } } |
Tag.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
// src/AppBundle/Entity/Tag.php namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Tag { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string") */ protected $name; /** * @ORM\ManyToMany(targetEntity="Product", mappedBy="tags") */ protected $products; /** * Constructor */ public function __construct() { $this->products = new \Doctrine\Common\Collections\ArrayCollection(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name * * @return Tag */ public function setName($name) { $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Add product * * @param \AppBundle\Entity\Product $product * * @return Tag */ public function addProduct(\AppBundle\Entity\Product $product) { $this->products[] = $product; return $this; } /** * Remove product * * @param \AppBundle\Entity\Product $product */ public function removeProduct(\AppBundle\Entity\Product $product) { $this->products->removeElement($product); } /** * Get products * * @return \Doctrine\Common\Collections\Collection */ public function getProducts() { return $this->products; } } |
Note that both the $tags and $products properties need to be set as an ArrayCollection in the constructor. This is explained in the Symfony documentation linked to in the beginning of this article on form collections.
Last but not least, we need to update our database by running:
$ php bin/console doctrine:schema:update –force
If all goes well, then you should see a friendly little message like this afterwards:
You should now have 3 tables added to your database:
We will setup our forms in the next article: Setting up Form Collection for Tags in Symfony