Symfony2

Using Doctrine lifecycle callbacks with Symfony2

After my previous post on refactoring controllers, Khepin made a great point about using lifecycle callbacks on your entities to further reduce your code footprint. I’ll reproduce it here for those who don’t care to look it up:

A different take on this would be to use lifecycle callbacks on models. You can tell a model to call a special action when its being created / updated / deleted etc … so whenever a post is deleted it could call a function that would do: $this->author->decreasePostCount() automatically.

This way nothing even appears in the controllers related to this. All you do in the controller is delete one post or all the post in a topic.

Same way, using cascade delete on the topic would mean you’d only have to call deleteTopic and the deletion of related posts would happen behind the scenes.

Lifecycle callbacks, if you’re not aware, are methods on your entities that you register with Doctrine, and are called at specific points in the entity’s existence during a request (its “lifecycle”). A lifecycle callback differs from a lifecycle event in that callbacks do not receive an EventArgs argument: when a lifecycle callback is triggered, it only knows about the object on which it was called.

Let’s clarify with an example. I use annotations in all my entities; check the docs for usage specific to your preferred mapping method.

[code language=”php”]
/**
* @ORMEntity
* @ORMHasLifecycleCallbacks
*/
class Post
{
/**
* @ORMId @ORMColumn(type="integer")
* @ORMGeneratedValue
*/
protected $id;

/**
* @ORMColumn(type="datetime")
*/
protected $createdAt;

/**
* @ORMManyToOne(targetEntity="User", cascade={"persist"})
*/
protected $author;

// … other properties, getters and setters

/**
* @ORMPrePersist
*/
public function onPrePersist()
{
$this->createdAt = new DateTime();
}
}
[/code]

This is a trivial example, of course. Before the entity is persisted for the first time, Doctrine will call any methods marked with the PrePersist callback (note that the method name is arbitrary: it needs only to be public and registered as a callback). In this case, the Post#onPrePersist() method updates the entity with a new creation timestamp just before it is saved to the database. Let’s add a slightly less trivial example:

[code language=”php”]
/**
* @ORMPreRemove
*/
public function onPreRemove()
{
$this->author->modifyPostCount(-1);
}
[/code]

Right before the entity is removed from the database, the author has their post count decremented by one. You will need to configure your entities to cascade persistence, or else manually persist the User entity representing the post author, but this is an excellent way to move code from your controllers onto your objects.

With relation to the example brought up in my previous post, when a topic is deleted, all the posts under it must be deleted as well. If you set up your entities to cascade delete from topic to its child posts, any lifecycle callbacks registered on those entities will be triggered as well. That would reduce the code footprint of TopicManager#deleteTopic() from this…

[code language=”php”]
public function deleteTopic(Topic $topic, $reason)
{
$user = $topic->getCreator();
$user->incrementTopicCount(-1);
$this->em->persist($user);
foreach ($topic->getPosts() as $post) {
$this->postManager->deletePost($post, $reason);
}
$this->sendTopicDeletionEmail($topic, $reason);
$this->em->remove($topic);
$this->em->flush();
}
[/code]

… to this:

[code language=”php”]
public function deleteTopic(Topic $topic, $reason)
{
$this->sendTopicDeletionEmail($topic, $reason);
$this->em->remove($topic);
$this->em->flush();
}
[/code]

The Topic#onPreRemove() method would be triggered, decrementing the topic owner’s count, and since the delete relationship is cascaded to child posts, their onPreRemove() methods would be fired as well. As you can see, this results in much cleaner code.

Be aware that lifecycle callbacks are not the holy grail of entity management, applicable in every situation: as I previously mentioned, the callbacks are not made aware of application state, which limits their scope to dealing with the entity and its relationships directly (you’ll notice that the topic deletion email is still sent from the manager method). Nor should you configure your entities to cascade on relationships without considering the design and performance implications of doing so. But, well-planned use can make your life a lot easier.

Further Reading on Symfony2

2 thoughts on “Using Doctrine lifecycle callbacks with Symfony2

Leave a Reply