Roll Your Own Repository in PHP: Defining a Generic Data Mapper

In this third part of the series, I add a simple mapping layer to the sample application. This layer will be composed of a single interface and a generic mapper class.

Understanding repositories may sound difficult, but they’re actually pretty straightforward, especially if you have already ventured into the terrain of Domain-Driven Design (DDD). A repository is an additional abstraction layer placed between the domain and mapping layers. It is used in applications with a rich domain for handling collections of domain objects that satisfy a specified criteria. In a typical implementation, a repository concentrates, behind an intuitive API, all of the logic required to query object collections. This significantly reduces code replication.

Naturally, the most effective way to understand the functionality of a repository in PHP (or in any other programming language, of course) is by means of a concrete example that justifies the upfront work required to construct this extra abstraction layer. In keeping with this idea, in the two previous tutorials of this series I went through the development of the domain and data access layers of a sample web application. The application’s main objective is to manipulate a bunch of simple user objects that persist through a MySQL database.

Now that we have two layers existing independently of each other, the next step in the implementation of a functional user repository is to create yet another layer. It will act like a mediator between the previous ones without introducing an unnecessary, undesired coupling effect. As you may have already guessed from the article’s title, this layer will be comprised of a set of mapping classes, which can perform CRUD operations on domain objects, and handle collections of them in a fairly simple fashion.

Now that the subject of this third installment of the series has been clearly outlined, it’s time to start creating the mapping classes. Let’s get going!

Review: the data access layer

As always, before I start defining the mapping classes of this sample application, I’d like to spend a few moments reintroducing the source code corresponding to the interface and the implementing class created in the preceding tutorial. These were the building blocks of the application’s data access layer.

First, here’s the interface, which defines a contract that must be fulfilled by any class that interacts with a specific database server. Pay attention to it, please: 

(DatabaseAdapterInterface.php)

<?php

interface DatabaseAdapterInterface
{
    public function connect();
   
    public function disconnect(); 
   
    public function query($query);
   
    public function fetch(); 
   
    public function select($table, $where, $fields, $order, $limit, $offset);
   
    public function insert($table, array $data);
   
    public function update($table, array $data, $where);
   
    public function delete($table, $where);
   
    public function getInsertId();
   
    public function countRows();
   
    public function getAffectedRows();
}

With the small number of methods it declares, we can’t say much about the “DatabaseAdapterInterface” interface, except that it establishes a contract that must be agreed to by all of the database adapters created from this point onward. In consonance with this, below is the definition of a concrete class which not only implements the interface, but abstracts common operations that are performed with MySQL:

(MySQLAdapter.php)

<?php

class MySQLAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_link;
    protected $_result;
    protected static $_instance;
   
    /**
     * Get the Singleton instance of the class
     */
    public static function getInstance(array $config = array())
    {
        if (self::$_instance === null) {
            self::$_instance = new self($config);
        }
        return self::$_instance;
    }
   
    /**
     * Class constructor
     */
    protected function __construct(array $config)
    {
        if (count($config) !== 4) {
            throw new MySQLAdapterException(‘Invalid number of connection parameters.’);  
        }
        $this->_config = $config;
    }
   
    /**
     * Prevent cloning the instance of the class
     */
    protected function __clone(){}
   
    /**
     * Connect to MySQL
     */
    public function connect()
    {
        // connect only once
        if ($this->_link === null) {
            list($host, $user, $password, $database) = $this->_config;
            if ((!$this->_link = @mysqli_connect($host, $user, $password, $database))) {
                throw new MySQLAdapterException(‘Error connecting to MySQL : ‘ . mysqli_connect_error());
            }
            unset($host, $user, $password, $database);      
        }
    }

    /**
     * Execute the specified query
     */
    public function query($query)
    {
        if (!is_string($query) || empty($query)) {
            throw new MySQLAdapterException(‘The specified query is not valid.’);  
        }
        // lazy connect to MySQL
        $this->connect();
        if (!$this->_result = mysqli_query($this->_link, $query)) {
            throw new MySQLAdapterException(‘Error executing the specified query ‘ . $query . mysqli_error($this->_link));
        }
    }
   
    /**
     * Perform a SELECT statement
     */
    public function select($table, $where = ”, $fields = ‘*’, $order = ”, $limit = null, $offset = null)
    {
        $query = ‘SELECT ‘ . $fields . ‘ FROM ‘ . $table
               . (($where) ? ‘ WHERE ‘ . $where : ”)
               . (($limit) ? ‘ LIMIT ‘ . $limit : ”)
               . (($offset && $limit) ? ‘ OFFSET ‘ . $offset : ”)
               . (($order) ? ‘ ORDER BY ‘ . $order : ”);
        $this->query($query);
        return $this->countRows();
    }
   
    /**
     * Perform an INSERT statement
     */ 
    public function insert($table, array $data)
    {
        $fields = implode(‘,’, array_keys($data));
        $values = implode(‘,’, array_map(array($this, ‘quoteValue’), array_values($data)));
        $query = ‘INSERT INTO ‘ . $table . ‘(‘ . $fields . ‘)’ . ‘ VALUES (‘ . $values . ‘)';
        $this->query($query);
        return $this->getInsertId();
    }
   
    /**
     * Perform an UPDATE statement
     */
    public function update($table, array $data, $where = ”)
    {
        $set = array();
        foreach ($data as $field => $value) {
            $set[] = $field . ‘=’ . $this->quoteValue($value);
        }
        $set = implode(‘,’, $set);
        $query = ‘UPDATE ‘ . $table . ‘ SET ‘ . $set
               . (($where) ? ‘ WHERE ‘ . $where : ”);
        $this->query($query);
        return $this->getAffectedRows(); 
    }
   
    /**
     * Perform a DELETE statement
     */
    public function delete($table, $where = ”)
    {
        $query = ‘DELETE FROM ‘ . $table
               . (($where) ? ‘ WHERE ‘ . $where : ”);
        $this->query($query);
        return $this->getAffectedRows();
    }
   
    /**
     * Single quote the specified value
     */
    public function quoteValue($value)
    {
        if ($value === null) {
            $value = ‘NULL';
        }
        else if (!is_numeric($value)) {
            $value = "’" . mysqli_real_escape_string($this->_link, $value) . "’";
        }
        return $value;
    }
   
    /**
     * Fetch a single row from the current result set (as an associative array)
     */
    public function fetch()
    {
        if ($this->_result !== null) {
            if ((!$row = mysqli_fetch_array($this->_result, MYSQLI_ASSOC))) {
                $this->freeResult();
                return false;
            }
            return $row;
        }
    }

    /**
     * Get the insertion ID
     */
    public function getInsertId()
    {
        return $this->_link !== null ?
               mysqli_insert_id($this->_link) :
               null; 
    }
   
    /**
     * Get the number of rows returned by the current result set
     */ 
    public function countRows()
    {
        return $this->_result !== null ?
               mysqli_num_rows($this->_result) :
               0;
    }
   
    /**
     * Get the number of affected rows
     */
    public function getAffectedRows()
    {
        return $this->_link !== null ?
               mysqli_affected_rows($this->_link) :
               0;
    }
   
    /**
     * Free up the current result set
     */
    public function freeResult()
    {
        if ($this->_result !== null) {
            mysqli_free_result($this->_result);  
        }
    }
   
    /**
     * Close explicitly the database connection
     */
    public function disconnect()
    {
        if ($this->_link !== null) {
            mysqli_close($this->_link);
            $this->_link = null;
        }
    }
   
    /**
     * Close automatically the database connection when the instance of the class is destroyed
     */
    public function __destruct()
    {
        $this->disconnect();
    }
}

 

(MySQLAdapterException.php)

<?php

class MySQLAdapterException extends Exception{}

Even though the source code is somewhat extensive, the above “MySQLAdapter” class is a simple wrapper for the “mysqli” PHP extension that allows it to execute typical operations on MySQL. These operations include fetching rows, handling result sets, and running queries. You’ve probably seen many classes similar to this one, so I’m not going to spend a long time explaining how it does its business.

Instead, it’s time to summarize what we’ve achieved so far. On one hand, this sample application contains a domain layer responsible for creating generic entities, and more specifically user objects; on the other hand, there’s a data access layer capable of dealing directly with the underlying persistence mechanism, which in this case turns out to be a MySQL database. The question is, how can these layers be bridged while preserving their mutual independence?

Well, as I said before, the implementation of a mapping layer will do the trick nicely. Thus, in the next section I’m going to start defining the structure and further behavior of this new layer.

To see how this will be done, jump ahead and read the lines to come.    

{mospagebreak title=The mapping layer}

Constructing the mapping layer is a two-steps process. First, it’s necessary to define a contract (pretty similar to the one used with the database adapters discussed previously), which must be satisfied by all of the mapper classes created afterward. And second, the generation of these classes will be achieved by subclassing a generic mapper, even though this last step is optional.

With that said, here’s the interface that defines the structure of the aforementioned contract:

(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);         
}

Although the definition of the above “DataMapperInterface” interface is pretty short, it’s clear to see that it declares a bunch of methods that can be used for performing CRUD operations against the data access layer. Apart from the classic finders that can be found in most mappers, like “findById()” and “findAll(),” there are a few additional methods that allow you to save and delete records associated with a specified entity. Since the role of an interface is to define a contract, and nothing else, it’s up to the mappers to concretely implement these entity-related operations. Got that? Great.

Now that you understand the purpose of defining the earlier interface, the next thing that we need to do is create a class that implements the interface in question, and also encapsulates common functionality shared by concrete mapping classes. This will be done in the section below, so keep reading.    

Implementing the previous interface: defining a generic mapper

In keeping with the concepts deployed above, the last step we must take to get the mapping layer of this sample application finished is to build a generic mapper class. For obvious reasons, this class will be declared abstract, and its initial definition will be as follows:

(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{}

While it’s fair to admit that, at a glance, the source code of the above “DataMapperAbstract” class looks somewhat complex, this is a misleading impression. In fact, the class is a generic mapper that concentrates most of the functionality required to perform CRUD operations in a MySQL database table. It’s that simple, really.

In addition, you should notice that its constructors inject two collaborators. The first one is an instance of the previous MySQL adapter, which is utilized internally for executing the aforementioned operations. The second one is an abstract collection object, which is used within the “findAll()” and “search()” methods. You’re wondering where the latter comes from, right? This object is an instance of a countable, iteratable class responsible for handling collections of entities. But leave your troubles behind, at least for the moment, and don’t feel concerned about the implementation of this class; it’ll be covered in detail in upcoming tutorials in this series.

Needless to say, if you test the “DataMapperAbstract” class in its current state, you’ll get an ugly fatal error; it doesn’t fully implement all of the methods declared by the “DataMapperInterface” interface. Again, fear not, as these pending methods will be added to the class and discussed in depth in the following installment.

It’s safe to say, however, that the mapping layer of this sample application is near completion. It now includes a generic data mapper which can be easily subclassed to create concrete mappers, including the one responsible for handling user objects. This is a remarkable advance toward the implementation of a user repository.

Final thoughts

In this third episode of the series, I added to this sample application a simple mapping layer composed of a single interface and a generic mapper class. Even though the development of the latter is still incomplete, as it’s necessary to add the methods that save and delete entities, it can already be used effectively as a mediator between the domain and data access layers defined previously.

As I said before, the implementation of these missing methods will be covered in the next tutorial, so if you’re interested in learning how this will be done, don’t miss the upcoming part!

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

antalya escort bayan antalya escort bayan Antalya escort diyarbakir escort