HomePHP Page 2 - Building a Unit of Work in PHP: Applying a Transactional Model to Domain Objects
Making the Unit of Work more functional: clearing entities and performing entity transactions - PHP
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.
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:
/** * 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.