Normalising DateTimes with Doctrine Events

Two of the biggest pains in development for me are character encoding and working with date time information. The former continually bites me when I least expect it, although making sure everything in a project is UTF-8 encoded seems to go a long way to mitigating this problem. Working with date time information is also subtly tricky. I’ve recently been working on an app that stores a lot of information about sports statistics. Part of this involves storing information on when a game , trades on players, etc took place. Since this information can be entered in any number of different timezones I thought I would write about the approach that we took to normalise the data as it is inserted into the database.

The Problem

In collecting data for the application we are getting a lot of date time information that can be entered in multiple timezones. When the information is retrieved we allow the user to select the timezone that they want to view the data in. We quickly decided that the only way to go was to store all date time information in the UTC timezone but we still needed a way to seamlessly convert date time data into that timezone when it is saved. The project is being built using a combination of Symfony2 and Doctrine2. We discovered that Doctrine will always populate DateTime objects for entities with a timezone that matches the default timezone set on the server when entities are hydrated. Solving half of the problem just involved making sure that UTC was set as the default server timezone. The advice of the Doctrine project is to convert instances of DateTime to use UTC before they are persisted in the database and to store the timezone information separately and we wanted to find a way to do this that was as simple and maintainable as possible.

Potential Solutions

The first solution we considered was to perform the timezone conversions at the entity level but we quickly abandoned this idea for a number of reasons. Firstly, this would involve a huge amount of code duplication. Every setter in every entity that dealt with DateTime objects would need to perform a conversion on DateTime instances that are saved. This would either involve lot of duplicated code or some complicated inheritance structure. Worse, if a developer created an entity that didn’t implement this behavior we could quickly find that we had corrupted data in the database.

The second solution we considered was an extension of the first. Doctrine allows you to define a series of lifecycle callbacks on entities that can be fired when certain events take place. We thought about adding PrePersist and PreUpdate callbacks that would perform the timezone conversion automatically whenever an entity is saved or updated by the entity manager. However this solution suffers from the same problems as the one above in that it requires code duplication and/or inheritance. It would also mean that developers would need to remember to add this behavior to all entities that need to work with DateTimes, leading to problems if this is forgotten.

The next solution we considered was possibly the worst. We thought about enforcing the timezone changes at the application level, before the entities were ever given to the entity manager. This was very quickly discarded due to the obvious code bloat and possibilities for duplication that exist.

The Solution

The solution we hit on was to leverage Doctrine’s system of event listeners to help us do the work. Doctrine allows you to register listeners with the entity manager that are called whenever certain events occur. We created an event listener that is triggered on the onFlush event. This collects all entities that are scheduled for insertion or update and uses Reflection to iterate over their private and protected properties looking for DateTime objects. Any that are found have their timezones set to UTC. The code for the event listener class is below:


<?php
namespace Events\Doctrine;

use ReflectionObject;
use ReflectionProperty;
use DateTime;
use DateTimeZone;
use Doctrine\ORM\Event\OnFlushEventArgs;
/**
* Class to provide an event listener that processes entities when they are persisted or updated
*/
class DateTimeListener
{
/**
* Method to listen to the onFlush event
* When this event is triggered the method will look at all entities
* scheduled for update or insertion.
* It will retrieve all DateTime instance from these and ensure they are
* set to UTC timezone
* @param Doctrine\ORM\EventListener\OnFlushEventArgs $args
*
* @return void
*/
public function onFlush(OnFlushEventArgs $args)
{
    $zone = new DateTimeZone('UTC');
    //Get hold of the unit of work.
    $entityManager = $args->getEntityManager();
    $unitOfWork = $entityManager->getUnitOfWork();
    //Get hold of the entities that are scheduled for insertion or update
    $entities = array_merge($unitOfWork->getScheduledEntityInsertions(), $unitOfWork->getScheduledEntityUpdates());
    foreach ($entities as $entity) {
        $reflect = new ReflectionObject($entity);
        foreach ($reflect->getProperties(ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED) as $prop) {
            $prop->setAccessible(true);
            $value = $prop->getValue($entity);
            if (! $value instanceof DateTime || $value->getTimezone()->getName() === 'UTC') {
                $prop->setAccessible(false);
                continue;
            }
            $value->setTimezone($zone);
            $prop->setValue($entity, $value);
            $prop->setAccessible(false);
            $unitOfWork->recomputeSingleEntityChangeSet($entityManager->getClassMetadata(get_class($entity)), $entity);
        }
    }
}
}

What I like about this solution is that it doesn’t require any intervention on the part of the developer higher up in the application. Entities are simply created or edited and no special attention needs to be paid to time zones saved in DateTime instances. However, this solution isn’t without it’s own issues. Firstly, the fact that this is done by the entity manager at the event level gives the whole thing a touch of ‘magic’. We’ll need to make sure this is well documented so that developers working on this code in the future don’t end up spending time trying to work out how this behavior is happening. Secondly, using Reflection and iterating over every property of every entity that is being saved or updated isn’t the fastest thing in the world to do. At this stage I’m not too concerned as, like most systems, there will be many more read than write operations in this application. If this does become a problem we could possibly introduce some sort of class map that we could use to decide which entities have DateTime instances in them that we might need to modify. It’s possible that the class meta data, held by the entity manager, would be able to give us this information. For now I’m quite happy with this solution and with the event system in Doctrine in general. We’ve also used Doctrine events to automatically calculate some statistics data for us when entities are flushed but that’s a topic for another post.

4 thoughts on “Normalising DateTimes with Doctrine Events

    1. Hi Daniel. We did consider this approach but my concern was that all developers would need to remember to use the custom type in the future. Still, it’s a good and perfectly valid solution :-)

Leave a Reply