Building a Unit of Work in PHP: Applying a Transactional Model to Domain Objects

Are you trying to optimize the way your web application handles domain objects, and not finding a good solution? If caching won’t help, and data and identity mappers won’t suit your needs, you might want to look at using a Unit of Work pattern. What is a UoW? Keep reading to learn how it can help you.

One of the most common issues that must be addressed during the development of a large web application that exposes a prolific domain layer is how to optimize the operations that are performed on domain objects. Clearly, if numerous objects need to be fetched, saved to or deleted from the underlying persistence mechanism, which usually rests on a relational database, the overhead can be huge. In fact, it may bring the application to its knees.

Of course, a caching system in any of its flavors may be helpful in a case like this, and I’d be the first to pick one that fits my needs. However, sooner or later the objects in question must be retrieved, persisted or even removed, which means that multiple (and expensive) trips to the database are unavoidable.

There’s no need to panic, though, as there exist some approaches to solving this problem, at least partially. For instance, a proper implementation of a data mapper together with an identity map can help to tackle the issue on two well-differentiated fronts: on one hand, the mapper would permit you to decouple domain objects from the persistence layer, thus addressing the so-called impedance mismatch; while on the other hand, the identity map would keep track of which objects have been fetched, and also assure that each of them would be fetched only once.

The major drawback with this approach is the intrinsic nature of an identity map, as it does reduce database trips, but only for retrieval operations. What about object insertions, updates and deletions? This is where an enterprise-level pattern known as Unit of Work (UoW) can help. Simply put, a UoW is an additional layer, usually placed between the domain and the mapping ones, which allows you to hold multiple domain objects in memory and “mark” them for further insertion, update or removal. The objects are queued deliberately in RAM until the actual database operations are performed in one go through a single method call.

While this transactional model permits you to save database trips, its functionality comes with an extra cost worth stressing. First off, a UoW requires you to implement an additional layer upon the existing ones, which in many cases may be overkill. And last but not least, it demands a careful manipulation of in-memory objects to maintain database integrity. With that said, you should consider whether or not your web project really needs to implement a UoW, given the hard upfront work that it demands. In short, be conscientious and evaluate its pros and cons with due care.

In addition, like any other pattern available out there, a UoW is “language agnostic,” which means that it can be implemented in any programming language, including PHP. Since the experience can be  instructive, especially for those PHP developers wanting to be in touch with the trends imposed by modern web application development, in this article series I’ll be building a simple UoW from scratch, so that you can grasp its underlying logic and see the actual benefits that it brings to the table.

Ready to take the first step of this hopefully educational journey? Then let’s get started!

Getting started: defining an abstract Unit of Work

As noted in the introduction, a UoW queues multiple domain objects in memory, which are later inserted, updated or even removed from the persistence layer in a single operation. This resembles the transactional model present in many RDBMS. The question is, how does the UoW know which objects need to be inserted, updated or deleted?

In reality, there are several ways to accomplish this, but in many cases the tracking process is ruled by a simple convention that you’ll find easy to follow. First, new domain objects created in memory will be marked “new,” thus set for further insertion; second, objects previously fetched from the storage mechanism and then modified in some way will be marked “dirty” and set for further update. Finally, objects explicitly marked “removed” will obviously be deleted.

In addition, it’s possible to implement a fourth state called “clean.” Objects tracked this way will be left untouched. Of course, client code consuming the UoW can swap these states at any time according to specific requirements; for now, don’t feel concerned about how this will be done, as the topic will be discussed in depth in upcoming tutorials of this series.

Having outlined basically how a UoW keeps track of domain objects, it’s time to start creating a class capable of performing the aforementioned tasks. Below I defined a basic, yet functional abstract UoW, whose partial implementation looks like this: 
  
(UnitOfWorkAbstract.php)

<?php

abstract class UnitOfWorkAbstract
{
    protected $_newEntities = array();
    protected $_cleanEntities = array();
    protected $_dirtyEntities = array();
    protected $_removedEntities = array();
    protected $_dataMapper;
   
    /**
     * Class constructor
     */
    public function __construct(DataMapperAbstract $dataMapper)
    {
        $this->_dataMapper = $dataMapper;
    }
   
    /**
     * Get the data mapper the Unit of Work uses
     */         
    public function getDataMapper()
    {
        return $this->_dataMapper;
    }
   
    /**
     * Mark an entity ‘new’
     */
    public function markNew(EntityAbstract $entity)
    {
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
        }
        if (!in_array($entity, $this->_newEntities, true)) {
            $this->_newEntities[] = $entity;
        }
    }
   
    /**
     * Remove an entity previously marked ‘new’
     */
    protected function _removeNew(EntityAbstract $entity)
    {
        if (in_array($entity, $this->_newEntities, true)) {
            $newEntities = array();
            foreach ($this->_newEntities as $_newEntity) {
                if ($entity !== $_newEntity) {
                    $newEntities[] = $_newEntity;
                }
            }
            $this->_newEntities = $newEntities;
        }
    }
   
    /**
     * Get all the ‘new’ entities
     */
    public function getNewEntities()
    {
        return $this->_newEntities;
    }
   
    /**
     * Mark an entity ‘clean’
     */
    public function markClean(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
            if (!array_key_exists($id, $this->_cleanEntities)) {
                $this->_cleanEntities[$id] = $entity;
            }
        }
    }
   
    /**
     * Get all the ‘clean’ entities
     */
    public function getCleanEntities()
    {
        return $this->_cleanEntities;
    }
   
    /**
     * Mark an entity ‘dirty’
     */
    public function markDirty(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
            if (!array_key_exists($id, $this->_dirtyEntities)) {
                $this->_dirtyEntities[$id] = $entity;
            }
        }
    }
   
    /**
     * Get all the ‘dirty’ entities
     */
    public function getDirtyEntities()
    {
        return $this->_dirtyEntities;
    }
   
    /**
     * Mark an entity ‘removed’
     */
    public function markRemoved(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (!array_key_exists($id, $this->_removedEntities)) {
                $this->_removedEntities[$id] = $entity; 
            }
        }
    }
   
    /**
     * Get all the ‘removed’ entities
     */
    public function getRemovedEntities()
    {
        return $this->_removedEntities;
    }
}

Don’t let the lengthy definition of the previous class overwhelm you; the logic implemented by its methods is easy to follow. As you can see from the above code snippet, each tracking process in domain objects is performed by four discrete methods called “markNew(),” “markClean(),” “markDirty()” and “markRemoved(),” which store the marked objects (in this case entities) in separate arrays. Also, the methods have some complementary getters, which I’ll skip over at this time, since they can be grasped in a snap.

Although the definition of this abstract UoW is incomplete right now, it should be clearer for you to see how it does its thing; each domain object is marked “new,” “clean,” “dirty” or “removed” via plain array manipulation. It’s that simple. Also, you may have noticed that the class’ constructor injects an instance of an abstract mapper. Once again, don’t worry about the implementation of this collaborator, since it will be covered in detail in another installment. 

So far, so good. In its current incarnation, the UoW is capable of assigning different states to the inputted entities, which is all well and good. You’re now wondering where this class implements the transactional model, right? Well, to be frank, this functionality hasn’t been added yet, but it’ll be delegated to a separate method that will be discussed in the following section. 

So click on the link below and keep reading.

{mospagebreak title=Making the Unit of Work more functional: clearing entities and performing entity transactions}

To leverage the full potential of the UoW just created, it’s necessary to provide it with the ability to insert, update and remove previously-marked entities through a single method call. Since these objects are stored in protected arrays, achieving this is much simpler than you might think.

But if you’re still skeptical, look at the code fragment below. It shows the definition of this brand new method, together with a few additional ones: 

/**
 * Clear all the ‘new’ entities
 */ 
public function clearNew()
{
    $this->_newEntities = array();
    return $this;
}

/**
 * Clear all the ‘clean’ entities
 */
public function clearClean()
{
    $this->_cleanEntities = array();
    return $this;
}

/**
 * Clear all the ‘dirty’ entities
 */ 
public function clearDirty()
{
    $this->_dirtyEntities = array();
    return $this;
}

/**
 * Clear all the ‘removed’ entities
 */ 
public function clearRemoved()
{
    $this->_removedEntities = array();
    return $this;
}

/**
 * Clear all the entities stored in the Unit Of Work
 */
public function clearAll()
{
    $this->clearNew()
         ->clearClean()
         ->clearDirty()
         ->clearRemoved();
}

/**
 * Find an entity by its ID (implements an identity map)
 */
public function findById($id)
{
    if (array_key_exists($id, $this->_cleanEntities)) {
        return $this->_cleanEntities[$id];
    }
    if ($entity = $this->_dataMapper->findById($id)) {
        $this->markClean($entity);
        return $entity;
    }
    return null;  
}

/**
 * Find all the entities
 */
public function findAll()
{
    $collection = $this->_dataMapper->findAll();
    if ($collection !== null) {
        foreach ($collection as $entity) {
            $this->markClean($entity);
        }
        return $collection;
    }
    return null;
}

/**
 * Commit all the pending entity operations in one go (create, update, delete)
 */
public function commit()
{  
    // save all the ‘new’ entities
    if (!empty($this->_newEntities)) {
        foreach ($this->_newEntities as $_newEntity) {
            $this->_dataMapper->insert($_newEntity);           
        }
    }
    // update all the ‘dirty’ entities
    if (!empty($this->_dirtyEntities)) {
        foreach ($this->_dirtyEntities as $_dirtyEntity){
            $this->_dataMapper->update($_dirtyEntity);
        }
    }
    // delete all the ‘removed’ ‘entities
    if (!empty($this->_removedEntities)) {
        foreach ($this->_removedEntities as $_removedEntity) {
            $this->_dataMapper->delete($_removedEntity);
        }
    }
}

Definitely, things are becoming much more interesting! As you can see above, now the UoW not only implements some convenience methods that allow you to selectively clear previously-marked entities, but it also defines a couple of typical finders, such as “findById()” and “findAll().” Although it’s worthwhile to analyze these extra methods in detail, you should pay attention to the real workhorse here, which is obviously “commit().”

The existence of this method justifies the development of the entire class, since its implementation gives a real meaning to the transactional nature of a UoW. In this case, the method iterates over each set of marked entities and performs the corresponding operation (insert, update or delete) through the injected mapper. This shows how convenient it can be to use a UoW to reduce the accesses to the persistence layer to a bare minimum. Not too bad, huh?

Since you’ve grasped how these methods do their thing, it’s time to show the UoW’s full source code. This will be done below, so just keep reading.  

The Unit of Work’s full source code

As I promised above, below I included the finished version of the abstract UoW, which will make it easier for you to understand the role played by each method that it defines. Check it out:

(UnitOfWorkAbstract.php)

<?php

abstract class UnitOfWorkAbstract
{
    protected $_newEntities = array();
    protected $_cleanEntities = array();
    protected $_dirtyEntities = array();
    protected $_removedEntities = array();
    protected $_dataMapper;
   
    /**
     * Class constructor
     */
    public function __construct(DataMapperAbstract $dataMapper)
    {
        $this->_dataMapper = $dataMapper;
    }
   
    /**
     * Get the data mapper the Unit of Work uses
     */         
    public function getDataMapper()
    {
        return $this->_dataMapper;
    }
   
    /**
     * Mark an entity ‘new’
     */
    public function markNew(EntityAbstract $entity)
    {
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
        }
        if (!in_array($entity, $this->_newEntities, true)) {
            $this->_newEntities[] = $entity;
        }
    }
   
    /**
     * Remove an entity previously marked ‘new’
     */
    protected function _removeNew(EntityAbstract $entity)
    {
        if (in_array($entity, $this->_newEntities, true)) {
            $newEntities = array();
            foreach ($this->_newEntities as $_newEntity) {
                if ($entity !== $_newEntity) {
                    $newEntities[] = $_newEntity;
                }
            }
            $this->_newEntities = $newEntities;
        }
    }
   
    /**
     * Get all the ‘new’ entities
     */
    public function getNewEntities()
    {
        return $this->_newEntities;
    }
   
    /**
     * Mark an entity ‘clean’
     */
    public function markClean(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
            if (!array_key_exists($id, $this->_cleanEntities)) {
                $this->_cleanEntities[$id] = $entity;
            }
        }
    }
   
    /**
     * Get all the ‘clean’ entities
     */
    public function getCleanEntities()
    {
        return $this->_cleanEntities;
    }
   
    /**
     * Mark an entity ‘dirty’
     */
    public function markDirty(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_removedEntities)) {
                unset($this->_removedEntities[$id]);
            }
            if (!array_key_exists($id, $this->_dirtyEntities)) {
                $this->_dirtyEntities[$id] = $entity;
            }
        }
    }
   
    /**
     * Get all the ‘dirty’ entities
     */
    public function getDirtyEntities()
    {
        return $this->_dirtyEntities;
    }
   
    /**
     * Mark an entity ‘removed’
     */
    public function markRemoved(EntityAbstract $entity)
    {
        $this->_removeNew($entity);
        $id = $entity->id;
        if ($id !== null) {
            if (array_key_exists($id, $this->_cleanEntities)) {
                unset($this->_cleanEntities[$id]);
            }
            if (array_key_exists($id, $this->_dirtyEntities)) {
                unset($this->_dirtyEntities[$id]);
            }
            if (!array_key_exists($id, $this->_removedEntities)) {
                $this->_removedEntities[$id] = $entity; 
            }
        }
    }
   
    /**
     * Get all the ‘removed’ entities
     */
    public function getRemovedEntities()
    {
        return $this->_removedEntities;
    }
   
    /**
     * Clear all the ‘new’ entities
     */ 
    public function clearNew()
    {
        $this->_newEntities = array();
        return $this;
    }
   
    /**
     * Clear all the ‘clean’ entities
     */
    public function clearClean()
    {
        $this->_cleanEntities = array();
        return $this;
    }
   
    /**
     * Clear all the ‘dirty’ entities
     */ 
    public function clearDirty()
    {
        $this->_dirtyEntities = array();
        return $this;
    }
   
    /**
     * Clear all the ‘removed’ entities
     */ 
    public function clearRemoved()
    {
        $this->_removedEntities = array();
        return $this;
    }
   
    /**
     * Clear all the entities stored in the Unit Of Work
     */
    public function clearAll()
    {
        $this->clearNew()
             ->clearClean()
             ->clearDirty()
             ->clearRemoved();
    }
   
    /**
     * Find an entity by its ID (implements an identity map)
     */
    public function findById($id)
    {
        if (array_key_exists($id, $this->_cleanEntities)) {
            return $this->_cleanEntities[$id];
        }
        if ($entity = $this->_dataMapper->findById($id)) {
            $this->markClean($entity);
            return $entity;
        }
        return null;  
    }
   
    /**
     * Find all the entities
     */
    public function findAll()
    {
        $collection = $this->_dataMapper->findAll();
        if ($collection !== null) {
            foreach ($collection as $entity) {
                $this->markClean($entity);
            }
            return $collection;
        }
        return null;
    }
   
    /**
     * Commit all the pending entity operations in one go (create, update, delete)
     */
    public function commit()
    {  
        // save all the ‘new’ entities
        if (!empty($this->_newEntities)) {
            foreach ($this->_newEntities as $_newEntity) {
                $this->_dataMapper->insert($_newEntity);           
            }
        }
        // update all the ‘dirty’ entities
        if (!empty($this->_dirtyEntities)) {
            foreach ($this->_dirtyEntities as $_dirtyEntity){
                $this->_dataMapper->update($_dirtyEntity);
            }
        }
        // delete all the ‘removed’ ‘entities
        if (!empty($this->_removedEntities)) {
            foreach ($this->_removedEntities as $_removedEntity) {
                $this->_dataMapper->delete($_removedEntity);
            }
        }
    }  
}

The work is done, at least for the moment. At this stage you should have a more accurate idea of what a Unit of Work is and how it can be used for optimizing transactions with domain objects.

But this is only the beginning of the journey. I plan to show you how to use this sample UoW in a pretty realistic scenario. This will done progressively in upcoming tutorials.  

Wrapping up

In this first episode of the series, I provide you with a humble introduction to what a Unit of Work is and how to implement it in a step-by-step fashion in PHP. While a lot of material has been covered so far, there’s still a long road ahead of us. The base UoW just created takes a few additional collaborators, namely an abstract mapper and the corresponding entities, whose originating classes haven’t been defined yet.

In the next tutorial I’m going to show the source code of these classes, so you can see more clearly how they can be put to work side by side with the earlier UoW.

Don’t miss the next part!

[gp-comments width="770" linklove="off" ]

chat