Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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!

16
Login or Register to join the conversation

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);
}
Reply

Hey kubaf

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!

2 Reply
Default user avatar
Default user avatar adrianbadarau | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar adrianbadarau | weaverryan | posted 5 years ago

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

Reply
Default user avatar
Default user avatar adrianbadarau | weaverryan | posted 5 years ago

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

Reply

You did it exactly right then :). Cheers!

Reply
Default user avatar
Default user avatar Jose Beteta Garcia | posted 5 years ago

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.

Reply

Hey Jose,

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

Cheers!

Reply
Default user avatar
Default user avatar Jose Beteta | Victor | posted 5 years ago

You're welcome :)

Reply
Default user avatar
Default user avatar eCosinus | posted 5 years ago

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

Reply

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 &lt;script&gt; tag, and know that there is JavaScript inside.

Cheers!

Reply
Default user avatar

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

Reply

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!

Reply
Default user avatar

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.

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice