PHP Service Layers: A Final Example

If you’re looking for an approachable guide that teaches you how to implement an easily-customizable service layer in PHP, then take a peek at this article series. In a step-by-step fashion, it walks you through the development of a sample web application, which uses a service to perform CRUD operations on a domain model composed of a few user entities.

And now that you know what to expect, it’s tiem to recap the topics discussed in the last installment. In that tutorial, I constructed a pair of dependency injection containers, along with a static helper. These allowed us to shield from client code the creation of the object graph corresponding to the user service.

With the implementation of these additional elements, the scene is finally set to put the service into action and see if it is actually as functional as it seems.

In this last chapter I’ll be creating a concrete example that will show you how to use the service to retrieve, save and delete user objects from a simple MySQL database. And thanks to the service’s inherent flexibility, you’ll be able to adapt it to work with your existing infrastructure (read your framework) and even with different client layers.

Now, leave the dull theory behind and start reading!

Putting all of the pieces together: showing the application’s full source code
 
First things first. Before I demonstrate how to put the sample application previously developed (and its user service, of course) to work, it’d be useful to list its full source code. This way you can have its building blocks available in one place, in case you want to alter them to suit your personal needs.

Having said that, are you ready to digest a huge amount of code samples? Great. So, this heavy roundup begins with the application’s domain layer. Here it is:

(MyApplication/Entity/AbstractEntity.php)

<?php

namespace MyApplicationEntity;

abstract class AbstractEntity
{
    protected $_values = array();
    protected $_allowedFields = array();
   
    /**
     * Class constructor
     */
    public function __construct(array $data)
    {
        foreach ($data as $name => $value) {
            $this->$name = $value;
        }
    }
   
    /**
     * Assign a value to the specified field via the corresponding mutator (if it exists);
     * otherwise, assign the value directly to the ‘$_values’ protected array
     */
    public function __set($name, $value)
    {  
        if (!in_array($name, $this->_allowedFields)) {
            throw new EntityException(‘The field ‘ . $name . ‘ is not allowed for this entity.’); 
        }
        $mutator = ‘set’ . ucfirst($name);
        if (method_exists($this, $mutator) && is_callable(array($this, $mutator))) {
            $this->$mutator($value);          
        }
        else {
            $this->_values[$name] = $value;
        }   
    }
   
    /**
     * Get the value assigned to the specified field via the corresponding getter (if it exists);
    otherwise, get the value directly from the ‘$_values’ protected array
     */
    public function __get($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new EntityException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);   
        }
        $accessor = ‘get’ . ucfirst($name);
        if (method_exists($this, $accessor) && is_callable(array($this, $accessor))) {
            return $this->$accessor;   
        }
        if (isset($this->_values[$name])) {
            return $this->_values[$name];  
        }
        throw new EntityException(‘The field ‘ . $name . ‘ has not been set for this entity yet.’);
    }

    /**
     * Check if the specified field has been assigned to the entity
     */
    public function __isset($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new EntityException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);
        }
        return isset($this->_values[$name]);
    }

    /**
     * Unset the specified field from the entity
     */
    public function __unset($name)
    {
        if (!in_array($name, $this->_allowedFields)) {
            throw new EntityException(‘The field ‘ . $name . ‘ is not allowed for this entity.’);
        }
        if (isset($this->_values[$name])) {
            unset($this->_values[$name]);
        }
    }

    /**
     * Get an associative array with the values assigned to the fields of the entity
     */
    public function toArray()
    {
        return $this->_values;
    }             
}

 

(MyApplication/Entity/User.php)

<?php

namespace MyApplicationEntity;

class User extends AbstractEntity
{  
    protected $_allowedFields = array(‘id’, ‘fname’, ‘lname’, ‘email’);
   
    /**
     * Set the user’s ID
     */
    public function setId($id)
    {
        if(!filter_var($id, FILTER_VALIDATE_INT, array(‘options’ => array(‘min_range’ => 1, ‘max_range’ => 999999)))) {
            throw new EntityException(‘The specified ID is invalid.’);
        }
        $this->_values['id'] = $id;
    }
   
    /**
     * Set the user’s first name
     */ 
    public function setFname($fname)
    {
        if (strlen($fname) < 2 || strlen($fname) > 32) {
            throw new EntityException(‘The specified first name is invalid.’);
        }
        $this->_values['fname'] = $fname;
    }
       
    /**
     * Set the user’s last name
     */
    public function setLname($lname)
    {
        if (strlen($lname) < 2 || strlen($lname) > 32) {
            throw new EntityException(‘The specified last name is invalid.’);
        }
        $this->_values['lname'] = $lname;
    }
   
    /**
     * Set the user’s email address
     */
    public function setEmail($email)
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new EntityException(‘The specified email address is invalid.’);
        }
        $this->_values['email'] = $email;
    }                   
}

 

(MyApplication/Entity/EntityException.php)

<?php

namespace MyApplicationEntity;

class EntityException extends Exception{}

Done. Having listed the application’s domain model, which in this case is responsible for spawning user entities, it’s time to show the following layer. This one is made up of a couple of data mappers, along with a segregated interface, and its source code looks like this:

(MyApplication/Mapper/DataMapperInterface.php)

<?php

namespace MyApplicationMapper;

interface DataMapperInterface
{
    public function findById($id);

    public function findAll();

    public function search($criteria);

    public function insert($entity);

    public function update($entity);

    public function delete($entity);
}

 

(MyApplication/Mapper/AbstractDataMapper.php)

<?php

namespace MyApplicationMapper;
use MyApplicationDatabase,
    MyApplicationCollection;

abstract class AbstractDataMapper implements DataMapperInterface
{
    protected $_adapter;
    protected $_collection;
    protected $_entityTable;
    protected $_entityClass;

    /**
     * Constructor
     */
    public function  __construct(DatabaseDatabaseAdapterInterface $adapter, CollectionAbstractCollection $collection, array $entityOptions = array())
    {
        $this->_adapter = $adapter;
        $this->_collection = $collection;
        if (isset($entityOptions['entityTable'])) {
            $this->setEntityTable($entityOptions['entityTable']);
        }
        if (isset($entityOptions['entityClass'])) {
            $this->setEntityClass($entityOptions['entityClass']);
        }
    }

    /**
     * Get the database adapter
     */
    public function getAdapter()
    {
        return $this->_adapter;
    }

    /**
     * Get the collection
     */
    public function getCollection()
    {
        return $this->_collection;
    }

    /**
     * Set the entity table
     */
    public function setEntityTable($entityTable)
    {
        if (!is_string($table) || empty($entityTable)) {
            throw new DataMapperException(‘The specified entity table is invalid.’);
        }
        $this->_entityTable = $entityTable;
    }

    /**
     * Get the entity table
     */
    public function getEntityTable()
    {
        return $this->_entityTable;
    }

    /**
     * Set the entity class
     */
    public function setEntityClass($entityClass)
    {
        if (!class_exists($entityClass)) {
            throw new DataMapperException(‘The specified entity class is invalid.’);
        }
        $this->_entityClass = $entityClass;
    }

    /**
     * Get the entity class
     */
    public function getEntityClass()
    {
        return $this->_entityClass;
    }

    /**
     * Find an entity by its ID
     */
    public function findById($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->_adapter->select($this->_entityTable);
        while ($data = $this->_adapter->fetch($this->_entityTable)) {
            $this->_collection[] = new $this->_entityClass($data);
        }
        return $this->_collection;
    }

    /**
     * Find all the entities that match the specified criteria
     */
    public function search($criteria)
    {
        $this->_adapter->select($this->_entityTable, $criteria);
        while ($data = $this->_adapter->fetch()) {
            $this->_collection[] = new $this->_entityClass($data);
        }
        return $this->_collection;
    }
   
    /**
     * Insert a new row in the table corresponding to the specified entity
     */
    public function insert($entity)
    {
        if ($entity instanceof $this->_entityClass) {
            return $this->_adapter->insert($this->_entityTable, $entity->toArray());
        }
        throw new DataMapperException(‘The specified entity is not allowed for this mapper.’);
    }

    /**
     * Update the row in the table corresponding to the specified entity
     */
    public function update($entity)
    {
        if ($entity instanceof $this->_entityClass) {
            $data = $entity->toArray();
            $id = $entity->id;
            unset($data['id']);
            return $this->_adapter->update($this->_entityTable, $data, "id = $id");
        }
        throw new DataMapperException(‘The specified entity is not allowed for this mapper.’);
    }

    /**
     * Delete the row in the table corresponding to the specified entity or ID
     */
    public function delete($id)
    {
        if ($id instanceof $this->_entityClass) {
            $id = $id->id;
        }
        return $this->_adapter->delete($this->_entityTable, "id = $id");
    }
}

 

(MyApplication/Mapper/UserMapper.php)

<?php

namespace MyApplicationMapper;
use MyApplicationDatabase,
    MyApplicationCollection;

class UserMapper extends AbstractDataMapper
{
    protected $_entityClass = ‘MyApplicationEntityUser’;
    protected $_entityTable = ‘users’; 
   
    /**
     * Constructor
     */
     public function __construct(DatabaseDatabaseAdapterInterface $adapter, CollectionUserCollection $collection)
     {
         parent::__construct($adapter, $collection);
     }  
}

 

(MyApplication/Mapper/DataMapperException.php)

<?php

namespace MyApplicationMapper;

class DataMapperException extends Exception{}

That wasn’t too long to read, was it? Now that you’ve reviewed the mapping layer, we’re going to look at the layer that persists the model in a MySQL database. Here it is:

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

 

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

Still with me? Good. Having listed the application’s data access layer, it’s time to check the layer tasked with handling collections of entities. I decided to place this layer under a directory called “Collection;” however, if you think it should be part of the mapping layer, feel free to make that change. In either case, the source code of this layer is as follows:

(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/UserCollection.php)

<?php

namespace MyApplicationCollection;

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

 

(MyApplication/Collection/CollectionException.php)

<?php

namespace MyApplicationCollection;

class CollectionException extends Exception{}
  
Definitely, understanding the functionality of the above array collection classes is a pretty straightforward process. So, the next thing I’ll show you is the application’s service layer. Here it is:

(MyApplication/Service/AbstractService.php)

<?php

namespace MyApplicationService;
use MyApplicationMapper,
    MyApplicationEntity;

abstract class AbstractService
{
    protected $_mapper;

    /**
     * Constructor
     */
    public function  __construct(MapperAbstractDataMapper $mapper)
    {
        $this->_mapper = $mapper;
    }

    /**
     * Find an entity by their ID
     */
    public function findById($id)
    {
        return $this->_mapper->findById($id);
    }

    /**
     * Find all the entities
     */
    public function findAll()
    {
        return $this->_mapper->findAll();
    }

    /**
     * Insert a new entity
     */
    public function insert(EntityEntityAbstract $entity)
    {
        return $this->_mapper->insert($entity);
    }

    /**
     * Update an entity
     */
    public function update(EntityEntityAbstract $entity)
    {
        return $this->_mapper->update($entity);
    }

    /**
     * Delete an entity
     */
    public function delete($id)
    {
        return $this->_mapper->delete($id);
    }
}

 

(MyApplication/Service/UserService.php)

<?php

namespace MyApplicationService;
use MyApplicationMapper,
    MyApplicationEntity;

class UserService extends AbstractService
{
    /**
     * Constructor
     */
    public function  __construct(MapperUserMapper $mapper)
    {
        parent::__construct($mapper);
    }

    /**
     * Save a user to persistence layer
     */
    public function save(EntityUser $user)
    {
        return $user->id === null ?
               $this->insert($user) :
               $this->update($user);
    }

    /**
     * Fetch all users in XML format
     */
    public function toXML()
    {
        $users = $this->_mapper->findAll();
        $xml = "<?xml version="1.0" encoding="UTF-8"?>n<users>n";
        foreach($users as $user) {
            $xml .= "<user>n<fname>$user->fname</fname>n"
                  . "<lname>$user->lname</lname>n"
                  . "<email>$user->fname</email>n</user>n";
        }
        $xml .= "</users>";
        return $xml;
    }
}

 

(MyApplication/Service/ServiceLocator.php)

<?php

namespace MyApplicationService;
use MyApplicationInjector;

class ServiceLocator
{
    protected static $_services = array();
    protected static $_injectors = array();

    /**
     * Protected constructor
     */
    protected function  __construct(){}

    /**
     * Add a single injector
     */
    public static function addInjector($name, InjectorInjectorInterface $injector)
    {
        if (!isset(self::$_injectors[$name])) {
            self::$_injectors[$name] = $injector;
            return true;
        }
        return false;
    }

    /**
     * Add multiple injectors
     */
    public static function addInjectors(array $injectors)
    {
        foreach ($injectors as $injector) {
            self::addInjector($injector);
        }
    }

    /**
     * Remove an existing injector
     */
    public static function removeInjector($name)
    {
        if (isset(self::$_injectors[$name])) {
            unset(self::$_injectors[$name]);
            return true;
        }
        return false;
    }
   
    /**
     * Get the specified injector
     */
    public static function getInjector($name)
    {
        return isset(self::$_injectors[$name]) ?
               self::$_injectors[$name] :
               null;
    }
   
    /**
     * Check if the specified injector exists
     */
    public static function injectorExists($name)
    {
        return isset(self::$_injectors[$name]);
    }

    /**
     * Add a service
     */
    public static function addService($name, AbstractService $service)
    {
        if (!isset(self::$_services[$name])) {
            self::$_services[$name] = $service;
            return true;
        }
        return false;
    }

    /**
     * Add multiple services
     */
    public static function addServices(array $services)
    {
        foreach($services as $service) {
            self::addService($service);
        }
    }

    /**
     * Remove an existing service
     */
    public static function removeService($name)
    {
        if (isset(self::$_services[$name])) {
            unset(self::$_services[$name]);
            return true;
        }
        return false;
    }

    /**
     * Get the specified service (if not created already, the associated injector builds the service)
     */
    public static function getService($name)
    {
        // if the service has been added and cached, get it from the cache
        if (isset(self::$_services[$name])) {
            return self::$_services[$name];
        }
        // otherwise, attempt to build the service via the associated injector and save it to the cache
        if (!$injector = self::getInjector($name)) {
            throw new ServiceLocatorException(‘Unable to get the injector associated to the specified service.’);
        }
        $service = $injector->create();
        self::addService($name, $service);
        return $service;
    }

    /**
     * Check if the specified service exists
     */
    public static function serviceExists($name)
    {
        return isset(self::$_services[$name]);
    }
}

 

(MyApplication/Service/ServiceLocatorException.php)

<?php

namespace MyApplicationService;

class ServiceLocatorException extends Exception{}

Indeed, that was a lot to digest! But don’t rest yet; there’s one more layer to show. This one is charged with implementing the corresponding dependency injection containers, and its source code looks like this:

(MyApplication/Injector/InjectorInterface.php)

<?php

namespace MyApplicationInjector;

interface InjectorInterface
{
    public function create();
}

 

(MyApplication/Injector/MysqlAdapterInjector.php)

<?php

namespace MyApplicationInjector;
use MyApplicationDatabase;

class MysqlAdapterInjector implements InjectorInterface
{
    protected $_mysqlAdapter;

    /**
     * Create an instance of the MysqlAdapter class
     */
    public function create()
    {
        if ($this->_mysqlAdapter === null) {
            $this->_mysqlAdapter = new DatabaseMysqlAdapter(array(‘host’, ‘user’, ‘password’, ‘database’));
        }
        return $this->_mysqlAdapter;
    }
}

 

(MyApplication/Injector/UserServiceInjector.php)

<?php

namespace MyApplicationInjector;
use MyApplicationDatabase,
    MyApplicationCollection,
    MyApplicationMapper,
    MyApplicationService;

class UserServiceInjector implements InjectorInterface
{
    /**
     * Create a user service
     */
    public function create()
    {
        $mysqlInjector = new MysqlAdapterInjector;
        $adapter = $mysqlInjector->create();
        return new ServiceUserService(
            new MapperUserMapper($adapter,
                new CollectionUserCollection
            )
        );
    }
}

It’s hard to believe, but we’ve finally accomplished our mission. At this point, you have at your disposal (from top to bottom, literally) the source code for this example application. But hold on a second! Since working with this user service requires a huge number of classes, I’m going to appeal to the functionality of an autoloader to lazy-load them.

This autoloader is similar to others that you’ve seen here at the Developer Shed Network. Its implementation is as follows:  

(MyApplication/Loader/Autoloader.php)

<?php

namespace MyApplicationLoader;

class Autoloader
{
    private static $_instance;
   
    /**
     * Get the Singleton instance of the autoloader
     */
    public static function getInstance()
    {
        if (self::$_instance === null) {
            self::$_instance = new self;
        }
        return self::$_instance;
    } 
   
    /**
     * Reset the instance of the autoloader
     */
    public static function resetInstance()
    {
        self::$_instance = null;
    }
   
    /**
     * Class constructor
     */
    private function __construct()
    {
        spl_autoload_register(array(__CLASS__, ‘load’));
    }
   
    /**
     * Prevent to clone the instance of the autoloader
     */
    private function __clone(){}

    /**
     * Load a given class or interface
     */
    public static function load($class)
    {
        $file = str_replace(array(‘MyApplication’, ”), array(”, ‘/’), $class) . ‘.php’;
        require $file;
        if (!class_exists($class) && !interface_exists($class)) {
            throw new AutoloaderException(‘The requested class or interface’ . $class . ‘ was not found.’);
        }
    }  
}

 

(MyApplication/Loader/AutoloaderException.php)

<?php

namespace MyApplicationLoader;

class AutoloaderException extends Exception{}

The above autoloader is a Singleton that includes classes and interfaces on demand via the SPL stack. If you’ve already grasped its driving logic, it’s time (finally) to set up a script that shows the actual functionality of the earlier user service.

This script will be created in the following segment, so click on the link below and keep reading.

{mospagebreak title=The service in action: fetching user entities from storage}

To demonstrate how to work with the previous user service in a concrete situation, I’ll be utilizing the following “users” MySQL table. It already contains a couple of trivial rows. Take a look at it: 

Admittedly, the above table isn’t the most complex one that you’ll see in your lifetime, but it’s good enough for demonstration purposes. Now, say that you need to fetch the above table records by means of the pertinent service. In a case like this, you could use a script similar to the following:   

(index.php)

<?php

use MyApplicationLoaderAutoloader as Autoloader,
    MyApplicationServiceServiceLocator as ServiceLocator,
    MyApplicationInjectorUserServiceInjector as UserServiceInjector;

// include the autoloader
require_once ‘Loader/Autoloader.php’;
Autoloader::getInstance();

//add the user service injector to the service locator
ServiceLocator::addInjector(‘user’, new UserServiceInjector);

// create an instance of the user service via the associated injector
$userService = ServiceLocator::getService(‘user’);

// fetch a single user from the persistence layer and display their data
$user = $userService->findById(1);
echo $user->fname . ‘ ‘ . $user->lname . ‘ ‘ . $user->email;

/*

displays the following

Alex Gervasio alex@domain.com

*/

// fetch all users from the persistence layer and display their data
$users = $userService->findAll();
foreach($users as $user) {
    echo $user->fname . ‘ ‘ . $user->lname . ‘ ‘ . $user->email . ‘<br />’;}

 

/*

displays the following

Sandra Smith sandra@domain.com
Alex Gervasio alex@domain.com

*/

As seen above, once an instance of the service is grabbed via the service locator helper, it’s used to retrieve all the records present in the previous “users” table via the “findById()” and “findAll()” methods. This shows how easy it is to put the service into action, and how it permits you to interact with different clients without having to modify the infrastructure of an application.

 

In addition, you may have noticed that the service is created by using its associated dependency injection container. This is added to the service locator in the following line:

//add the user service injector to the service locator
ServiceLocator::addInjector(‘user’, new UserServiceInjector);

In this case, this was done within the same script that fetches the database records (and that in turn reconstitutes the corresponding user entities); nevertheless, in production environments, all of the initialization tasks, such as injecting DI containers or creating a database adapter, should be performed in a separate bootstrap file or class. Keep this in mind when developing your MVC applications, and you’ll save yourself from many maintenance headaches.

And now that you’ve seen how simple it is employ this sample service to retrieve some user objects via a couple of generic finders, it’s time to show how to use it to add a new user to the database and delete an existing one.     

Creating a final example: saving, removing domain objects and more

In fact, saving and removing users from the earlier MySQL table via the service layer is a simple process. Of course, the best way to prove this is by example. Below, I coded one for you that performs these tasks in a comprehensive manner:

(index.php)

<?php

use MyApplicationLoaderAutoloader as Autoloader,
    MyApplicationServiceServiceLocator as ServiceLocator,
    MyApplicationInjectorUserServiceInjector as UserServiceInjector;

// include the autoloader
require_once ‘Loader/Autoloader.php’;
Autoloader::getInstance();

//add the user service injector to the service locator
ServiceLocator::addInjector(‘user’, new UserServiceInjector);

// create an instance of the user service via the associated injector
$userService = ServiceLocator::getService(‘user’);

// insert a new user into the persistence layer
$user = new MyApplicationEntityUser(array(
    ‘fname’ => ‘Jennifer’,
    ‘lname’ => ‘Smith’,
    ‘email’ => ‘jennifer@domain.com’
));
$userService->insert($user);

// remove a user from the persistence layer
$userService->delete(1);

As the above code snippet shows, inserting a new user into the storage and deleting an existing one is reduced to calling the service’s “insert()” and “delete()” methods, and nothing else. If you wan tot see how to fetch all current users in XML, the example below should answer your questions:

(index.php)

<?php

use MyApplicationLoaderAutoloader as Autoloader,
    MyApplicationServiceServiceLocator as ServiceLocator,
    MyApplicationInjectorUserServiceInjector as UserServiceInjector;

// include the autoloader
require_once ‘Loader/Autoloader.php’;
Autoloader::getInstance();

//add the user service injector to the service locator
ServiceLocator::addInjector(‘user’, new UserServiceInjector);

// create an instance of the user service via the associated injector
$userService = ServiceLocator::getService(‘user’);


// fetch all users in XML format
header(‘Content-type: text/xml’);
echo $userService->toXML();

There you have it. This example effectively shows how to use the service for performing a variety of tasks, and not merely running CRUD functions on domain objects. It also demonstrates how easy it is to call it from different client layers (for instance, from within an action controller or even directly from a view).

Naturally, this doesn’t imply that you should use a service in every possible situation. However, its intrinsic flexibility turns it into a hard-to-beat contender, especially when developing medium/large-scale applications that need to be interfaced with multiple clients. So, keep the pattern at hand in your developer’s toolbox. Who knows? Maybe you’ll need to use it sooner than you think.  

Final Thoughts

Finally, we’ve come to the end of this series. You’ve learned how to implement a functional and easily customizable service layer. As I just noted, the construction of a service isn’t something that you need to face in every use case. That’s especially true in the PHP field, where Domain-Driven Design is still in its infancy (even though it’s moving forward quickly).

Despite this, as PHP applications grow in complexity and size, sooner or later you’ll need to create a flexible structure that permits you to extend their functionality or even migrate them to a different framework. In those cases, you’ll love having a service layer at your disposal, take my word for it.

See you in the next PHP tutorial!

Google+ Comments

Google+ Comments