If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
In AppBundle
, create a new directory called Doctrine
and a new class called
HashPasswordListener
:
... lines 1 - 2 | |
namespace AppBundle\Doctrine; | |
... lines 4 - 7 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 10 - 13 | |
} |
If this is your first Doctrine listener, welcome! They're pretty friendly. Here's the idea: we'll create a function that Doctrine will call whenever any entity is inserted or updated. That'll let us to do some work before that happens.
Implement an EventSubscriber
interface and then use Command
+N
or the "Code"->"Generate"
menu, select "Implement Methods" and choose the one method: getSubscribedEvents()
:
... lines 1 - 4 | |
use Doctrine\Common\EventSubscriber; | |
... lines 6 - 7 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
public function getSubscribedEvents() | |
{ | |
... line 12 | |
} | |
} |
In here, return an array with prePersist
and preUpdate
:
... lines 1 - 7 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
public function getSubscribedEvents() | |
{ | |
return ['prePersist', 'preUpdate']; | |
} | |
} |
These are two event names that Doctrine makes available. prePersist
is called
right before an entity is originally inserted. preUpdate
is called right before
an entity is updated.
Next, add public function prePersist()
:
... lines 1 - 6 | |
use Doctrine\ORM\Event\LifecycleEventArgs; | |
... lines 8 - 9 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 12 - 18 | |
public function prePersist(LifecycleEventArgs $args) | |
{ | |
... lines 21 - 30 | |
} | |
... lines 32 - 36 | |
} |
When Doctrine calls this, it will pass you an object called LifecycleEventArgs
,
from the ORM namespace.
This method will be called before any entity is inserted. How do we know what
entity is being saved? With $entity = $args->getEntity()
. Now, if this is not
an instanceof User
, just return and do nothing:
... lines 1 - 4 | |
use AppBundle\Entity\User; | |
... lines 6 - 9 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 12 - 18 | |
public function prePersist(LifecycleEventArgs $args) | |
{ | |
$entity = $args->getEntity(); | |
if (!$entity instanceof User) { | |
return; | |
} | |
... lines 25 - 30 | |
} | |
... lines 32 - 36 | |
} |
Now, on to encoding that password.
Symfony comes with a built-in service that's really good at encoding passwords. It's
called security.password_encoder
and if you looked it up on debug:container
, its
class is UserPasswordEncoder
. We'll need that, so add a __construct()
function
and type-hint a single argument with UserPasswordEncoder $passwordEncoder
. I'll hit
Option
+Enter
and select "Initialize Fields" to save me some time:
... lines 1 - 7 | |
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoder; | |
class HashPasswordListener implements EventSubscriber | |
{ | |
private $passwordEncoder; | |
public function __construct(UserPasswordEncoder $passwordEncoder) | |
{ | |
$this->passwordEncoder = $passwordEncoder; | |
} | |
... lines 18 - 36 | |
} |
In a minute, we'll register this as a service.
Down below, add $encoded = $this->passwordEncoder->encodePassword()
and pass it
the User - which is $entity
- and the plain-text password: $entity->getPlainPassword()
.
Finish it with $entity->setPassword($encoded)
:
... lines 1 - 9 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 12 - 18 | |
public function prePersist(LifecycleEventArgs $args) | |
{ | |
$entity = $args->getEntity(); | |
if (!$entity instanceof User) { | |
return; | |
} | |
$encoded = $this->passwordEncoder->encodePassword( | |
$entity, | |
$entity->getPlainPassword() | |
); | |
$entity->setPassword($encoded); | |
} | |
... lines 32 - 36 | |
} |
That's it: we are encoded!
So now also handle update, in case a User's password is changed! The two lines that
actually do the encoding can be re-used, so let's refactor those into a private method.
To shortcut that, highlight them, press Command
+T
- or go to the "Refactor"->"Refactor this"
menu - and select "Method". Call it encodePassword()
with one argument that's a
User
object:
... lines 1 - 9 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 12 - 18 | |
public function prePersist(LifecycleEventArgs $args) | |
{ | |
... lines 21 - 25 | |
$this->encodePassword($entity); | |
} | |
... lines 28 - 48 | |
/** | |
* @param User $entity | |
*/ | |
private function encodePassword(User $entity) | |
{ | |
if (!$entity->getPlainPassword()) { | |
return; | |
} | |
$encoded = $this->passwordEncoder->encodePassword( | |
$entity, | |
$entity->getPlainPassword() | |
); | |
$entity->setPassword($encoded); | |
} | |
} |
Tip
I didn't mention it, but you also need to prevent the user's password from being encoded if plainPassword is blank. This would mean that the User is being updated, but their password isn't being changed.
Super nice!
Now that we have that, copy prePersist
, paste it, and call it preUpdate
. You
might think that these methods would be identical... but not quite. Due to a quirk
in Doctrine, you have to tell it that you just updated the password field, or it
won't save.
The way you do this is a little nuts, and not that important: so I'll paste it in:
... lines 1 - 6 | |
use Doctrine\ORM\Event\LifecycleEventArgs; | |
... lines 8 - 9 | |
class HashPasswordListener implements EventSubscriber | |
{ | |
... lines 12 - 28 | |
public function preUpdate(LifecycleEventArgs $args) | |
{ | |
$entity = $args->getEntity(); | |
if (!$entity instanceof User) { | |
return; | |
} | |
$this->encodePassword($entity); | |
// necessary to force the update to see the change | |
$em = $args->getEntityManager(); | |
$meta = $em->getClassMetadata(get_class($entity)); | |
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity); | |
} | |
... lines 43 - 63 | |
} |
Ok, the event subscriber is perfect! To hook it up - you guessed it - we'll register
it as a service. Open app/config/services.yml
and add a new service called
app.doctrine.hash_password_listener
. Set the class. And you guys know by now that
I love to autowire things. It doesn't always work, but it's great when it does:
... lines 1 - 5 | |
services: | |
... lines 7 - 21 | |
app.doctrine.hash_password_listener: | |
class: AppBundle\Doctrine\HashPasswordListener | |
autowire: true | |
... lines 25 - 27 |
Finally, to tell Doctrine about our event subscriber, we'll add a tag. This is something
we talked about in our services course: it's a way to tell the system that your service
should be used for some special purpose. Set the tag to doctrine.event_subscriber
:
... lines 1 - 5 | |
services: | |
... lines 7 - 21 | |
app.doctrine.hash_password_listener: | |
class: AppBundle\Doctrine\HashPasswordListener | |
autowire: true | |
tags: | |
- { name: doctrine.event_subscriber } |
The system is complete. Before creating or updating any entities, Doctrine will call our listener.
Let's update our fixtures to try it.
Yo Mike!
Hmm, are you sure about the deprecation? It actually makes sense - changes like this have been made in the past - but I don't see it marked as deprecated in the code yet: https://github.com/doctrine.... Is there another spot you were seeing it? I'd definitely be interested to know :).
And quite likely about Ctrl+T - Command+T is a Mac thing, Windows & Linux are different (i.e. swap Command for Ctrl). And actually, unfortunately, *some* keyboard shortcuts are *totally* different between operating systems... and that's no fun at all :p.
For those reading this later, PHPStorm's docs on shortcuts have a little OS selector in the upper right, so you can see what a shortcut is in your OS: https://www.jetbrains.com/h...
Cheers!
My mistake, I had used `Doctrine\Common\Persistence\Event\LifecycleEventArgs` instead of `Doctrine\ORM\Event\LifecycleEventArgs`. Sorry for the noise.
Sweeeet! No worries, things *do* become deprecated (or I make other mistakes), and it's nice that there are people who take the time to let me know :).
Cheers!
Hi,
When I register a new user (chapter 18), I have an error "Integrity constraint violation: 1048 [password field cant be null] (translated from french ^^'". So after a few tests, I've determined that the problem must come from the listener.
Inside the prePersist function, if I put a "dump($entity );die;" right after "$entity = $args->getEntity();", I see that the prepersist event is detected. and obtain this :
User {#5368 â–¼
-id: null
-name: "Test"
-mail: "test"
-password: null
-plainPassword: "test"
}
But ! If I put the dump/die after the "if (!$entity instanceof User)", I have the Integrity Constraint Violation". That shoud mean that my $entity isn't an instance of User, therefore the encodedPassword function isn't executed, right ? Yet, the previous test has shown that it really is an instance of User. So I don't understand.
Could someone please explain me what I've missed ?
Thank you.
Edit : if I comment the "if !$entity part" in the prePersist function, no more errors and the user is registered.
Edit 2 : found my mistake. A namespace error, again. Mfff.
Hey Xav!
First: let's be 100% sure that your $entity is not an User object by dumping it and checking its type
Second: Check at the top of your file (in the use statement section) what type of User was declared, maybe if you used autocompletion, you might chose a wrong one, it should be your "User Class"
If neither of these cases, please let me see your listener code
Cheers!
Hi,
when you implemented the prePersist/preUpdate methods, they take a LifecycleEventArgs parameter. But for someone new to symfony like me, how do I know this is need here or even what this is called?
Hey Yang!
That's a GREAT question - I hate when things are just magically shown - so shame on me :).
Of course, you can read the documentation. And, at the very least, you would probably need to read a little bit of documentation to at least know that there is an argument to this method. But, to know what class this is (i.e. that it is a LifecycleEventArgs), I like to simply dump() the variable to find out what it is:
public function prePersist($event)
{
dump($event);die; // what are you!?
}
When in doubt, this is what I like to do!
Cheers!
That won't work if ONLY plainPassword is changed and nothing else, since plain password is not persisted field - so preUpdate Event won't fire - since ORM doesn't see any changes in entity.
Hey Bozhidar Hristov!
You are absolutely correct :). That's why, in the previous video, we modify the setPlainPassword() field to also set the password field to null - https://symfonycasts.com/sc...
That's a bit of a hack, but it was done to solve this issue: it causes the preUpdate to fire and the password to bet set to the encoded value before save :).
Btw, in newer tutorials, I've decided against using a Doctrine listener to encode the password - it's just a bit too magic, and requires tricky things like this to work. Now, I encode the password manually when I create a User, which is usually just in registration and maybe also in a "user fixture" place (so, it's not too many places, and you could centralize this all into a service if you want).
Cheers!
Hi guys,
A little question, I have created a user profile edit form but when I submit it the password changes to null because the HashPasswordListener (created in chapter 10) is processing it. To fix that, I added a condition in the preUpdated function:
if ((!$entity instanceof Users) or ($_SERVER['REQUEST_URI'] == '/myURL')) {
return;
}
It works ok but I am wondering if it's correct to do that or if there is a better ("professional") way to correct that. I hope you can give me advice.
Cesar
Hey Cesar.
It's strange, maybe you missed something, check out `encodePassword()` method, it should check if there is a plain password and encode only if there is a new password. Here is a tip for you https://symfonycasts.com/sc...
Hope this will help
Cheers!
How do I know what class to include in use statement when phpstorm suggests multiple one? For instance of LifecycleEventArgs I have Doctrine\Common and Doctrine\ORM.
Hey,
What about LifecycleEventArgs class - it depends on do you use Doctrine ORM or no. Most probably you do in Symfony projects, so choose the second one which "Doctrine\ORM\LifecycleEventArgs", otherwise - choose the "Doctrine\Common\LifecycleEventArgs". But this things is hard to guess, that's why you should always based on its docs! In docs take to account what FQCN is used in the use statement. In popular third party libraries it always well documented, otherwise you just need to guess or inspect their code.
Cheers!
Hi,
I put a post previously in form. I have now got as a result of my code:
$userPassword = $formForgotPassword->getData();
var_dump($userPassword["plainPassword"]);
$user->setPassword($userPassword["plainPassword"]);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
var_dump($user->getPassword());
die();
this:
string(3) "123" string(3) "123"
So now the password is here but it is not encoded... I have looked twice the video and just can not understand why and how to get this listener to listen to my controller which is normally called before any entity is inserted. I have tried to put a var_dump() but it just show me that when updating the password the listener is not called at all. How can I get this listener to listen when the user reset his/her password?
Thanks
Hey James,
I'm glad you handled with setting a password! Well, first of all you should achieve this event listener to work. Please, double check you defined event listener as service correctly, it should has a tag - { name: doctrine.event_subscriber }
.
Then ensure you have all necessary methods inside event listener class: prePersist() and preUpdate() - where I'd recommend you to simply add a die('listener works');
statement at the beginning of each method to see it works. Also your event listener must have getSubscribedEvents()
method:
public function getSubscribedEvents()
{
return ['prePersist', 'preUpdate'];
}
Please note, you must use exactly 'prePersist' and 'preUpdate' strings as it's a reserved event names. And the same names you should use for methods.
BTW, please, ensure you clear the cache, I'd recommend you manually remove "dev" and "prod" folders in the cache directory.
Let me know if it helps
Cheers!
Just understood now the whole process from the code above, I need to not try to add directly the password but send it to setplainPassword and then the listener will encode and setPassword for me so:
not: $user->setPassword($userPassword["plainPassword"]);
but: $user->setplainPassword($userPassword["plainPassword"]);
Thanks again for your help!
Hey James,
Yes! Actually, it depends on how you are you doing it, but the most common way is using the secondary $plainPassword property instead of the same ($password property) for it.
I got an error in a console, while tried to load fixtures:
> purging database
> loading AppBundle\DataFixtures\ORM\LoadFixtures
[UnexpectedValueException]
Could not determine how to assign plainPassword to a AppBundle\Entity\User object
what should I do in this case?...
Ah, this *probably* means that you don't have a setPlainPassword() method - so Alice doesn't know how to set the plainPassword field. Can you check to see if you have that?
Cheers!
Would that do it?
# Prevent the user's password from being encoded if plainPassword is blank.
# This would mean that the User is being updated, but their password isn't being changed.
if (!$entity->getPlainPassword()) {
return;
}
$this->encodePassword($entity);
Yo Vlad!
There is a slight difference between the Doctrine event subscriber and the event subscriber used by the JMSSerializerBundle. They philosophically do the same thing, but under the hood, Doctrine uses *its* event library, and JMS uses Symfony's event library. So the way that you wire them up is slightly different. So I don't think adding the "class" option works with Doctrine. BUT, Doctrine does support something slightly different, that does exactly this - it's called an entity listener. They're great, but the config is a little weird for me so I stick to subscribers. Check them out http://docs.doctrine-projec... and http://symfony.com/doc/curr...
And yes to your code block - checking for !$entity->getPlainPassword() should do it - that's the bit I forgot!
Cheers!
Ah, great question :). Unfortunately, it's just one of those things that's up to the docs of the different libraries to support. *Ideally*, it would be on the php doc of the interface. For Doctrine, that's true (it simply says that this method returns an array of event names). For JMS, it should be in its docs, or you can go digging for it (https://github.com/schmittj.... In short, it's up to the implementation of the event system, so they can kinda do whatever they want. tl;dr: it can only really be found in the docs, or, buried in the code.
Cheers!
Hi Ryan,
In your REST tutorial, your <strong>getSubscribedEvents()</strong> method is slightly different:
public static function getSubscribedEvents()
{
# Only call onPostSerialize for Programmer classes!
return array(
array(
'event' => 'serializer.post_serialize',
'method' => 'onPostSerialize',
'format' => 'json',
'class' => 'AppBundle\Entity\Programmer'
)
);
}
Instead of doing the if (!$entity instanceof User)
check, can the getSubscribedEvents be refactored to the following?
public static function getSubscribedEvents()
{
# Only call onPostSerialize for Programmer classes!
return array(
array(
'event' => 'prePersist',
'method' => 'prePersist',
'class' => 'AppBundle\Entity\User'
),
array(
'event' => 'preUpdate',
'method' => 'preUpdate',
'class' => 'AppBundle\Entity\User'
)
);
}
Hi,
I have a question regarding:
if(!$entity instanceof User)
{
return;
}
From what I understand HashPasswordListener will be used/called for every Entity (not only User).
If I want to have listeners for more then one entity type I have two options:
1. register more listeners and have similar if statement in all of them - this means
that all listeners will be called even if only one (or none) will actually be used for particular entity type.
2. Have only one listener and many if/else statements in order to find out which entity I am saving - and this will break few SOLID rules.
Is there a better way?
Thank you!
Best regards,
Damir
Hey Damjan,
Think about feature, i.e. what tasks exactly should your listener do. If you name your listener as HashPasswordListener, so this listener should be used for hashing passwords only. If you need to hash passwords for more than one entity - you do not need to create another HashPasswordListener to implement the same feature for another entity, just add a new "if (!$entity instanceof SomeClass)" statement to it. But if you need to implement another feature, better to create another listener and give it a good name. You can also take a look from a bit another side: Do not duplicate code, so if you have the same functionality but for a few entities - implement it for both in one listener. But if you have different tasks - better to decouple code to different listeners with a good names which will easy to understand and maintain for others, or for you in the future.
So, in short, your 1st option is better, but it depends :)
Cheers!
While I enjoyed learning about the Doctrine listener, it seems like a lot of extra work to put in the Doctrine listener to encode the password. Why not just encode the password whenever setPassword() is called? Something like the following?
User Entity:
public function setPassword($password)
{
$encoded = $this->passwordEncoder->encodePassword(
$this,
$password
);
$this->password = $encoded ;
}
Hey Terry,
Good question! Because in this case you have to inject the password encoder service into the entity, which is a really bad practice, since entity is just a *data* not a service.
Cheers!
Just curious... why does Doctrine give ORM annotation to allow manipulation within the Entity file? Is it still bad practice to do it as part of the Entity file as opposed to a separate listener initiated in the services.yml file?
Hey Terry,
Actually, Doctrine Lifecycle Callbacks are fine and you can use it as well, but they have some limitation like you can't inject other services into the entity. That's why listeners help with it - we can do the same with event listeners and we also can inject other service into it since listener is just a service.
Cheers!
Hello,
I did this for practice and got :
Cannot autowire service "app.doctrine.hash_password_listener": argument "$passwordEncoder" of method "App\Doctrine\HashPasswordListener::__construct()" references class "Symfony\Component\Security\Core\Encoder\UserPasswordEncoder" but no such service exists. Try changing the type-hint to "Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface" instead.
I changed to UserPasswordEncoderInterface and when updating the user details i get:
Warning: strlen() expects parameter 1 to be string, object given
pointing to encodeUserPassword ---
$encoded = $this->passwordEncoder->encodePassword($entity,$entity->getPlainPassword());
Hey Alexandru Lazar!
Ok, really interesting things are happening here! We've changed & improved autowiring in Symfony 3.3 and 3.4, so that's part of what you're seeing. First, what version of Symfony are you on? I would not expect the first error, though I would expect a deprecation notice. Here are some details:
1) In Symfony 3, autowiring is quite magic: if you type-hint UserPasswordEncoder, it tries to find any service with that class and pass it in. But, that magical way is now deprecated. And in Symfony 4, it's much simpler: your type-hint must exactly match a service id in the container. Here are all the details about that: https://knpuniversity.com/s.... This is why - unless you're trying Symfony 4.0 BETA - I am surprised by your first error. On Symfony 4, you *must* use UserPasswordEncoderInterface (that's the service id in the container). But in Symfony 3, both should work (but using UserPasswordEncoder will trigger a deprecation notice).
2) About the strlen() error, that's interesting. Basically, if you look deep enough, the second argument to encodePassword() - $entity->getPlainPassword() - is ultimately passed to a class that checks its length: https://github.com/symfony/.... Is $user->getPlainPassword() somehow an object? I don't think this has anything to do with autowiring, I think there's another issue.
Let me know what you find out! Cheers!
Heyweaverryan !
Thanks for the fast answer. I am using 4.0 Beta and will go with the encoder interface. A for the second one I am clueless at the moment. I even tried to force it to string and it says it can`t be transformed. It`s clearly not autowiring since i got passed that using UserPassswordEncoderInterface. I`ll keep trying and let you know.
Thanks for the fast answer!
Cheers!
Ah, cool! That answers the first question perfectly then :).
Try this:
1) dump($this->passwordEncoder);die;
. It should be an instance of UserPasswordEncoder
2) dump($entity->getPlainPassword());die;
. This should be a string... but your error seems to make me think this is an object!
Cheers!
I sort it out. Before finding this tutorial i tried to pass * @ORM\PreUpdate in the user entity on the password field. Working great now! Thanks for the support you got me i`ll buy this tutorial!
Cheers!
Hi,
I was using FOSUserBundle on my project, but i would to change and take a solution coded by myself. I found your solution very usefull but i've one question. I have 100 account registered and also 100 password, registered with sha and salt...But in the screencast we used bcrypt, so what could i migrate the account member ?
Thanks for your answer
Hey Christophe Lablancherie!
You can definitely do this (sort of) and you have 2 options:
1) Continue using sha with salt - this is just a setting in security.yml, and you can keep the same hashing algorithm even if you're changing from FOSUserBundle to something else. Of course, bcrypt is more secure than just using sha.
2) Allow your 100 old accounts to use sha, but use bcrypt for the new accounts. You would do this by adding some flag on the old accounts so that you know they use the old method. Then, implement the EncoderAwareInterface
in your User class to tell Symfony which to use in which case.
Cheers!
Thanks for your answer ! I will do some research for EncoderAwareInterface and if it's easy to implement i will do this to do on bcrypt :)
Your service tag auto completed towards the end there. Mine didn't! Why? Yes. I have symfony plugin... ;)
Yo Richard!
Weeeeeeeeell, mine only "kinda" auto completed :). It auto-completed doctrine.event_listener, but as soon as I typed doctrine.event_subscriber... the auto-complete did *not* work! If you got no auto-completion, I'm not sure - just make sure you have the latest version of the plugin! That magic definitely comes from the plugin :).
Cheers!
Why don't you encode the password in setter method setPlainPassword()? You have to encode password if 'plainPassword' is not empty. So it would make sense to encode it right away once you set it. If there is a listener, it would be called everytime User entity is updated unnecessarily. No?
Hey Trashcan4U,
Because... we can't do it in setPlainPassword(), i.e. we can't inject services into entities - actually that's a bad practice. Entities are just data, to perform any actions on data we need other stuff like services, listeners, etc. And since we need a PasswordEncored service, we can't get it inside User entity. That's why we do it outside.
And yes, you're correct, it'll be called every time when user is update but... we have a quick check to make sure we can proceed further:
if (!$entity->getPlainPassword()) {
return;
}
So if password wasn't updated, i.e. just ther fields were updated - then skip encoding password. Actually, it's not a big deal.
How does it sound to you?
Cheers!
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
It seems LifecycleEventArgs->getEntity() and LifecycleEventArgs->getEntityManager() are deprecated, replaced by LifecycleEventArgs->getObject() and LifecycleEventArgs->getObjectManager().
Also, the for the refactor shortcut (super cool btw) the video mentions to use Command+T, when on my PHPStrom, it is Ctrl+T.