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:
/** * 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;
/** * 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;
/** * 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:
/** * 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 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(); } }
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:
/** * 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'; }
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;
/** * 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]); } }
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.