Datamapper pattern in PHP

  • Sharebar

The object-relational gap is a general problem in every OO programming language. Since PHP joined the OO club a while ago this problem also came along with it.

What I mean by object relational gap is the difference between a row in a relational database and an object in an OO language. Even though they have a lot in common, they aren’t the same thing and an OO programmer has to solve this by mapping rows to real objects. In an ideal world a programmer would only be concerned about objects and not database queries (and all the details that come with it) and many smart people have been trying to find the perfect solution to realize this. The result is many persistency frameworks that require the programmer to meta-tag class attributes to map them to database columns or write schemas to do the mapping. Some examples include JDO, JPA, (N)Hibernate, PDO and so on. In my opinion at this moment Microsoft offers the best solution with Linq, because it truly became part of the programming language and the programmer only has to deal with objects, while syntax checking happens at programming/compile time instead of runtime. Hopefully other languages (including PHP will follow), but for now we have to suffice with other solutions.

One of those solutions is the Datamapper design pattern which can be applied in every OO language. The UML schema (may) look like this:

In this example we have a class called UserMapper which has the responsibility to retrieve and store User objects. So basically it handles the mapping of a database row(s) to a User object. The different findBy* methods perform a SELECT query to find matching rows and then use the create(from the abstract superclass) and populate methods to create an object and fill it with the retrieved data.

The save method in the abstract superclass usually checks whether the id attribute of $obj has a value. If not it calls the private implementation of _insert and otherwise _update. Those specifics are up to you as the programmer/architect, but the main idea here is to keep the datamapping logic separated from the (business)logic, which is a good thing. If you want to get real fancy you can add an extra layer, often called the Gateway layer, which contains the actual database queries. You pass a gateway object (i.e. UserGateway) into the mapper class and just have the mapper class call the gateway instance to perform the database queries. In that case the schema would look more like this:

In this scenario you could switch one gateway layer for another, for instance if you want to support a different type of database.

Anyway the version without the extra gateway layer would like something like this in PHP (using Zend_Db for the database adapter):

MapperAbstract:

abstract class MapperAbstract
{
    /**
      * @var Zend_Db_Adapter_Pdo_Mysql
     */
    private $dbAdapter;
 
    /**
     * Create a new instance of the domain object. If the $data parameter is specified
     * then the object will be populated with it.
     * @param array $data
     * @return DomainObjectAbstract
     */
    public function create(array $data = null)
    {
        $obj = $this->_create();
        if ($data)
        {
            $obj = $this->populate($obj, $data);
        }
        return $obj;
    }
 
    /**
     * Save the domain object
     *
     * @param DomainObjectAbstract $obj
     */
    public function save(Application_Model_Abstract $obj)
    {
        if (is_null($obj->getId()))
        {
            $this->_insert($obj);
	}
	else
	{
            $this->_update($obj);
        }
    }
 
    /**
     * Delete the domain object
     * @param DomainObjectAbstract $obj
     */
    public function delete(Application_Model_Abstract $obj)
    {
        $this->_delete($obj);
    }
 
    /**
     * Populate the domain object with the values from the data array.
     *
     * @param DomainObjectAbstract $obj
     * @param array $data
     * @return DomainObjectAbstract
     */
    abstract public function populate(Application_Model_Abstract $obj, array $data);
 
    /**
     * Create a new instance of a domain object
     *
     * @return DomainObjectAbstract
     */
    abstract protected function _create();
 
    /**
     * Insert the domain object into the database
     *
     * @param DomainObjectAbstract $obj
     */
    abstract protected function _insert(Application_Model_Abstract $obj);
 
    /**
     * Update the domain object in persistent storage
     *
     * @param DomainObjectAbstract $obj
     */
    abstract protected function _update(Application_Model_Abstract $obj);
 
    /**
     * Delete the domain object from peristent Storage
     *
     * @param DomainObjectAbstract $obj
     */
    abstract protected function _delete(Application_Model_Abstract $obj);
}

UserMapper:

class UserMapper extends MapperAbstract
{
	private $dbAdapter;
 
	public function __construct($dbAdapter)
	{
		$this->dbAdapter = $dbAdapter;
	}
 
	/**
	* Create a new User domainobject
	*/
	protected function _create()
	{
		return new User();
	}
 
	/**
    * Insert the DomainObject in persistent storage
    *
    * @param DomainObjectAbstract $obj
	*/
	protected function _insert(DomainAbstract $obj)
	{
		// Insert User object into database
		/* @var $obj Application_Model_User */
		$query = 'INSERT INTO users (username,password) VALUES (?,SHA1(?))';
		$ret = $this->dbAdapter->query($query, array($obj->getUsername(),
								$obj->getPassword())
                );
 
		// Assign the new user id to the User object
		$obj->setId($this->dbAdapter->lastInsertId());
	}
 
	/**
	* Update
	*
	* @param DomainObjectAbstract $obj
	*/
	protected function _update(DomainAbstract $obj)
	{
		// Update User object in database
			$query = 'UPDATE users SET username=?,password=SHA1(?) WHERE id=?';
			$ret = $this->dbAdapter->query($query, array($obj->getUsername(),
									$obj->getPassword(),
									$obj->getId()
							));
	}
 
	/**
	* Delete the User from the database
	*
	* @param DomainObjectAbstract $obj
	*/
	protected function _delete(DomainAbstract $obj)
	{
		$query = 'DELETE FROM users WHERE id=?';
		$ret = $this->dbAdapter->query($query, array($obj->getId()));
    }
 
	/**
	* Populate the User (DomainObject) with the data array.
	*
	* @param DomainObjectAbstract $obj
	* @param array $data
	* @return User
	*/
	public function populate(DomainAbstract $obj, array $data)
	{
		if (isset($data['id']))
		{
			$obj->setId($data['id']);
		}
 
		if (isset($data['username']))
		{
			$obj->setUsername($data['username']);
		}
 
		if (isset($data['password']))
		{
			$obj->setPassword($data['password']);
		}
 
		return $obj;
	}
 
 
	/**
	 * Retrieves user by userid
	 * 
	 * @param int $userId
	 * @return User (null if not found)
	 */
	public function findById($userId)
	{
		$sql = "SELECT id,username,password FROM users WHERE id=?";
 
		$data = $this->dbAdapter->fetchRow($sql, array($userId), Zend_Db::FETCH_ASSOC);
 
		$user = null;
 
		if ($data != false)
		{
			$user = $this->create($data);
		}
 
		return $user;
	}
 
	/**
	 * Retrieves user by username
	 * 
	 * @param string $username
	 * @return User (null if not found)
	 */
	public function findByUsername($username)
	{
		$sql = "SELECT id,username,password FROM users WHERE username=?";
 
		$data = $this->dbAdapter->fetchRow($sql, array($username), Zend_Db::FETCH_ASSOC);
 
		$user = null;
 
		if ($data != false)
		{
			$user = $this->create();
		}
 
		return $user;
	}
}

How to use this? To retrieve a user in this setup you have to instantiate a UserMapper object and then ask it to retrieve and create the user domain object for you.

$userMapper = new UserMapper($dbAdapter);
$user = $userMapper->findByUsername('ruben');

Notice how easy it becomes to retrieve a domain object this way. It keeps your business logic code from getting cluttered with dirty database and object mapping details. The same goes for storing the object into the database:

$userMapper = new UserMapper($dbAdapter);
$user = $userMapper->findByUsername('ruben');
$user->setPassword('newpassword');
$userMapper->save($user);

The UserMapper class will figure out whether it has to perform an insert or an update query and do all the hard work.

Hope this helps anyone.

Leave a Reply