Buy

Using the ManyToMany so Users can Attend an Event

UPGRADE! Check out the newest version of this tutorial

Using the ManyToMany so Users can Attend an Event

Let’s put our new relationship into action. Create two new routes next to our other event routes: one for attending an event and another for unattending:

# src/Yoda/EventBundle/Resources/config/routing/event.yml
# ...

event_attend:
    pattern:  /{id}/attend
    defaults: { _controller: "EventBundle:Event:attend" }

event_unattend:
    pattern:  /{id}/unattend
    defaults: { _controller: "EventBundle:Event:unattend" }

Next, hop into the EventController and create the two corresponding action methods:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function attendAction($id)
{

}

public function unattendAction($id)
{

}

Start with attendAction. The logic here should feel familiar. First, query for an Event entity. Next, throw a createNotFoundException if no Event is found:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function attendAction($id)
{
    $em = $this->getDoctrine()->getManager();
    /** @var $event \Yoda\EventBundle\Entity\Event */
    $event = $em->getRepository('EventBundle:Event')->find($id);

    if (!$event) {
        throw $this->createNotFoundException('No event found for id '.$id);
    }

    // ... todo
}

All we need to do now is add the current User object as an attendee on this Event. Remember that the attendees property is actually an ArrayCollection object. Use its add method then save the Event. Finally, redirect when you’re finished:

// src/Yoda/EventBundle/Controller/EventController.php
// ...

public function attendAction($id)
{
    $em = $this->getDoctrine()->getManager();
    /** @var $event \Yoda\EventBundle\Entity\Event */
    $event = $em->getRepository('EventBundle:Event')->find($id);

    if (!$event) {
        throw $this->createNotFoundException('No event found for id '.$id);
    }

    $event->getAttendees()->add($this->getUser());

    $em->persist($event);
    $em->flush();

    $url = $this->generateUrl('event_show', array(
        'slug' => $event->getSlug(),
    ));

    return $this->redirect($url);
}

Notice that we just added an attendee without needing a setAttendees method on Event. This works because attendees is an object, so we can just call getAttendees and then modify it.

Printing Attendees in Twig

Before we try this out, let’s update the event show page. Use the length filter to count the number of attendees, to make sure we make enough guacamole:

{# src/Yoda/EventBundle/Resources/views/Event/show.html.twig #}
{# ... #}

<dt>who:</dt>
<dd>
    {{ entity.attendees|length }} attending!

    <ul class="users">
        <li>nobody yet!</li>
    </ul>
</dd>

We can even loop over the event’s attendees and print each of them out. Print a nice message when nobody’s attending, using Twig’s really nice for-else functionality:

{# src/Yoda/EventBundle/Resources/views/Event/show.html.twig #}
{# ... #}

<dt>who:</dt>
<dd>
    {{ entity.attendees|length }} attending!

    <ul class="users">
        {% for attendee in entity.attendees %}
            <li>{{ attendee }}</li>
        {% else %}
            <li>nobody yet!</li>
        {% endfor %}
    </ul>
</dd>

Now help me add a link to the new event_attend route if the user is logged in:

{# src/Yoda/EventBundle/Resources/views/Event/show.html.twig #}
{# ... #}

<dt>who:</dt>
<dd>
    {# ... #}

    <a href="{{ path('event_attend', {'id': entity.id}) }}" class="btn btn-success btn-xs">
        I totally want to go!
    </a>
</dd>

Testing out the Relationship

Head over to an event in your browser. It says 0 attending. Now click the new link. After the redirect, we see 1 attending, but we also see a huge error:

Catchable Fatal Error: Object of class YodaUserBundleEntityUser could not be converted to string

The fact that we show 1 attending means that the database relationship was stored correctly. We can prove it by querying for the join table:

php app/console doctrine:query:sql "SELECT * FROM event_user"

Yep, we see one row that links our user to this event.

Adding a __toString to User

So what’s the error? Look closely: PHP is trying to convert our User object into a string. This is happening because we’re looping over event.attendees, which gives us User objects that we’re printing:

{# src/Yoda/EventBundle/Resources/views/Event/show.html.twig #}

{% for attendee in entity.attendees %}
    <li>{{ attendee }}</li>
{% else %}
    <li>nobody yet!</li>
{% endfor %}

We have two options to fix this. First, we could just print out a specific property on the User:

{# src/Yoda/EventBundle/Resources/views/Event/show.html.twig #}

{% for attendee in entity.attendees %}
    <li>{{ attendee.username }}</li>
{% else %}
    <li>nobody yet!</li>
{% endfor %}

But if you do just want to print the object, you can add a __toString method to the User class:

// src/Yoda/UserBundle/Entity/User.php
// ...

public function __toString()
{
    return (string) $this->getUsername();
}

Refresh now. Sweet, no errors!

Let’s also take a second and fill in the # of attendees on the index page:

{# src/Yoda/EventBundle/Resources/views/Event/index.html.twig #}
{# ... #}

{% for entity in entities %}
    {# ... #}

    <dt>who:</dt>
    <dd>{{ entity.attendees|length }} attending!</dd>

    {# ... #}
{% endfor %}

Leave a comment!

  • 2016-08-11 weaverryan

    Hi Daria!

    Phew! So, this stuff is tough :). Fortunately, I think you understand it perfectly - it looks like it's actually Doctrine's docs, which are wrong/misleading. If you read the paragraph below the code-block that you linked to, it says that their implementation is still not really "complete". Specifically, their Comment::addUserFavorite() just won't work (from a persistence standpoint). But, why are they calling $comment->addUserFavorite($this) from User? I think they're doing that *not* for persistence, just to try to keep the Comment and the User "in sync" for the rest of the request. That's not something I usually worry about, unless I *know* that I'll be working with the Comment later in my code, on this request.

    Does that help? Cheers!

  • 2016-08-11 Daria

    Hi Ryan,

    I'm a little confused by the association management methods of ManyToMany relationships in Doctrine. If I understand correctly, the reverse calls need to be done in the setters of the inverse side, not in the owning side ones. For example if we have the User as the owning side for the `User::$favorites` property, then `User::addFavorite($comment)` will persist fine, but in `Comment::addUserFavorite($user)` needs to invoke `$user->addFavorite($this)` additionally in its setter.

    ```
    class User {
    //...
    /**
    * Bidirectional - Many users have Many favorite comments (OWNING SIDE)
    *
    * @ManyToMany(targetEntity="Comment", inversedBy="userFavorites")
    * @JoinTable(name="user_favorite_comments")
    */
    private $favorites;

    public function addFavorite(Comment $comment)
    {
    $this->favorites[] = $comment;
    return $this;
    }
    ```

    ```
    class Comment {
    //...
    /**
    * Bidirectional - Many comments are favorited by many users (INVERSE SIDE)
    *
    * @ManyToMany(targetEntity="User", mappedBy="favorites")
    */
    private $userFavorites;

    public function addUserFavorite(User $user)
    {
    $this->userFavorites[] = $user;
    $user->addFavorite($this);
    return $this;
    }
    ```

    Why is it then that in the Doctrine doc http://docs.doctrine-project.o... they do it the other way around? Isn't it mandatory to put the stuff on the inverse side of the association?

    Thanks for your awesome work!

  • 2016-05-20 Shairyar Baig

    Thanks Ryan :) was going nuts over it.

  • 2016-05-19 weaverryan

    Ah, I know what that is! It's recursion! On the owning side (i.e. Interest), in removeUser(), remove the code that updates the inverse side - i.e. $user->removeInterest($this). This line is causing a recursive loop!

  • 2016-05-19 Shairyar Baig

    Hi Ryan,

    Thanks for getting back and thats what I thought too, the moment i make that change and then try to remove the interest via form the app crashes and gives me HTTP ERROR 500 there is no symfony error that generally appears in dev environment to let me know what went wrong so I am pretty clueless as to why this is happening.

    I have to then shut everything down and restart in order to get back into the app.

    Baig

  • 2016-05-19 weaverryan

    Hi Shairyrar!

    Since you've created a form where you can select several interests to be added to your User, it means that the form is calling User::addInterest() and User::removeInterest(). Unfortunately, this is the *inverse* side of the relationship - so Doctrine is completely ignoring it! So, why does adding work? Because in User::addInterest, you've cleverly set the *owning* side by having

    $interest->addUser($this)

    To fix the problem, you need to also update the owning side in User::removeInterest:



    public function removeInterest(\AppBundle\Entity\Interest $interest)
    {
    $interest->removeUser($this);
    $this->interest->removeElement($interest);
    }

    That will likely fix your issue :)

  • 2016-05-17 Shairyar Baig

    Hi Ryan,

    I am in a situation where i am unable to delete the association between manytomany relationship thats kept in the third auto generate table.

    A user can select as many interest tags as they like and on saving these interest tags get linked with the user its only the delete part that i am not able to wrap my head around.



    manyToMany:
    interest:
    targetEntity: AppBundle\Entity\Interest
    mappedBy: user




    manyToMany:
    user:
    targetEntity: AppBundle\Entity\User
    inversedBy: interest


    Inside User Entity I have




    /**
    * Add interest
    *
    * @param \AppBundle\Entity\Interest $interest
    *
    * @return User
    */

    public function addInterest(\AppBundle\Entity\Interest $interest)
    {
    $interest->addUser($this);
    $this->interest[] = $interest;
    return $this;
    }

    /**
    * Remove interest
    *
    * @param \AppBundle\Entity\Interest $interest
    */
    public function removeInterest(\AppBundle\Entity\Interest $interest)
    {
    $this->interest->removeElement($interest);
    }




    Inside the Interest Entity I have



    /**
    * Add user
    *
    * @param \AppBundle\Entity\User $user
    *
    * @return Interest
    */

    public function addUser(\AppBundle\Entity\User $user)
    {
    $this->user[] = $user;
    return $this;
    }

    /**
    * Remove user
    *
    * @param \AppBundle\Entity\User $user
    */
    public function removeUser(\AppBundle\Entity\User $user)
    {
    $this->user->removeElement($user);
    $user->removeInterest($this);
    }


    On adding the persist works fine, but if i remove any of the interest tags it does not get removed, what am i missing here?

  • 2015-09-14 weaverryan

    Thanks! I just tweaked some code blocks to correct things or make them more clear: https://github.com/knpuniversi....

  • 2015-09-14 guest

    It may be a bit confusing for student.

    You have

    {% else %}
    <li>We're cool! RSVP!</li>

    in show.html.twig

    and

    {% else %}
    <li>nobody yet!</li>

    later in the text.

    Looks it should be unified.

    Also at the end of the post you describe index.html.twig change

    <dt>who:</dt>
    <dd>{{ entity.attendees|length }} attending!</dd>

    we have a table row for each Event. There should be instruction what column to use...

    or to do as i prefer: create additional column for this

  • 2015-09-03 weaverryan

    Twig, ftw! :)

  • 2015-09-02 Victor Bocharsky

    Wow, I don't know about for-else syntax before. It's a cool feature! :)

  • 2015-01-21 Diego Aguiar

    Alright, if I find something useful I'll post it here

    Thanks for your answer

  • 2015-01-21 weaverryan

    Hey Diego!

    Hmm, I'm not sure about that, but I would maybe doubt it, because we're telling Doctrine that we basically want to get our event attendee's data, which means it should join all the way over to that table. The middle table is an implementation detail for Doctrine, so I'm not sure this is possible.

    Btw, ultimately, if/when performance becomes a concern and you find that querying through Doctrine is a bottleneck, it's very common to make "raw" DQL queries instead: just query directly for the exact fields you want and skip object hydration. That would give you a much bigger performance boost, but you are just returned an array of data instead of an object (which is why it's faster). Typically I recommend that people use the ORM (like we're doing), then go back later if/when they need to and use the "lower-level" API to make DQL queries if needed.

    Cheers!

  • 2015-01-21 Diego Aguiar

    Great answer! I really appreciatte it ;]
    I'll try to don't engage with this so early, but when it comes to DB performance I can't help it.

    I just have one more question:
    I did what you said, adding a Left Join to my query, the only problem is that Doctrine is executing double Left Join
    1 to Users and 1 to Event_User Table.
    I guess is because Event entity has 2 different relationships (users and event_user)

    Is there any form to specify Doctrine to only add one Left Join ?

    Thanks for your time.

  • 2015-01-21 weaverryan

    Hey Diego!

    Really interesting topic :). Let's think about it. First, we query for the Event entity, and *only* the Event entity. We know nothing about the attendees of that event. Then, when we use the length filter (or the same would be true if we did count($event->getAttendees()) in PHP), Doctrine says "I don't know how many attendees there are, let me query for them).

    There are 2 ways to fix this:

    1) Mark the "attendees" relationship as extra lazy: http://doctrine-orm.readthedoc.... Basically, this will still make a second query, but it will be a quick "COUNT" query - *just* to get the count of the items. Useful if you're commonly counting something, but not actually iterating over them and printing out attendee details (another query would be made if you did this).

    2) Use a JOIN when querying for your Event that fetches the attendee data all at once. For example, from inside of EventRepository, you could have something like:


    public function findEventBySlugWithAttendeesJoins($slug)
    {
    return $this->createQueryBuilder('event')
    ->leftJoin('event.attendees', 'attendee')
    ->addSelect('attendee')
    ->andWhere('event.slug = :slug')
    ->setParameter('slug', $slug)
    ->getQuery()
    ->getOneOrNullResult();
    }

    The Event object this returns will be identical to before - so none of your other code needs to change. But later, when you reference the Event's attendees, you'll notice that there's no second query! Doctrine says "hey, I already know about this event's attendees, so I don't need another query).

    But above all of this, I would give this advice: don't worry too much about this early. I don't worry about extra queries at first. Then later, if/when I have performance issues on some pages, I'll look at how I can improve things. You could easily change your query later to remove these extra queries *if* you ever need to. Chances are, you won't need to - or if you *do* want things to be faster, you'll use some sort of caching that renders a bigger improvement over eliminating the extra query.

    This is actually something I've been thinking about making a small screencast on - great question!

    Cheers!

  • 2015-01-20 Diego Aguiar

    Hi!

    I didn't realize at first but when you use "length" filter it force Doctrine to do an extra query per event. Is there any form to avoid those extra queries ?

    I would like something like a "$countAttendies" property in the Event entity, but I'm just saying

    Thanks in advance.