cult3

Understanding Doctrine 2 Lifecycle Events

Aug 04, 2014

Table of contents:

  1. What are Events?
  2. The Doctrine Event System
  3. Lifecycle Callbacks
  4. Lifecycle Event Listeners
  5. Conclusion

A common requirement when building applications is the notion that something must be triggered as a consequence of something else happening within the system.

This could be as simply as keeping an updated_at field up-to-date whenever a record in the database is changed, or perhaps sending an email whenever a new user registers for your application.

Doctrine 2 comes bundled with an event system to allow you to implement events as part of your project.

In this week’s tutorial we are going to look at Doctrine 2’s event system and the lifecycle events that can be hooked on to as part of your entity management.

What are Events?

Events are a way of triggering certain actions that should be executed as a consequence of that particular event occurring.

For example, when a new user registers for your application you might want to send them a welcome email.

Typically you wouldn’t want the code to register a new user and the code to send a new email coupled together.

Instead an event system can be used to trigger sending a welcome email whenever a new user registers. This allows you to keep each part of your application to a single responsibility whilst also making it very easy to add or remove what should happen on the occurrence of an event without ever having to touch your existing code.

If you would like to read more about events and how they are technically implemented have a read of the following two previous posts in this series:

The Doctrine Event System

Doctrine triggers a number of events that you can hook on to or listen for. The Doctrine events you have available to you are:

  • preRemove - The preRemove event occurs for a given entity before the respective EntityManager remove operation for that entity is executed. It is not called for a DQL DELETE statement.
  • postRemove - The postRemove event occurs for an entity after the entity has been deleted. It will be invoked after the database delete operations. It is not called for a DQL DELETE statement.
  • prePersist - The prePersist event occurs for a given entity before the respective EntityManager persist operation for that entity is executed. It should be noted that this event is only triggered on initial persist of an entity (i.e. it does not trigger on future updates).
  • postPersist - The postPersist event occurs for an entity after the entity has been made persistent. It will be invoked after the database insert operations. Generated primary key values are available in the postPersist event.
  • preUpdate - The preUpdate event occurs before the database update operations to entity data. It is not called for a DQL UPDATE statement.
  • postUpdate - The postUpdate event occurs after the database update operations to entity data. It is not called for a DQL UPDATE statement.
  • postLoad - The postLoad event occurs for an entity after the entity has been loaded into the current EntityManager from the database or after the refresh operation has been applied to it.
  • loadClassMetadata - The loadClassMetadata event occurs after the mapping metadata for a class has been loaded from a mapping source (annotations/xml/yaml). This event is not a lifecycle callback.
  • preFlush - The preFlush event occurs at the very beginning of a flush operation. This event is not a lifecycle callback.
  • onFlush - The onFlush event occurs after the change-sets of all managed entities are computed. This event is not a lifecycle callback.
  • postFlush - The postFlush event occurs at the end of a flush operation. This event is not a lifecycle callback.
  • onClear - The onClear event occurs when the EntityManager#clear() operation is invoked, after all references to entities have been removed from the unit of work. This event is not a lifecycle callback.

Note: I’ve lifted these descriptions straight from the Doctrine documentation.

You can hook on to these events in two different ways. You can either use Lifecycle Callbacks on your entities or Lifecycle Event Listeners, which are dedicated objects. We’ll look at how to implement both.

Lifecycle Callbacks

The first and easiest method for hooking on to Doctrine Lifecycle events are known as Lifecycle Callbacks. These are simply methods that are implement on your entity.

For example, imagine if we had a User entity and we wanted to maintain an updated_at timestamp whenever that user updater her profile.

First we would start with our basic entity class:

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="users")
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $name;
}

The first thing we need to do is define the $updatedAt class property:

/**
 * @ORM\Column(name="updated_at", type="datetime")
 * @var \DateTime
 */
private $updatedAt;

Next we can define a setUpdatedAt() method that will set the property to a new DateTime instance:

public function setUpdatedAt()
{
    $this->updatedAt = new DataTime;
}

Now in theory we could call the setUpdatedAt() method in our codebase whenever we wanted to set the $updateAt property.

However that would be a terrible solution because we would have to litter our codebase with calls to the method. If we wanted to change this behaviour in the future we would have to find all occurrences of that method call. So we really don’t want to do that!

Instead we can tell Doctrine to automatically call the method whenever this entity is updated.

First add the following annotation to the entity:

/**
 * @ORM\HasLifecycleCallbacks()
 */

Next add the following annotation to the setUpdatedAt() method:

/**
 * @ORM\PreUpdate
 */
public function setUpdatedAt()
{
    $this->updatedAt = new DataTime;
}

HasLifecycleCallbacks() notifies Doctrine that this entity should listen out for callback events. You only need to have this annotation on your entity if you are using callback events.

@ORM\PreUpdate tells Doctrine to automatically call this method on the PreUpdate event.

Now whenever you update a User entity, the updated_at table column for that record will automatically be set! Pretty nice huh?

For really common functionality like maintaining updated at or created at properties, instead of implementing this code on each of your entities, you’re better off creating a trait that you can simply include on the entities that require it.

Or even better yet, if you’re using Mitchell van Wijngaarden’s Doctrine 2 for Laravel package, you can use the Timestamps trait that is already created for you.

Lifecycle Event Listeners

Lifecycle Callbacks are a great solution for a number of situations you will face.

However, Lifecycle Callbacks can also bloat your entity classes with extra methods that really shouldn’t be there.

In the example above, using setUpdatedAt() to set the $updateAt property is fair enough because you are just setting a property on the entity automatically.

But what if you wanted to send an email? We definitely do not want to couple our entity to the ability to send emails!

Instead we can use Lifecycle Event Listeners. A Lifecycle Event Listener is an encapsulated object that listens for an event trigger. This means you can create these little nuggets of code to perform extraneous actions that are triggered by Doctrine’s Event system.

For example, to send an email whenever a user is created we could create a SendWelcomeEmail class that would be triggered on the postPersist event. This class would encapsulate the logic for sending the welcome email.

Doctrine has two ways in which you could implement this functionality, using either a Listener of a Subscriber.

Using a Listener would look like this:

use Doctrine\Common\Persistence\Event\LifecycleEventArgs;

class SendWelcomeEmailListener
{
    public function postPersist(LifecycleEventArgs $event)
    {
        // Send welcome email
    }
}

You would then register the event using Doctrine’s Event manager like this:

// $em is an instance of the Event Manager
$em->getEventManager()->addEventListener(
    [Event::postPersist],
    new SendWelcomeEmailListener()
);

Alternatively you can create an Event Subscriber, like this:

use Doctrine\ORM\Events;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;

class SendWelcomeEmailSubscriber implements EventSubscriber
{
    public function postPersist(LifecycleEventArgs $event)
    {
        // Send welcome email
    }

    public function getSubscribedEvents()
    {
        return [Events::postPersist];
    }
}

You would then register the Event Subscriber like this:

// $em is an instance of the Event Manager
$em->getEventManager()->addEventSubscriber(new SendWelcomeEmailSubscriber());

As you can see, the Event Subscriber implements the EventSubscriber interface and has a getSubscribedEvents() that returns an array of events that should be subscribed to. This is in contrast to an Event Listener where you would subscribe to the events when you register the listener on the Event Manager.

As far as I know, you can use these two different methods interchangeably. Personally I prefer the Event Subscriber method as it forces you to define the events it should listen for inside the class.

Now that we have a class to deal with the logic of the event, we can inject dependencies into the class to deal with sending the email:

public function __construct(Mailer $mailer)
{
    $this->mailer = $mailer;
}

This means we don’t have to couple the event with the mailer and we can very easily switch out the implementation or mock it for testing.

Next we can write the logic to send the email in the postPersist() method:

public function postPersist(LifecycleEventArgs $event)
{
    $em = $event->getEntityManager();
    $entity = $event->getEntity();
}

First we can get the Entity Manger and an instance of the Entity from the LifecycleEventArgs that is passed in as an argument.

Next we can send the email using the Mailer object that we passed in as a dependency:

$mailer->send("welcome", $data, function ($msg) use ($entity) {
    $message->to($entity->email, $entity->name)->subject("Welcome!");
});

However, this Event Subscriber will be triggered for every entity that is persisted by Doctrine. This means if we only wanted to send emails to new User entities, we would have to include an if () statement:

if (!$entity instanceof User) {
    return;
}

If you wanted to send welcome emails to multiple different types of entity you could type hint for an interface instead:

if ( ! $entity->instanceOf Welcomeable ) { return; }

If you were using an Event Listener or Subscriber to update properties of the entity it gets a little bit more complicated.

To do that we need to grab an instance of Doctrine’s UnitOfWork. The UnitOfWork is how the Entity Manager is able to process the changes to your entities.

$uow = $event->getEntityManager()->getUnitOfWork();

$uow->recomputeSingleEntityChangeSet(
    $em->getClassMetaData(get_class($entity)),
    $entity
);

This is basically telling Doctrine that the instance that is currently being held in the Entity Manager should be replaced with the one that is passed in here with the updated properties.

Doctrine’s Unit Of Work is really the power behind Doctrine. I won’t get into the finer technical details of how the Unit Of Work operates because it is both out of the scope of this article and way over my head. I’m sure we will need to take a deeper look into how it works in future articles in this series.

Conclusion

Doctrine has a very good event’s system that allows you to easily hook on and perform certain actions on specific database events.

However, Doctrine events will be triggered for all entities in your project. This is either a really good thing or a really bad thing.

It’s good because it makes it really easy to maintain entity properties like updated_at or created_at across all of your entities. It’s really easy to create a trait and have all of your entities automatically pick up that functionality.

However I don’t think Doctrine’s events are particularly great for triggering emails or other none database stuff actions. As you can see from the example above, having to type hint for certain entities gets messy.

In reality, I would never actually use the email example that I’ve shown in this article. Domain Events should be completely separate from infrastructure events. Hopefully this contrasting example will show you that Doctrine Events are very different to Domain Events and should not be used interchangeably.

Instead we can use an external event system that allows us to keep our Domain Events out of the scope of Doctrine. We’ll be looking at how to do that in a couple of weeks.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.