Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.3.3
Subscribe to download the code!Compatible PHP versions: >=5.3.3
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Doctrine Listeners on Update
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
Doctrine Listeners on Update¶
But what if a user updates their password? Hmm, our listener isn’t called on updates, so the encoded password can never be updated. Crap!
Add a second tag to services.yml to listen on the preUpdate event and create the preUpdate method by copying from prePersist:
# src/Yoda/UserBundle/Resources/config/services.yml
services:
doctrine.user_listener:
class: Yoda\UserBundle\Doctrine\UserListener
arguments: ["@security.encoder_factory"]
tags:
- { name: doctrine.event_listener, event: prePersist }
- { name: doctrine.event_listener, event: preUpdate }
Add a die statement so we can test things:
// src/Yoda/UserBundle/Doctrine/UserListener.php
// ...
public function preUpdate(LifecycleEventArgs $args)
{
die('UUPPPPPDDAAAAAATING!');
$entity = $args->getEntity();
if ($entity instanceof User) {
$this->handleEvent($entity);
}
}
Also, if the plainPassword field isn’t set, don’t do any work. This will happen if a User is being saved, but their password isn’t being changed:
// src/Yoda/UserBundle/Doctrine/UserListener.php
// ...
private function handleEvent(User $user)
{
if (!$user->getPlainPasword()) {
return;
}
// ...
}
Testing the Update¶
We can’t test this easily because we don’t have a way to update users yet. No worries. Just open up the play script from episode 1. We already have a user here - just change his plain password and save:
// play.php
// ...
use Doctrine\ORM\EntityManager;
$em = $container->get('doctrine')
->getEntityManager()
;
$wayne = $em
->getRepository('UserBundle:User')
->findOneByUsernameOrEmail('wayne');
$wayne->setPlainPassword('new');
$em->persist($user);
$em->flush();
Ok, run the play script:
php play.php
Hmm, it didn’t hit our die statement. Our listener function wasn’t called.
Gotcha 1: Event Listeners don’t fire on Unchanged Objects¶
It’s a gotcha! The plainPassword property isn’t saved to Doctrine, but we do use it to set the password field, which is persisted.
The problem is that when we change only the plainPassword field, the User looks “unmodified” to Doctrine. So, instead of calling our listener, it does nothing.
To fix the issue, let’s nullify the password field whenever plainPassword is set:
// src/Yoda/UserBundle/Entity/User.php
// ...
public function setPlainPassword($plainPassword)
{
$this->plainPassword = $plainPassword;
$this->setPassword(null);
return $this;
}
Since password is persisted to Doctrine, this is enough to trigger all the normal behavior. Our listener should make sure password is set to the encoded value, and not left blank.
Now run the play script again. Great, it hits the die statement. Remove that and try it again.
No errors, so let’s try to login. Yes!
We just saw prePersist and preUpdate and Doctrine has several other events you can find on their website. Symfony also has events, which are fired at different points during the request-handling process.
Fortunately, Symfony’s event system is very similar to Doctrine’s. Don’t you love it when good ideas are shared?
17 Comments
Hey Diego!
Wow, that's good detective work! I'll admit that I'm not familiar with this issue, but it makes sense - it's a legitimate issue! So, what you need to do is (obviously) not nullify or change the password. But, we *do* need to change *some* persisted field on `User` in order to trigger the Doctrine listener.
I would add an updatedAt field, and have it be set with a Lifecyclecallback (PreUpdate) - just like we did a few chapters ago: https://knpuniversity.com/s.... Then, in setPlainPassword(), instead of setting password to null, call $this->setUpdatedAt(new \DateTime()); This will cause the entity to look "changed" and the listener to be called.
Let me know if it works!
Hehehe, I thought to do the same thing and yeah it works!
But what would be the drawbacks of doing it ?
None! The whole "setting a property so that Doctrine actually saves" is a bit of a hack. If there were a way to do it, I would prefer to be able to reach into Doctrine directly via some function and say "my User entity *is* modified, so even though it doesn't look like it, please save it". I don't think such a thing exists (or it's not easy to get to), so we just find the most clever way we can to making the entity look modified ("dirty" in Doctrine speak). Heck, if you set `updatedAt` to some absurd data (2025), it still wouldn't matter - it would simply cause Doctrine to save your entity, then your lifecycle callback for PreUpdate would set it back to the proper time of "now" :)
Cheers!
That's great!
I found something interesting "Entity listener class", in order to avoid calling your events from entities not engaged with em.
For more details see docs: http://docs.doctrine-projec...
Would be nice to hear a bit of em in your videos or in a post. I'm finding it dificult to implement to my project.
Thanks for your time.
Hi Diego!
Nice find! It's a new feature in Doctrine, and not one that I've used yet. It looks like a mixture of "Lifecycle callbacks" (where the "callback" is always inside of the entity class) and an event listener. The upside of this "Entity Listener" is that it only gets called for your entity (of course, I know you saw this). The downside is that you (similar to lifecycle callbacks, but unlike event listeners) don't have access to do any dependency injection - i.e. you can't access anything in the service container. So, what you can do inside of them is still a bit limited. Actually, it looks like you can work around this (http://egeloen.fr/2013/12/0..., but wow - not sure that's worth it.
Let me know what your experiences are - I enjoy the chat :).
Cheers!
Hey Weaver,
Thanks for your answer! Sometimes I think I'm asking too much, but there is a lot to learn in this Symfony world. Hehehe ;]
I followed that article and it works without problems!
But I coulnd't understand what I did. To be honest I got frustrated because I tried to follow Doctrine's documentation in how to make your custom resolver and I wouldn't find a way to achieve this without a step by step tutorial.
plz provide me video but i hv not much money
Hey @vinod!
Email me - ryan@knpuniversity.com - and I'll see if I can help :).
Cheers!
Or provide code at end of every episode like 1,2,3,4
// play.php
// ...
$em = $container->get('doctrine')
->getEntityManager()
;
The code above from the script is deprecated, use the original code or follow the video:
$em = $container->get('doctrine')->getManager();
+1 - both will work until 3.0 (so no worries), but definitely use getManager()!
Cheers!
Little late to the discussion, but I noticed something. This chapter makes User.setPlainPassword clear the password field. But if your User.eraseCredentials function also calls setPlainPassword, as it does here https://knpuniversity.com/s..., that will now clear the password field as well. This means that when your user is persisted in the session, and you flush the Doctrine entity manager, it will clear your password in the database. So you should change eraseCredentials to just set $this->plainPassword to null, instead of using the setter.
This makes perfect sense to me :) - I think we accidentally called the setPlainPassword() not thinking about the side-effects! Fortunately, we've fixed this mistake for our Symfony 3 tutorials - you can see an example of the correct code at the bottom of this page: http://knpuniversity.com/sc...
Thanks for the comment!
Hello Ryan,
I don't know if this is still relevant but i added $this->setUpdatedAt(new \DateTime()); in the setPlainPassword() rather than setting password to null to avoid any side effects and it did work.
Cheers.
Awesome! Thanks for sharing Junaid Farooq :)
weaverryan
You are always welcome and thank you for sharing your great knowledge and always reaching out.
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "~2.4", // v2.4.2
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.0.1
"symfony/assetic-bundle": "~2.3", // v2.3.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.5
"symfony/monolog-bundle": "~2.4", // v2.5.0
"sensio/distribution-bundle": "~2.3", // v2.3.4
"sensio/framework-extra-bundle": "~3.0", // v3.0.0
"sensio/generator-bundle": "~2.3", // v2.3.4
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
"ircmaxell/password-compat": "~1.0.3", // 1.0.3
"phpunit/phpunit": "~4.1", // 4.1.0
"stof/doctrine-extensions-bundle": "~1.1.0" // v1.1.0
}
}
Hi, I've found a problem, after adding this preUpdate listener to my project I found that remember_me action is not working anymore. After researching for a bit I found that removing this line from setPlainPassword method fixed it
$this->setPassword(null);
I found the reason here: http://stackoverflow.com/qu...
But, now the preUpdate listener doesn't work. Any suggestion ?
Thanks in advance