Roll Your Own Repository in PHP: Data Mapper Finishing Touches

In this fourth tutorial of the series, I added three methods to the data mapper class. They will be used for saving and deleting the table rows related to a specified entity. With this parent class fully implementing the methods declared by the “DataMapperInterface” interface, it will be easy to create a refined subclass that can specifically map user objects.

If you’re a PHP developer who is just starting out with Domain-driven Design (DDD) and want to learn in a step-by-step fashion how to build a repository from scratch, then look no further; you’ve come to the right place. In this set of tutorials, you’ll be guided through the development of a sample web application which will use the functionality provided by a basic repository to manipulate collections of user objects in a simple manner.

If you’ve read the previous installments, you’re already familiar with the concepts that surround the implementation of the user repository. In those parts I progressively created the domain, data access and mapping layers of this example application, setting up the scenario for building the repository.

Well, to be frank the scenario isn’t completely set yet; the abstract mapper class that composes the application’s mapping layer still must implement some additional methods that permit us to insert, update and delete the table records associated with a specified entity. To address this issue and get this layer finally up and running, in this article I’m going to add to this class the extra methods just mentioned.

Want to see how this will be accomplished? Then begin reading now!

Building a basic mapping layer: a quick look at the previous data mapper

Since my plan here is to complete the definition of the data mapper class created in the previous part of the series, it’d be useful to take a brief look at this class in its current state. In consonance with this,  here’s the interface implemented by this generic mapper:

(DataMapperInterface.php)

<?php

interface DataMapperInterface
{
    public function findById($id);
   
    public function findAll();
   
    public function search($criteria);
   
    public function insert(EntityAbstract $entity);
   
    public function update(EntityAbstract $entity);
   
    public function delete($id);         
}

Definitely, the contract defined by the above “DataMapperInterface” interface is very easy to follow, right? So, move on and look at the source code of the mapper, which is an implementer of the pertinent interface:

(DataMapperAbstract.php)

<?php

abstract class DataMapperAbstract implements DataMapperInterface
{
    protected $_adapter;
    protected $_collection;
    protected $_entityClass;
    protected $_entityTable; 
        
    /**
     * Class constructor
     */
    public function __construct(MySQLAdapter $adapter, CollectionAbstract $collection, array $entityOptions = array())
    {
        $this->_adapter = $adapter;
        $this->_collection = $collection;
        if (isset($entityOptions['entityClass'])) {
            $this->setEntityClass($entityOptions['entityClass']);
        }
        if (isset($entityOptions['entityTable'])) {
            $this->setEntityTable($entityOptions['entityTable']);
        }
        $this->init();
    }
   
    /**
     * Initialize the data mapper here
     */
    public function init(){}
   
    /**
     * Get the instance of the database adapter
     */
    public function getAdapter()
    {
        return $this->_adapter;
    }
   
    /**
     * Get the collection the mapper uses
     */
    public function getCollection()
    {
        return $this->_collection;
    }
   
    /**
     * Set the class for reconstructing entities
     */
    public function setEntityClass($entityClass)
    {
        if (!class_exists($entityClass, false)) {
            throw new DataMapperException(‘The specified entity class ‘ . $entityClass . ‘ does not exist.’);
        }
        $this->_entityClass = $entityClass;
    }
   
    /**
     * Get the class for reconstructing entities
     */
    public function getEntityClass()
    {
        return $this->_entityClass;
    }
   
    /**
     * Set the entity database table the mapper works with
     */
    public function setEntityTable($entityTable)
    {
        if (!is_string($entityTable) || empty($entityTable)) {
            throw new DataMapperException(‘The specified entity table ‘ . $entityTable . ‘ is invalid.’);
        }
        $this->_entityTable = $entityTable;
    }
   
    /**
     * Get the entity database table the mapper works with
     */
    public function getEntityTable()
    {
        return $this->_entityTable;
    }
   
    /**
     * Find an entity by its ID
     */
    public function findById($id)
    {
        $id = (int) $id;
        $this->_adapter->select($this->_entityTable, "id = $id");
        if ($data = $this->_adapter->fetch()) {
            return new $this->_entityClass($data);
        }
        return null;
    }
   
    /**
     * Find all the entities
     */
    public function findAll()
    {
        $this->_collection->clear();
        $this->_adapter->select($this->_entityTable);
        while ($data = $this->_adapter->fetch()) {
            $this->_collection->add($data['id'], new $this->_entityClass($data));   
        }
        return $this->_collection->count() !== 0 ?
               $this->_collection :
               null;
    }
   
    /**
     * Find all the entities that match the specified criteria
     */
    public function search($criteria)
    {
        $this->_collection->clear();
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $this->_collection->add($data['id'], new $this->_entityClass($data));   
        }
        return $this->_collection->count() !== 0 ?
               $this->_collection :
               null;
    }
}

 

(DataMapperException.php)

<?php

class DataMapperException extends Exception{}

As you can see from the above code fragment, the “DataMapperAbstract” class includes a set of finders that allow you to find entities by their ID or even retrieve all of them in one go. While this is effective, the class in its current state is still unfinished. It doesn’t implement all of the methods declared by the “DataMapperInterface” interface. Too bad.

To solve this issue, in the following section I’m going to add these methods to the mapper, thus providing it with the ability to save and delete the table rows associated with a given entity.

To learn how these additional methods will be defined, click on the link that appears below and keep reading.     

{mospagebreak title=Saving and deleting entity-related table records with additional methods}

In reality, giving the earlier mapper the ability to save and remove the table rows related to a specified entity is a straightforward process that can be tackled in a snap. Don’t believe me? Take a peek at the following methods, which perform these tasks in an approachable way:

/**
 * Insert a new row in the table corresponding to the specified entity
 */
public function insert(EntityAbstract $entity)
{
    $data = $entity->toArray();
    return $this->_adapter->insert($this->_entityTable, $data);
}

/**
 * update the row in the table corresponding to the specified entity
 */
public function update(EntityAbstract $entity)
{
    $id = (int) $entity->id;
    $data = $entity->toArray();
    unset($data['id']);
    return $this->_adapter->update($this->_entityTable, $data, "id = $id");
}
 
/**
 * Delete the row in the table corresponding to the specified entity or ID
 */
public function delete($id)
{
    if ($id instanceof EntityAbstract) {
        $id = (int) $id->id;
    }
    return $this->_adapter->delete($this->_entityTable, "id = $id");
}

Mission accomplished. Thanks to the functionality aggregated by the above methods, the mapper class is capable of retrieving entities, as well as saving and removing them from their associated MySQL table. But wait a minute! Showing these extra methods as if they were isolated pieces doesn’t help too much if you’re trying to see the mapper as a whole. Therefore, in the coming segment I’m going to list the mapper’s full source code, so you can see how it looks in its finished state.

So just keep reading. 

The finished version of the data mapper

In consonance with the concepts deployed above, below I included the complete version of the previous data mapper. This should help you understand how it works more easily. Here it is:

(DataMapperAbstract.php)

<?php

abstract class DataMapperAbstract implements DataMapperInterface
{
    protected $_adapter;
    protected $_collection;
    protected $_entityClass;
    protected $_entityTable; 
        
    /**
     * Class constructor
     */
    public function __construct(MySQLAdapter $adapter, CollectionAbstract $collection, array $entityOptions = array())
    {
        $this->_adapter = $adapter;
        $this->_collection = $collection;
        if (isset($entityOptions['entityClass'])) {
            $this->setEntityClass($entityOptions['entityClass']);
        }
        if (isset($entityOptions['entityTable'])) {
            $this->setEntityTable($entityOptions['entityTable']);
        }
        $this->init();
    }
   
    /**
     * Initialize the data mapper here
     */
    public function init(){}
   
    /**
     * Get the instance of the database adapter
     */
    public function getAdapter()
    {
        return $this->_adapter;
    }
   
    /**
     * Get the collection the mapper uses
     */
    public function getCollection()
    {
        return $this->_collection;
    }
   
    /**
     * Set the class for reconstructing entities
     */
    public function setEntityClass($entityClass)
    {
        if (!class_exists($entityClass, false)) {
            throw new DataMapperException(‘The specified entity class ‘ . $entityClass . ‘ does not exist.’);
        }
        $this->_entityClass = $entityClass;
    }
   
    /**
     * Get the class for reconstructing entities
     */
    public function getEntityClass()
    {
        return $this->_entityClass;
    }
   
    /**
     * Set the entity database table the mapper works with
     */
    public function setEntityTable($entityTable)
    {
        if (!is_string($entityTable) || empty($entityTable)) {
            throw new DataMapperException(‘The specified entity table ‘ . $entityTable . ‘ is invalid.’);
        }
        $this->_entityTable = $entityTable;
    }
   
    /**
     * Get the entity database table the mapper works with
     */
    public function getEntityTable()
    {
        return $this->_entityTable;
    }
   
    /**
     * Find an entity by its ID
     */
    public function findById($id)
    {
        $id = (int) $id;
        $this->_adapter->select($this->_entityTable, "id = $id");
        if ($data = $this->_adapter->fetch()) {
            return new $this->_entityClass($data);
        }
        return null;
    }
   
    /**
     * Find all the entities
     */
    public function findAll()
    {
        $this->_collection->clear();
        $this->_adapter->select($this->_entityTable);
        while ($data = $this->_adapter->fetch()) {
            $this->_collection->add($data['id'], new $this->_entityClass($data));   
        }
        return $this->_collection->count() !== 0 ?
               $this->_collection :
               null;
    }
   
    /**
     * Find all the entities that match the specified criteria
     */
    public function search($criteria)
    {
        $this->_collection->clear();
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $this->_collection->add($data['id'], new $this->_entityClass($data));   
        }
        return $this->_collection->count() !== 0 ?
               $this->_collection :
               null;
    }
     
    /**
     * Insert a new row in the table corresponding to the specified entity
     */
    public function insert(EntityAbstract $entity)
    {
        $data = $entity->toArray();
        return $this->_adapter->insert($this->_entityTable, $data);
    }
   
    /**
     * update the row in the table corresponding to the specified entity
     */
    public function update(EntityAbstract $entity)
    {
        $id = (int) $entity->id;
        $data = $entity->toArray();
        unset($data['id']);
        return $this->_adapter->update($this->_entityTable, $data, "id = $id");
    }
    
    /**
     * Delete the row in the table corresponding to the specified entity or ID
     */
    public function delete($id)
    {
        if ($id instanceof EntityAbstract) {
            $id = (int) $id->id;
        }
        return $this->_adapter->delete($this->_entityTable, "id = $id");
    }               
}

For obvious reasons, the construction of a generic mapper like the one above is not an effective solution in all cases; this depends strongly on the type of application being developed, the domain objects involved in this process, and so forth. In this specific case, though, where the only domain objects that will be manipulated are simple user objects, the implementation of a generic mapper makes a lot of sense, trust me. And if you’re still reluctant to accept this, see for yourself how easy it is to create a mapper that only works with user entities:   

(UserMapper.php)

<?php

class UserMapper extends DataMapperAbstract
{
    protected $_entityClass = ‘User’;
    protected $_entityTable = ‘users’; 
   
    /**
     * Class constructor
     */
     public function __construct(MySQLAdapter $adapter, UserCollection $collection, array $entityOptions = array())
     {
         parent::__construct($adapter, $collection, $entityOptions);
     }  
}

That was easy to code, wasn’t it? As the previous code sample shows, building a concrete user mapper is only a question of subclassing the base abstract parent and overriding its constructor. In fact, the last step is optional, but it assures that the collection injected into the mapper will be one that handles only user objects.

And now that I’ve managed to create a user mapper class in a few easy steps, we’ve gotten a bit closer to the construction of a fully-functional user repository. With the domain, data access and mapping layers of this sample application already up and running, it’s really tempting to take the plunge and start building the repository, right? Well, let’s not move so fast. A repository in its simplest form should be capable of handling collections of domain objects, a process that hasn’t been demonstrated so far.

However, this is about to change. In the next tutorial I’m going to develop a couple of classes that will be tasked specifically with manipulating entities, and more specifically, user objects.  

Final thoughts

That’s it for now. In this fourth tutorial of the series, I added three methods to the previous data mapper class, which came in handy for saving and deleting the table rows related to a specified entity. With this parent class fully implementing the methods declared by the “DataMapperInterface” interface, it was really easy to create a refined subclass, capable of specifically mapping user objects. In simple terms, the whole subclassing process was reduced to overriding the parent’s constructor.

At this point, it could be said that it’s safe to use the earlier “DataMapperAbstract” class to spawn concrete mappers like the “UserMapper” shown before. But, is this completely true? Not really. As you may have noticed, there’s a small catch here. The abstract parent and its concrete child inject, through their respective constructors, a pair of collaborators that are instances of two classes called “CollectionAbstract” and “UserCollection,” which remain undefined.

As their names imply, these classes are countable and “iteratable” (if the term is applicable) structures that will allow you to handle collections of entities as if they were array elements. It’s necessary, therefore, to show the source code of these classes, to truly complete the mapping layer.

Since this will be accomplished in the forthcoming part of the series, I suggest you don’t miss it!

Google+ Comments

Google+ Comments