Buy

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.

Leave a comment!

  • 2016-01-30 Shairyar Baig

    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....

  • 2016-01-30 weaverryan

    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 :)

  • 2016-01-30 Shairyar Baig

    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

    What I am trying to do
    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)

    What I have done so far
    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.

    What i am stuck at
    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.

    How i created the form.
    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 CategoryType 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 PostType, 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 PostType 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 PostType 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.

  • 2015-09-11 weaverryan

    Nice catch! Fixed at https://github.com/knpuniversi.... Thanks!

  • 2015-09-11 guest

    There is a header "AJAX and JSON Responses"
    Looks it is something that should be changed because there is nothing about AJAX or JSON