Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

More with ManyToMany: Avoiding Duplicates

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

More with ManyToMany: Avoiding Duplicates

Now click the attend link again. Ah, an error!

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ‘4-4’ for key ‘PRIMARY’

Our User is once again added as an attendee to the Event. And when Doctrine saves, it tries to add a second row to the join table. Not cool!

Adding the hasAttendee Method

To fix this, create a new method in Event called hasAttendee. This will return true or false depending on whether or not a given user is attending this event:

// src/Yoda/EventBundle/Entity/Event.php
// ...

/**
 * @param \Yoda\UserBundle\Entity\User $user
 * @return bool
 */
public function hasAttendee(User $user)
{
    return $this->getAttendees()->contains($user);
}

Avoiding Duplicates

Find attendAction in EventController. We can use the new hasAttendee method to avoid adding duplicate Users:

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

public function attendAction($id)
{
    // ...

    if (!$event->hasAttendee($this->getUser())) {
        $event->getAttendees()->add($this->getUser());
    }

    // ...
}

Try it out! Go crazy, click the attend link as many times as you want: you’re only added the first time.

Adding Unattend Logic

Let’s fill in the logic in unattendAction. Actually, we can just copy attendAction and remove the current user from the attendee list by using the removeElement method:

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

public function unattendAction($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);
    }

    if ($event->hasAttendee($this->getUser())) {
        $event->getAttendees()->removeElement($this->getUser());
    }

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

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

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

In our show template, let’s show only the “attend” or “unattend” link based on whether we’re attending the event or not. That’s easy with the hasAttendee method:

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

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

        {% if entity.hasAttendee(app.user) %}
            <a href="{{ path('event_unattend', {'id': entity.id}) }}" class="btn btn-warning btn-xs">
                Oh no! I can't go anymore!
            </a>
        {% else %}
            <a href="{{ path('event_attend', {'id': entity.id}) }}" class="btn btn-success btn-xs">
                I totally want to go!
            </a>
        {% endif %}
</dd>

When we refresh, the unattend button is showing. Click it and then click the attend button again. This bake sale is going to be off the hook!

What’s really going on in the Base Controller

Quickly, look back at the redirect and generateUrl methods we’re using in our controller. Let’s see what these really do by opening up Symfony's base controller class:

// vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
// ...

public function generateUrl($route, $parameters = array(), $absolute = false)
{
    return $this->container->get('router')->generate($route, $parameters, $absolute);
}

public function redirect($url, $status = 302)
{
    return new RedirectResponse($url, $status);
}

Like we’ve seen over and over again, generateUrl is just a shortcut to grab a service from the container and call a method on it. The redirect method is even simpler: it returns a special type of Response object that’s used when redirecting users.

The point is this: Symfony is actually pretty simple under the surface. Your job in every controller is to return a Response object. The container gives you access to all types of powerful objects to make that job easier.

Leave a comment!

4
Login or Register to join the conversation
Default user avatar

if ($event->hasAttendee($this->getUser())) {
$event->getAttendees()->removeElement($this->getUser());
}

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

is it not better to do this instead? Why we should disturb our database if nothing needed?

if ($event->hasAttendee($this->getUser())) {
$event->getAttendees()->removeElement($this->getUser());

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

Reply

Hey there!

Yes, you're right! But either way, Doctrine won't make a query to the database if it doesn't need to. In other words, both ways will result in 0 queries if there is no change. But, there will be a small performance boost with your method, since Doctrine doesn't even need to check on this.

Cheers!

Reply
Default user avatar

hasAttendee doing its job well in a Twig template.
But when a user tries to add new relationship it still could lead to error due to concurrency reasons.
Wouldn't it be better if we just try{} to add a relationship no matter what and then catch() the "duplicate key" exception?

Reply

Hey Ivan!
In that case you might preffer to create a new method for adding attendees like this:


public function addAttendee(User $user){
    if(!$this->getAttendees()->contains($user){
        $this->getAttendees()->add($user);
    }
}

So you can get rid of adding that try catch in every place where you need to add attendees

Have a nice day!

Reply
Cat in space

"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
    }
}
userVoice