Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.3.3
Subscribe to download the code!Compatible PHP versions: >=5.3.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
ManyToMany Relationship
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
ManyToMany Relationship¶
I want you to attend my event! So, you are going to need to be able to RSVP.
Adding a ManyToMany Relationship¶
First, think about how this would be stored in the database. One user should be able to attend many events, and one event will have many attendees. This is a classic ManyToMany relationship between the Event and User entities.
We already added a ManyToOne relationship earlier and adding a ManyToMany will be very similar.
To model this, create a new attendees property on Event that’ll hold an array of Users that can’t wait to go:
// src/Yoda/EventBundle/Entity/Event.php
// ...
protected $attendees;
Like with a ManyToOne, we just need an annotation that tells Doctrine what type of association this is and what entity it relates to:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToMany(targetEntity="Yoda\UserBundle\Entity\User")
*/
protected $attendees;
Whenever you have a relationship that holds multiple things, you need to add a __construct method and initialize it to an ArrayCollection:
// src/Yoda/EventBundle/Entity/Event.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
// ...
public function __construct()
{
$this->attendees = new ArrayCollection();
}
We saw this on the User.events property earlier when we added the OneToMany association.
Next, we’ll add a getter method only - I’ll explain why the setter isn’t needed in a moment:
// src/Yoda/EventBundle/Entity/Event.php
// ...
public function getAttendees()
{
return $this->attendees;
}
And that’s it! Let’s dump the schema update to see how this will change our database:
php app/console doctrine:schema:update --dump-sql
CREATE TABLE event_user (
event_id INT NOT NULL,
user_id INT NOT NULL,
INDEX IDX_92589AE271F7E88B (event_id),
INDEX IDX_92589AE2A76ED395 (user_id),
PRIMARY KEY(event_id, user_id))
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
ALTER TABLE event_user
ADD CONSTRAINT FK_92589AE271F7E88B FOREIGN KEY (event_id)
REFERENCES yoda_event (id) ON DELETE CASCADE;
ALTER TABLE event_user
ADD CONSTRAINT FK_92589AE2A76ED395 FOREIGN KEY (user_id)
REFERENCES yoda_user (id) ON DELETE CASCADE;
Doctrine is smart enough to know that we need a new “join table” that has event_id and user_id properties. When we relate an Event to a User, it’ll insert a new row in this table for us. Doctrine will handle all of those ugly details.
Re-run the command with --force to add the table:
php app/console doctrine:schema:update --force
The Optional JoinTable¶
With a ManyToMany, you can optionally add a JoinTable annotation. Add this only if you want to customize something about the join table. For example, you can control the onDelete behavior that happens if a User or Event is deleted:
// src/Yoda/EventBundle/Entity/Event.php
// ...
/**
* @ORM\ManyToMany(targetEntity="Yoda\UserBundle\Entity\User")
* @ORM\JoinTable(
* joinColumns={@ORM\JoinColumn(onDelete="CASCADE")},
* inverseJoinColumns={@ORM\JoinColumn(onDelete="CASCADE")}
* )
*/
protected $attendees;
Run the doctrine:schema:update command again.
php app/console doctrine:schema:update --dump-sql
Actually, no changes are needed: Doctrine uses this onDelete behavior by default.
11 Comments
Nice catch! Fixed at https://github.com/knpunive.... Thanks!
Hey!
Why exactly do you need to create this __construct function? Could you elaborate a little bit further on this? Thanks!
Hey Max,
I assume you already know what's the __construct() function do in PHP classes and I think you're just wondering about "$this->attendees = new ArrayCollection();" in it, right? )
So if answer on it simply - it just allows you to avoid some unexpected bugs, i.e. just imagine, the getAttendees() method should always return an instance of ArrayCollection, so when you call some method on it like `$event->getAttendees()->count()` - you will get an error "Cannot call method count() on null" because getAttendees() returns null by default if we don't set it to a new ArrayCollection() in constructor. And one more thing - in PHP you can set only scalar values or array on fields initializing, i.e. "private $attendees = array();", but if you need to set an object by default - constructor is the only right place where you can do it.
Cheers!
Hey Victor!
Thank you very much for your helpful explanation ! :)
Hi Ryan,
I am stuck at a problem that in theory sounds pretty straight forward but in Symfony I am not able to implement it so I am hoping you can push me in right direction. I will explain first what I am trying to do and let you know how far did I get trying to achieve this. Just for your information I am using Symfony 3.0
<strong>What I am trying to do</strong>
1) I have two Entity 'Post' and 'Category' and they are linked together in ManytoMany relationship bidirectional.
2) When I am creating a new post, I want to assign it a category which should be checkboxes and linked with Category Entity
3) After saving the post, when I edit it the category checkboxes should appear as selected (those categories that are assigned to the post)
<strong>What I have done so far</strong>
Entity are already linked and the relationship seems to be working fine because when i save the post it gets saved and the join table of category and entity gets updated as well with post id and category id. If i select multiple categories they get saved as well in the join table.
<strong>What i am stuck at</strong>
When I edit the post, the post data appears fine but the category check boxes do not appear as selected and this is where i need your help.
<strong>How i created the form.</strong>
I am not sure if this is the right way to do it but it seems to be working, so if i am wrong somewhere and not doing it right the symfony way please do correct me.
The <I>CategoryType</I> is straight forward
class CategoryType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name');
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Category'
));
}
}
Here is the <I>PostType</I>, this is where i have doubt if i have done it right.
class PostType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('featuredImage', FileType::class, array(
'data_class' => null,
'constraints' => array(new Image(
array(
'mimeTypes' => array(
"image/jpeg",
"image/jpg",
"image/png",
"image/x-png",
"image/pjpeg"
),
'mimeTypesMessage' => "Only jpeg and png images are allowed",
'maxHeight' => '600',
'maxWidth' => '600'
)))))
->add('category', EntityType::class, array(
'class' => 'MyBundle\Entity\Category',
'choice_label' => 'name',
'multiple' => true,
'expanded' => true,
'by_reference' => false,
))
->add('categories', CollectionType::class, array(
'entry_type' => CategoryType::class,
'by_reference' => false
));;
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyBundle\Entity\Post'
));
}
}
In order for me to display the already created categories i created a new property in Post entity with name 'Category' which in the <I>PostType</I> is pulling the categories from Category Entity.
Then there is categories CollectionType as well. When the form is submitted i use the loop to add categories in the join table.
$catagories = $form->get('category')->getData();
foreach ($catagories as $cat) {
$post->getCategories()->add($cat);
}
Doing this does save the information in join table but when I edit it the checkboxes do not appear as selected. Another strange thing I noticed if i remove the ->add('categories', CollectionType::class from the <i>PostType</i> I get the following error which is really wierd because these values DONT exist in the table so i am not sure why it says they are duplicate
An exception occurred while executing 'INSERT INTO posts_categories (post_id, category_id) VALUES (?, ?)' with params [30, 1]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '30-1' for key 'PRIMARY'
This is manyTomany from post.orm.yml
manyToMany:
categories:
targetEntity: MyBundle\Entity\Category
inversedBy: posts
joinTable:
name: posts_categories
joinColumns:
post_id:
referencedColumnName: id
inverseJoinColumns:
category_id:
referencedColumnName: id
cascade: ["persist"]
This is manyTomany from category.orm.yml
manyToMany:
posts:
targetEntity: MyBundle\Entity\Post
mappedBy: categories
joinTable:
name: posts_categories
joinColumns:
post_id:
referencedColumnName: id
inverseJoinColumns:
category_id:
referencedColumnName: id
I will really appreciate if you can let me know what I am doing wrong here.
Hi Shairyar!
Wow, what a web of complexity indeed :). Great explanation, but I'm a little confused on the goal. From what I see (in your code), you want to:
A) The "categories" field tells me that you want to *embed* (meaning, actually show the name of each category as an editable field) all of the existing categories that a post has. This is a *little* weird from a user's perspective already, because if I update the name of one of its categories, it will rename that category across the entire system.
B) The "category" field tells me that you *also* want to show all of the categories in the system as checkboxes.
So when you submit, you (of the form, in your controller) need to figure out what categories the Post should have. But with these two fields, it just gets a little strange. I might rename a category thanks to the "categories" field, but then un-check that box in the "category" field. So now... I guess this Category should be renamed in the database, but unlinked from the Post? You can see how it gets confusing :).
In your original "What I am trying to do", it sounds like you simply want a list of Category checkboxes on your Post form that the user can select. If this is the case, then you should:
1) Keep the categories field, but configure it like your category field (e.g. make it an EntityType + the other options)
2) Remove the category field
3) Remove that code in your controller that gets the "category" form data and loops over it - not needed anymore.
And that's it! The EntityType is *built* to be able to render checkboxes for a ManyToMany field, as long as you have multiple=>true and expanded => true.
P.S. You can also remove the "joinTable" from your category.orm.yml file - the joinTable config is only read on the *owning* side of the relationship (Post in this case). It's ignored on the other side. This mapping stuff can be tricky with Doctrine :).
I hope this guides you :)
wow, you are great....
Following the 3 steps made it work :) and I did not know that The EntityType is *built* to be able to render checkboxes for a ManyToMany field
Regarding the explanation you got me right the following statement was what i meant
In your original "What I am trying to do", it sounds like you simply want a list of Category checkboxes on your Post form that the user can select.
Many thanks, i am so glad to come across knp....
Hello There,
Is it possible to set multiple ManToMany Relationships on the same Model/Entity. For Example, considering the current scenario, I may have multiple hosts and multiple attendees to an event. In this case, would i need the above or there is some better option, Thanks in advance.
Hey Junaid Farooq
Yes, you can. In your scenario you would have an "Event" entity with two fields, hosts and attendees, each one would have a ManyToMany relationship with the correspondent entity (Host, Attendee)
I hope it helps even a bit. Cheers!
Thanks MolloKhan
I was thinking of the same solution.
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "~2.4", // v2.4.2
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.0.1
"symfony/assetic-bundle": "~2.3", // v2.3.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.5
"symfony/monolog-bundle": "~2.4", // v2.5.0
"sensio/distribution-bundle": "~2.3", // v2.3.4
"sensio/framework-extra-bundle": "~3.0", // v3.0.0
"sensio/generator-bundle": "~2.3", // v2.3.4
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
"ircmaxell/password-compat": "~1.0.3", // 1.0.3
"phpunit/phpunit": "~4.1", // 4.1.0
"stof/doctrine-extensions-bundle": "~1.1.0" // v1.1.0
}
}
There is a header "AJAX and JSON Responses"
Looks it is something that should be changed because there is nothing about AJAX or JSON