PHP Service Layers: Handling Entity Collections

In this fourth part of the series, I add to the mapping layer of this sample application a couple of additional classes. These new classes will handle collections of entities (and more specifically, user objects).

Superbly covered in Martin Fowler’s book Patterns of Enterprise Application Architecture, a service layer (http://martinfowler.com/eaaCatalog/serviceLayer.html) is an enterprise-level pattern that can be used for encapsulating application logic behind a single interface. This interface can be used by different clients. As with a few other patterns available, a service is particularly useful in the development of applications that use the domain-driven design paradigm (DDD), where the domain model has its own constraints and rules, and lives completely isolated from the persistence layer (the so-called persistence agnosticism).

Of course, this doesn’t mean that a service must be implemented in every possible use case, just because it looks like a sophisticated approach whose name sounds quite impressive. However, as current web applications become larger, more complex and functional, implementing a service can be a great help. This is especially true when they’re used in conjunction with an MVC stack, where action controllers tend to get "fatty" easily (either due to ignorance or an improper design of the model).

You, as a PHP developer, can take advantage of the benefits provided by a service as well, without having to radically modify the infrastructure of your MVC applications. And to prove it, in previous chapters of this series I started building a sample program, whose main area of concern is to employ a service layer to perform CRUD operations on some user entities.

So far, the program’s functionality is admittedly pretty limited. I’ve only managed to implement its domain model, along with its mapping and data access layers. However, the former is still incomplete. It’s  necessary to add an extra dependency to it, which will be responsible for handling collections of entities.

Given that, in this fourth installment of the series I’ll be implementing the aforementioned dependency, which will be easy to understand.  
   
Persisting the model in a MySQL database: a quick review of the application’s persistence layer

Just in case you haven’t read the previous episode of this series, where I implemented the data access layer of the sample application, below I included this layer’s source code.

Here’s the first building block of the layer. It’s a segregated interface, responsible for defining a contract for generic database adapters. Check it out:

(MyApplication/Database/DatabaseAdapterInterface.php)

<?php

namespace MyApplicationDatabase;

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

Although its contract is somewhat revealing, the above interface is useful for constructing a pluggable structure that permits users to consume different database adapters at runtime. Since in this case, I plan to use a MySQL database as the underlying storage mechanism, the only thing we need to do to get this persistence layer ready to go is define an adapter that works with that specific RDBMS.

Not surprisingly, the class shown below does that: 

(MyApplication/Database/MysqlAdapter.php)

<?php

namespace MyApplicationDatabase;

class MysqlAdapter implements DatabaseAdapterInterface
{
    protected $_config = array();
    protected $_link;
    protected $_result;
   
    /**
     * Constructor
     */
    public function __construct(array $config)
    {
        if (count($config) !== 4) {
            throw new MySQLAdapterException(‘Invalid number of connection parameters.’);  
        }
        $this->_config = $config;
    }
   
    /**
     * Connect to MySQL
     */
    public function connect()
    {
        // connect only once
        if ($this->_link !== null) {
            return $this->_link;
        }
        list($host, $user, $password, $database) = $this->_config;
        if (($this->_link = @mysqli_connect($host, $user, $password, $database))) {
            unset($host, $user, $password, $database);
            return $this->_link;
        }
        throw new MySQLAdapterException(‘Error connecting to the server : ‘ . mysqli_connect_error());
    }

    /**
     * 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)) {
            return $this->_result;
        }
        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();
    }
   
    /**
     * Escape the specified value
     */
    public function quoteValue($value)
    {
        $this->connect();
        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)) !== false) {
                return $row;
            }
            $this->freeResult();
            return false;
        }
        return null;
    }

    /**
     * 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);
            return true;
        }
        return false;
    }
   
    /**
     * Close explicitly the database connection
     */
    public function disconnect()
    {
        if ($this->_link !== null) {
            mysqli_close($this->_link);
            $this->_link = null;
            return true;
        }
        return false;
    }
   
    /**
     * Close automatically the database connection when the instance of the class is destroyed
     */
    public function __destruct()
    {
        $this->disconnect();
    }
}

 

(MyApplication/Database/MysqlAdapterException.php)

<?php

namespace MyApplicationDatabase;

class MysqlAdapterException extends Exception{}

Since the logic of the above "MysqlAdapter" class has already been discussed, we needn’t waste any time explaining how it functions again. However, I’d like to summarize what we’ve achieved so far with reference to the implementation of a functional service.

On the one hand, the domain and data access layers of this sample application are up and running, living happily separated from each other. On the other hand, its mappers are almost functional, since they require an additional collaborator that handles collections of entities to work as expected.

Obviously, it’s time to give the mappers what they need! So, in the following section I’ll be showing you the definition of the collection handling class just mentioned, which will be implemented as a plain array collection.   

Now, click on the link below and keep reading.

{mospagebreak title=Building a generic array collection class}

As I noted in the prior segment, it’s necessary to create a pair of classes capable of traversing and accessing collections of entities, so they can be used internally by the "findAll()" and "search()" methods implemented by the previous data mappers.

The following class performs these tasks in a simple fashion. Look at it:

(MyApplication/Collection/AbstractCollection.php)

<?php

namespace MyApplicationCollection;

abstract class AbstractCollection implements Iterator, Countable, ArrayAccess
{
    protected $_entities = array();
    protected $_entityClass;

    /**
     * Constructor
     */
    public function  __construct(array $entities = array())
    {
        if (!empty($entities)) {
            $this->_entities = $entities;
        }
        $this->rewind();
    }

    /**
     * Get the entities stored in the collection
     */
    public function getEntities()
    {
        return $this->_entities;
    }
   
    /**
     * Clear the collection
     */
    public function clear()
    {
        $this->_entities = array();
    }
    
    /**
     * Reset the collection (implementation required by Iterator Interface)
     */    
    public function rewind()
    {
        reset($this->_entities);
    }
   
    /**
     * Get the current entity in the collection (implementation required by Iterator Interface)
     */
    public function current()
    {
        return current($this->_entities);
    }
   
    /**
     * Move to the next entity in the collection (implementation required by Iterator Interface)
     */
    public function next()
    {
        next($this->_entities);
    }
   
    /**
     * Get the key of the current entity in the collection (implementation required by Iterator Interface)
     */
    public function key()
    {
        return key($this->_entities);
    }
   
    /**
     * Check if there’re more entities in the collection (implementation required by Iterator Interface)
     */
    public function valid()
    {
        return ($this->current() !== false);
    }
   
    /**
     * Count the number of entities in the collection (implementation required by Countable Interface)
     */
    public function count()
    {
        return count($this->_entities);
    }
   
    /**
     * Add an entity to the collection (implementation required by ArrayAccess interface)
     */
    public function offsetSet($key, $entity)
    {
        if ($entity instanceof $this->_entityClass) {
            if (!isset($key)) {
                $this->_entities[] = $entity;
            }
            else {
                $this->_entities[$key] = $entity;
            }
            return true;
        }
        throw new CollectionException(‘The specified entity is not allowed for this collection.’);
    }
   
    /**
     * Remove an entity from the collection (implementation required by ArrayAccess interface)
     */
    public function offsetUnset($key)
    {
        if ($key instanceof $this->_entityClass) {
            $this->_entities = array_filter($this->_entities, function ($v) use ($key) {
                return $v !== $key;
            });
            return true;
        }
        if (isset($this->_entities[$key])) {
            unset($this->_entities[$key]);
            return true;
        }
        return false;
    }
   
    /**
     * Get the specified entity in the collection (implementation required by ArrayAccess interface)
     */
    public function offsetGet($key)
    {
        return isset($this->_entities[$key]) ?
               $this->_entities[$key] :
               null;
    } 
   
    /**
     * Check if the specified entity exists in the collection (implementation required by ArrayAccess interface)
     */    
    public function offsetExists($key)
    {
        return isset($this->_entities[$key]);
    }
}

 

(MyApplication/Collection/CollectionException.php)

<?php

namespace MyApplicationCollection;

class CollectionException extends Exception{}

The "AbstractCollection" class is nothing but a simple wrapper for a plain PHP array. This class encapsulates the functionality required for iterating over its elements, as well as for counting and accessing them, thanks to the implementation of the Iterator, Countable and ArrayAccess SPL interfaces.

While this abstract class does a good job of handling collections of generic entities, in this case it’s mandatory to spawn a concrete subclass that allows it to specifically handle user objects. Remember, our final goal is to implement a user service.

Don’t worry, however, because this derivative will be shown in the following segment. So keep reading. 

Taking the final step: developing a user collection class

If you think that implementing a concrete class responsible for traversing collections of user entities is difficult, the following code bit should make you change your mind. Check it out:

(MyApplication/Collection/UserCollection.php)

<?php

namespace MyApplicationCollection;

class UserCollection extends AbstractCollection
{
    protected $_entityClass = ‘MyApplicationEntityUser’;

  
That was really easy to code and read, right? As you can see, the creation of a child class that works with a specified type of domain object is reduced to overriding the abstract parent’s $_entityClass field and nothing else. Period.

With these two collection handling classes living in sweet harmony, we’ve taken a big step toward the implementation of a functional user service.

Final Thoughts

In this fourth part of the series, I added a couple of additional classes to the mapping layer of this sample application. They’re charged with handling collections of entities (and more specifically, user objects).

With the inclusion of these classes, at this point the application is functional (assuming that the corresponding MySQL database has been previously created). What’s more, it’s really easy to set up a domain model with a few user entities in it, then build a mapper and finally start putting data into, and pulling it out of, the storage area. 

But, what does this buy us, if what we’re trying to implement here is a user service, not a user mapper (which is already done)? Well, leave your concerns behind, because in the upcoming tutorial I’ll be defining the service, so that you can finally see how it fits into the current application schema.

Don’t miss the next part!

[gp-comments width="770" linklove="off" ]
antalya escort bayan antalya escort bayan