Buy Access to Course
10.

Doctrine Listener: Encode the User's Password

Share this awesome video!

|

Keep on Learning!

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:

27 lines | app/config/services.yml
// ... 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:

27 lines | app/config/services.yml
// ... 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.