A Unit of Work in PHP

by jon on August 5, 2008

A couple of days ago I was looking at the Repository Pattern and how you could implement it in PHP.

According to Evans' Domain-Driven-Design book, a Repository depends on a Unit of Work.

Why Bother Using A Unit of Work?

The Unit of Work is an enterprise design pattern that facilitates the persistence of many objects across a single or several data sources. It’s helpful to think of the Unit of Work as a way of putting everything that you would want to update, insert and delete into one bag before sending it to the database.
Functionally, the Unit of Work simplifies transactional requests and helps mitigate concurrency issues on multi-user systems.

I wrote a Unit of Work that depends on Doctrine. Assuming we have a Project model with a name property, the consuming class can perform operations like so:

 
$t = Doctrine::getTable('Project');
$lastProjects = $t->findByName('new project');
 
$unitOfWork = new UnitOfWork();
// prepare an UPDATE
    $lastProjects[0]->username = 'old project';
    $unitOfWork->RegisterModelForCreateOrUpdate($lastProjects[0]);
 
// prepare a CREATE
    $project = new Project();
    $project->name = 'new project name';
 
    $unitOfWork->RegisterModelForCreateOrUpdate($project);
 
// prepare a DELETE
    $unitOfWork->RegisterModelForDelete($lastProjects[3]);
 
// perform the transaction
$unitOfWork->CommitAll();
 

A commit already leverages the existing Doctrine connection and handles the transaction for you, ensuring that the Unit of Work either commits, or throws an Exception.

The UnitOfWork is also an excellent place to include logging code or event additional column data (such as UpdatedAt and UpdatedBy which might exist in all Doctrine models for concurrency / logging purposes.)

I haven't found a better way of doing object-comparison in PHP arrays, however I've pulled out that code into existsInCollections() if anyone has any ideas.

 
 
class UnitOfWork {
 
	/**
	 * Collection of models to be persisted
	 *
	 * @var array Doctrine_Record
	 */
	protected $createOrUpdateCollection = array();
 
	/**
	 * Collection of models to be persisted
	 *
	 * @var array Doctrine_Record
	 */
	protected $deleteCollection = array();
 
	/**
	 * Add a model object to the create collection
	 *
	 * @param Doctrine_Record $model
	 */
	public function RegisterModelForCreateOrUpdate($model)
	{
		// code to check to see if the model exists already
        if ($this->existsInCollections($model))
		    throw new Exception('model already in another collection for this transaction');
 
		// no? add it
			$this->createOrUpdateCollection[] = $model;
	}
 
	/**
	 * Add a model object to the delete collection
	 *
	 * @param Doctrine_Record $model
	 */
	public function RegisterModelForDelete($model)
	{
		// code to check to see if the model exists already
		    if ($this->existsInCollections($model))
    		    throw new Exception('model already in another collection for this transaction');
 
		// no? add it
			$this->deleteCollection[] = $model;
 
	}
 
	/**
	 * Clear the Unit of Work
	 *
	 */
	public function ClearAll()
	{
		$this->deleteCollection = array();
		$this->createOrUpdateCollection = array();
	}
 
	/**
	 * Perform a Commit and clear the Unit Of Work. Throw an Exception if it fails and roll back.
	 *
	 */
	public function CommitAll()
	{
    	$manager = Doctrine_Manager::getInstance();
    	$conn = array_pop($manager->getConnections()); 
 
    	try {
            $conn->beginTransaction();
 
            $this->performCreatesOrUpdates($conn);
            $this->performDeletes($conn);
 
            $conn->commit();
    	}
    	catch(Doctrine_Exception $e)
    	{
    	    $conn->rollback();
        }	    
 
        $this->ClearAll();
	}
 
	private function performCreatesOrUpdates($conn)
	{
	    foreach ($this->createOrUpdateCollection as $model)
			$model->save($conn);
 
	}
 
	private function performDeletes($conn)
	{
		foreach ($this->deleteCollection as $model)
			$model->delete($conn);
	}
 
	private function existsInCollections($model)
	{
 
	   foreach ($this->createOrUpdateCollection as $m)
	   {
	        if ($model->__toString() == $m->__toString())
	            return true;
	   }
 
	   foreach ($this->deleteCollection as $m)
	   {
	        if ($model->__toString() == $m->__toString())
	            return true;
	   }
 
	   return false;
	}
 
}
 

{ 4 comments… read them below or add one }

Simon May 3, 2009 at 2:21 pm

Thank you!
That will help me developing my MVC-Framework

Simon

Fedyashev Nikita December 9, 2009 at 4:27 pm

Thanks, Jon!

It looks like you’re the one who write about “Unit of work” pattern implementation in PHP at the moment :)

thanks for explanation

jb January 21, 2010 at 8:11 am

OMG Fedyashev is right … you’re the only one that is actually proposing an implementation … thanks^^

Jack February 25, 2010 at 3:52 am

Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module – is your UnitOfWork class that leverages Doctrine a better choice then doctrine_record $rec->save() with signature
public function save(Doctrine_Connection $conn = null)
{
if ($conn === null) {
$conn = $this->_table->getConnection();
}
$conn->unitOfWork->saveGraph($this);
}

Leave a Comment

Previous post:

Next post: