Removing a ManyToMany Item

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

Back on the Genus page, I want to add a little "X" icon next to each user. When we click that, it will make an AJAX call that will remove the scientist from this Genus.

To link a Genus and a User, we just added the User object to the genusScientists property:

... lines 1 - 14
class Genus
{
... lines 17 - 174
public function addGenusScientist(User $user)
{
if ($this->genusScientists->contains($user)) {
return;
}
$this->genusScientists[] = $user;
}
... lines 183 - 190
}

So guess what? To remove that link and delete the row in the join table, we do the exact opposite: remove the User from the genusScientists property and save. Doctrine will notice that the User is missing from that collection and take care of the rest.

Setting up the Template

Let's start inside the the genus/show.html.twig template. Add a new link for each user: give some style classes, and a special js-remove-scientist-user class that we'll use in JavaScript. Add a cute close icon:

... lines 1 - 4
{% block body %}
<h2 class="genus-name">{{ genus.name }}</h2>
<div class="sea-creature-container">
<div class="genus-photo"></div>
<div class="genus-details">
<dl class="genus-details-list">
... lines 12 - 21
<dd>
<ul class="list-group">
{% for genusScientist in genus.genusScientists %}
<li class="list-group-item">
... lines 26 - 31
<a href="#"
class="btn btn-link btn-xs pull-right js-remove-scientist-user"
>
<span class="fa fa-close"></span>
</a>
</li>
{% endfor %}
</ul>
</dd>
</dl>
</div>
</div>
<div id="js-notes-wrapper"></div>
{% endblock %}
... lines 46 - 71

Love it! Below, in the javascripts block, add a new script tag with a $(document).ready() function:

... lines 1 - 46
{% block javascripts %}
... lines 48 - 62
<script>
jQuery(document).ready(function() {
... lines 65 - 67
});
</script>
{% endblock %}

Inside, select the .js-remove-scientist-user elements, and on click, add the callback with our trusty e.preventDefault():

... lines 1 - 46
{% block javascripts %}
... lines 48 - 62
<script>
jQuery(document).ready(function() {
$('.js-remove-scientist-user').on('click', function(e) {
e.preventDefault();
});
});
</script>
{% endblock %}

The Remove Endpoint Setup

Inside, we need to make an AJAX call back to our app. Let's go set that up. Open GenusController and find some space for a new method: public function removeGenusScientistAction(). Give it an @Route() set to /genus/{genusId}/scientist/{userId}:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 117
/**
* @Route("/genus/{genusId}/scientists/{userId}", name="genus_scientists_remove")
... line 120
*/
public function removeGenusScientistAction($genusId, $userId)
{
}
}

You see, the only way for us to identify exactly what to remove is to pass both the genusId and the userId. Give the route a name like genus_scientist_remove. Then, add an @Method set to DELETE:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 117
/**
* @Route("/genus/{genusId}/scientists/{userId}", name="genus_scientists_remove")
* @Method("DELETE")
*/
public function removeGenusScientistAction($genusId, $userId)
{
}
}

You don't have to do that last part, but it's a good practice for AJAX, or API endpoints. It's very clear that making this request will delete something. Also, in the future, we could add another end point that has the same URL, but uses the GET method. That would return data about this link, instead of deleting it.

Any who, add the genusId and userId arguments on the method:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 121
public function removeGenusScientistAction($genusId, $userId)
{
}
}

Next, grab the entity manager with $this->getDoctrine()->getManager() so we can fetch both objects:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 121
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
... lines 125 - 145
}
}

Add $genus = $em->getRepository('AppBundle:Genus')->find($genusId):

... lines 1 - 123
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
... lines 129 - 148

I'll add some inline doc to tell my editor this will be a Genus object. And of course, if !$genus, we need to throw $this->createNotFoundException(): "genus not found":

... lines 1 - 123
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
if (!$genus) {
throw $this->createNotFoundException('genus not found');
}
... lines 133 - 148

Copy all of that boring goodness, paste it, and change the variable to $genusScientist. This will query from the User entity using $userId. If we don't find a $genusScientist, say "genus scientist not found":

... lines 1 - 123
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
if (!$genus) {
throw $this->createNotFoundException('genus not found');
}
$genusScientist = $em->getRepository('AppBundle:User')
->find($userId);
if (!$genusScientist) {
throw $this->createNotFoundException('scientist not found');
}
... lines 140 - 148

Now all we need to do is remove the User from the Genus. We don't have a method to do that yet, so right below addGenusScientist(), make a new public function called removeGenusScientist() with a User argument:

... lines 1 - 14
class Genus
{
... lines 17 - 183
public function removeGenusScientist(User $user)
{
... line 186
}
... lines 188 - 195
}

Inside, it's so simple: $this->genusScientists->removeElement($user):

... lines 1 - 14
class Genus
{
... lines 17 - 183
public function removeGenusScientist(User $user)
{
$this->genusScientists->removeElement($user);
}
... lines 188 - 195
}

In other words, just remove the User from the array... by using a fancy convenience method on the collection. That doesn't touch the database yet: it just modifies the array.

Back in the controller, call $genus->removeGenusScientist() and pass that the user: $genusScientist:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 121
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
... lines 129 - 133
$genusScientist = $em->getRepository('AppBundle:User')
->find($userId);
... lines 136 - 140
$genus->removeGenusScientist($genusScientist);
... lines 142 - 145
}
}

We're done! Just persist the $genus and flush. Doctrine will take care of the rest:

... lines 1 - 13
class GenusController extends Controller
{
... lines 16 - 121
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
... lines 129 - 133
$genusScientist = $em->getRepository('AppBundle:User')
->find($userId);
... lines 136 - 140
$genus->removeGenusScientist($genusScientist);
$em->persist($genus);
$em->flush();
... lines 144 - 145
}
}

Returning from the Endpoint

At the bottom, we still need to return a Response. But, there's not really any information we need to send back to our JavaScript... so I'm going to return a new Response with null as the content and a 204 status code:

... lines 1 - 11
use Symfony\Component\HttpFoundation\Response;
class GenusController extends Controller
{
... lines 16 - 121
public function removeGenusScientistAction($genusId, $userId)
{
$em = $this->getDoctrine()->getManager();
/** @var Genus $genus */
$genus = $em->getRepository('AppBundle:Genus')
->find($genusId);
... lines 129 - 133
$genusScientist = $em->getRepository('AppBundle:User')
->find($userId);
... lines 136 - 140
$genus->removeGenusScientist($genusScientist);
$em->persist($genus);
$em->flush();
return new Response(null, 204);
}
}

This is a nice way to return a response that is successful, but has no content. The 204 status code literally means "No Content".

Now, let's finish this by hooking up the frontend.

Leave a comment!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}