Doctrine Listener: Encode the User's Password
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.
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 | |
} |
Encoding the Password
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!
Encoding on Update
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 | |
} |
Registering the Subscriber as a Service
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.
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.