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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBack 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
.
How a ManyToMany Link is Removed
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 |
Deleting the Link
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.
If you're fetching User and Genus, why not to make use of param converter, that will handle NotFoundException and fetching from DB ?