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
.
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.
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 %} |
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 | |
} | |
} |
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.
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!
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!
I wanted to do something like public ' function removeScientistAction(Genus $genus, User $user) '
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.
Hey Jose,
Yes, you're right, it's unnecessary to call persist() if entity is already in the DB. Thank for this tip!
Cheers!
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
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!
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
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!
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.
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!
// 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
}
}
If you're fetching User and Genus, why not to make use of param converter, that will handle NotFoundException and fetching from DB ?