Buy

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:

192 lines src/AppBundle/Entity/Genus.php
... 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:

71 lines app/Resources/views/genus/show.html.twig
... 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:

71 lines app/Resources/views/genus/show.html.twig
... 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():

71 lines app/Resources/views/genus/show.html.twig
... 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}:

126 lines src/AppBundle/Controller/GenusController.php
... 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:

126 lines src/AppBundle/Controller/GenusController.php
... 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:

126 lines src/AppBundle/Controller/GenusController.php
... 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:

148 lines src/AppBundle/Controller/GenusController.php
... 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):

148 lines src/AppBundle/Controller/GenusController.php
... 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":

148 lines src/AppBundle/Controller/GenusController.php
... 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":

148 lines src/AppBundle/Controller/GenusController.php
... 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:

197 lines src/AppBundle/Entity/Genus.php
... 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):

197 lines src/AppBundle/Entity/Genus.php
... 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:

148 lines src/AppBundle/Controller/GenusController.php
... 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:

148 lines src/AppBundle/Controller/GenusController.php
... 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:

148 lines src/AppBundle/Controller/GenusController.php
... 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!

  • 2017-06-23 Jose Beteta

    You're welcome :)

  • 2017-06-23 Victor Bocharsky

    Hey Jose,

    Yes, you're right, it's unnecessary to call persist() if entity is already in the DB. Thank for this tip!

    Cheers!

  • 2017-06-22 Jose Beteta Garcia

    Persist is not necessary to remove an existing element from the collection.

    Following the docs.

    * NOTE: The persist operation always considers entities that are not yet known to
    * this EntityManager as NEW. Do not pass detached entities to the persist operation.

  • 2017-05-10 Diego Aguiar

    Hey Kuba Florczuk

    You are totally right, we can be lazier and let the ParamConverter to do the job for us, but from a teaching perspective, it is more complicated to explain what's going on behind the scenes

    Have a nice day!

  • 2017-05-10 Kuba Florczuk

    If you're fetching User and Genus, why not to make use of param converter, that will handle NotFoundException and fetching from DB ?


    /**
    * @Route("/genus/{slug}/remove-user/{user_id}", name="genus_scientist_remove")
    * @ParamConverter("scientist", class="AppBundle:User", options={"id" = "user_id"})
    * @Method("DELETE")
    *
    * @return Response
    */
    public function removeGenusScientistAction(Genus $genus, User $scientist)
    {
    $genus->removeGenusScientist($scientist);

    $em = $this->getDoctrine()->getManager();
    $em->persist($genus);
    $em->flush();

    return new Response(null, 204);
    }
  • 2017-01-29 weaverryan

    You did it exactly right then :). Cheers!

  • 2017-01-28 adrianbadarau

    But it's ok, I managed to do it with @ParamConverter

  • 2017-01-28 adrianbadarau

    I wanted to do something like public ' function removeScientistAction(Genus $genus, User $user) '

  • 2017-01-28 weaverryan

    Yo adrianbadarau!

    I'm not sure what you mean - can you give an example of your setup and what you'd like to accomplish?

    Cheers!

  • 2017-01-27 adrianbadarau

    How could we use route=>model binding to get Doctrine Entity versions and not just ids?

  • 2016-12-14 Victor Bocharsky

    Hm, I'd recommend you to update your PhpStorm to the latest version - as I understand your version isn't the latest one. Probably it helps. If not - try to reset your PhpStorm settings. Actually, they migrate from version to version, so even upgraded PhpStorm will still use your previous settings. So I suppose you need to delete your "Project Settings" and "IDE Settings" - see the https://www.jetbrains.com/h... . Make its backup if you need it.

    Btw, could you take a screenshot of opened file in PhpStorm where you doesn't have autocompletion? It helps to understand your case better.

    Cheers!

  • 2016-12-14 eCosinus

    Hi, I m using
    phpStorm 2016.1.2
    Build #PS-145.1616, built on May 24, 2016
    JRE: 1.8.0_76-release-b198 amd64
    JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

    I' ve tried to unisntall and reinstall it from stratch It's still not working.

  • 2016-12-12 Victor Bocharsky

    Hey eCosinus ,

    Do you use the latest PhpStorm version? It'w weird, if it works well for templates, it should work inside javascript tags too. I'm wondering, what is name of your template where you don't have autocompletion? Is it something like "my-script.js.twig"? Have you wrapped your code with the "<script></script>" tag *inside* this template?

    Cheers!

  • 2016-12-11 eCosinus

    Hi, I m using phpstorm too with the symfony plugin, completion works well in twig template and php files except for javascript tags <script> in template files. I have no completion netiher color highlighting

  • 2016-12-10 weaverryan

    Ah, thanks eCosinus! I'm using PhpStorm, and I believe it naturally gives you highlighting and code completion on your JavaScript inside Twig. Do you see something different in PhpStorm for you? It should see the <script> tag, and know that there is JavaScript inside.

    Cheers!

  • 2016-12-10 eCosinus

    Hi, how to you get code completion and color syntaxing on javascript in your twig template ? Thanks
    By the way this lesson about doctrine relationship is amazing