Buy

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?

Leave a comment!

  • 2016-09-01 weaverryan

    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/scree...

    Thanks for the comment!

  • 2016-09-01 hooplehead

    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/scre..., 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.

  • 2015-04-08 weaverryan

    +1 - both will work until 3.0 (so no worries), but definitely use getManager()!

    Cheers!

  • 2015-04-05 Mihail Bashliy

    // 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();

  • 2015-01-27 Diego Aguiar

    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.

  • 2015-01-27 weaverryan

    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/01/s..., but wow - not sure that's worth it.

    Let me know what your experiences are - I enjoy the chat :).

    Cheers!

  • 2015-01-26 Diego Aguiar

    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-project.o...

    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.

  • 2015-01-24 weaverryan

    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!

  • 2015-01-23 Diego Aguiar

    Hehehe, I thought to do the same thing and yeah it works!

    But what would be the drawbacks of doing it ?

  • 2015-01-23 weaverryan

    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/scre.... 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!

  • 2015-01-23 Diego Aguiar

    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/quest...

    But, now the preUpdate listener doesn't work. Any suggestion ?

    Thanks in advance

  • 2014-11-04 weaverryan

    Hey @vinod!

    Email me - ryan@knpuniversity.com - and I'll see if I can help :).

    Cheers!

  • 2014-11-04 vinod

    Or provide code at end of every episode like 1,2,3,4

  • 2014-11-04 vinod

    plz provide me video but i hv not much money