Home arrow PHP arrow PHP: Applying the Law of Demeter

PHP: Applying the Law of Demeter

In this second part of a two-part series, we will be learning how to apply the Law of Demeter in PHP.

By: Alejandro Gervasio
Rating: starstarstarstarstar / 0
August 18, 2011

print this article
SEARCH DEV SHED

TOOLS YOU CAN USE

advertisement

Perhaps better known as "The Principle of Least Knowledge", the Law of Demeter (http://en.wikipedia.org/wiki/Law_of_Demeter) is a programming paradigm, which encourages the design of loosely-coupled classes that take advantage only of the functionality offered by their closest collaborators. Although this may sound logical, the truth is that sometimes we tend (either due to ignorance or as a consequence of bad design), to provide our classes with dependencies that aren’t actually required, or act as mediators to access the ones that are needed.

As with many other concepts of object-oriented programming, the best way to understand the underlying logic behind the Law of Demeter is by setting up an example that breaks the principle in question. In line with this idea, in the previous part of this tutorial, I went through the implementation of a small – yet extendable — PHP framework, whose set of data mappers committed a cardinal sin: they injected a service locator into their internals, only to get the database adapter they needed to run CRUD operations in the persistence layer.

Learning from our mistakes, in this final installment I’ll be refactoring the framework’s mapping layer, which this time will follow the commandments of the Law of Demeter, breaking up the undesired coupling introduced by the aforementioned service locator.

Before we begin however, if you missed the first part of this series - or need a refresher - you can find it here:

http://www.devshed.com/c/a/PHP/PHP-and-the-Law-of-Demeter-43810/

A Quick Recap: Showing the Badly-Designed Version of the Earlier Mapping Layer

If you haven't viewed the previous installment - or don't want to go back and review it - below I will reintroduce its source files, so that you can study them in detail and gain a better understanding of how they work.

Here’s the framework domain model, which is made up of some straightforward interfaces and classes, tasked with modeling generic entities, users and collections of domain objects. Check them out:

(SampleApp/Model/AbstractEntity.php)

<?php
namespace SampleAppModel;
abstract class 
AbstractEntity
{
protected 
$_values = array();
protected 
$_allowedFields = array();
    
/**
    * Constructor
    */
   
public function __construct(array $fields)
   {
       foreach (
$fields as $name = > $value) {
$this->$name $value;
       }
   }<
br />
   
/**
    * Assign a value to the specified field via the corresponding mutator (if it exists);
    * otherwise, assign the value directly to the '$_values' array
    */
   
public function __set($name$value)
   {
       if (!
in_array($name$this->_allowedFields)) {
         throw new 
InvalidArgumentException("Setting 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 of the specified field (via the getter if it exists or by getting it from the $_values array)
    */
   
public function __get($name)
   {
       if (!
in_array($name$this->_allowedFields)) {
throw new 
InvalidArgumentException("Getting 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;
       }
       else if (isset(
$this->_values[$name])) {
return 
$this->_values[$name];
       }
       else {
throw new 
InvalidArgumentException("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 
InvalidArgumentException("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 
InvalidArgumentException("Unsetting the field '$name' is not allowed for this entity.");
       }
       if (isset(
$this->_values[$name])) {
unset(
$this->_values[$name]);
return 
true;
       }
       throw new 
InvalidArgumentException("The field '$name' has not been set for this entity yet.");
   }
   
/**
    * Get an associative array with the values assigned to the fields of the entity
    */
   
public function toArray()
   {
       return 
$this->_values;
   }
}

(SampleApp/Model/User.php)

<?php
namespace SampleAppModel;
class 
User extends AbstractEntity
{
   protected 
$_allowedFields = array('id''name''email');
   
/**
    * Set the user's ID
    */
   
public function setId($id)
   {
       if(!
filter_var($idFILTER_VALIDATE_INT, array('options' => array('min_range' => 1'max_range' => 999999)))) {
throw new 
InvalidArgumentException('The ID of the user is invalid.');
       }
       
$this->_values['id'] = $id;
   }
   
/**
    * Set the user's full name
    */
   
public function setName($name)
   {
       if (
strlen($name) < || strlen($name) > 32) {
throw new 
InvalidArgumentException('The name of the user is invalid.');
       }
       
$this->_values['name'] = $name;
   }
   
/**
    * Set the user's email address
    */
   
public function setEmail($email)
   {
       if (!
filter_var($emailFILTER_VALIDATE_EMAIL)) {
throw new 
InvalidArgumentException('The email address of the user is invalid.');
       }
       
$this->_values['email'] = $email;
   }
}

(SampleApp/Model/Collection/CollectionInterface.php)

<?php
namespace SampleAppModelCollection;
use 
SampleAppModel;
interface 
CollectionInterface extends CountableIteratorAggregateArrayAccess
{
   public function 
toArray();
   public function 
clear();
   public function 
reset();
   public function 
add($keyModelAbstractEntity $entity);
   public function 
get($key);
   public function 
remove($key);
   public function 
exists($key);
}

(SampleApp/Model/Collection/EntityCollection.php)

<?php
namespace SampleAppModelCollection;
use 
SampleAppModel;
class 
EntityCollection implements CollectionInterface
{
   protected 
$_entities = array();
   
/**
   * Constructor
   */
   
public function  __construct(array $entities = array())
   {
       
$this->_entities $entities;
       
$this->reset();
   }
   
/**
    * Get the entities stored in the collection
    */
   
public function toArray()
   {
       return 
$this->_entities;
   }
   
/**
    * Clear the collection
    */
   
public function clear()
   {
       
$this->_entities = array();
   }
   
/**
    * Rewind the collection
    */
   
public function reset()
   {
       
reset($this->_entities);
   }
   
/**
    * Add an entity to the collection
    */
   
public function add($keyModelAbstractEntity $entity) {
       return 
$this->offsetSet($key$entity);
   }
   
/**
    * Get from the collection the entity with the specified key
    */
   
public function get($key)
   {
       return 
$this->offsetGet($key);
   }
   
/**
    * Remove from the collection the entity with the specified key
   */
   
public function remove($key)
   {
       return 
$this->offsetUnset($key);
   }
   
/**
   * Check if the entity with the specified key exists in the collection
   */
   
public function exists($key)
   {
       return 
$this->offsetExists($key);
   }
   
/**
    * Count the number of entities in the collection
   */
   
public function count()
   {
       return 
count($this->_entities);
   }
   
/**
    * Get the external array iterator
   */
   
public function getIterator()
   {
       return new 
ArrayIterator($this->toArray());
   }
   
/**
    * Add an entity to the collection
   */
   
public function offsetSet($key$entity)
   {
       if (!
$entity instanceof ModelAbstractEntity) {
throw new 
InvalidArgumentException('The entity to be added to the collection must be an instance of EntityAbstract.');
       }
       if (!isset(
$key)) {
$this->_entities[] = $entity;
      }
       else {
$this->_entities[$key] = $entity;
      }
       return 
true;
   }  
   
/**
    * Remove an entity from the collection
   */
   
public function offsetUnset($key)
   {
       if (
$key instanceof ModelAbstractEntity) {
$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
   */
   
public function offsetGet($key)
   {
       return isset(
$this->_entities[$key])
       ? 
$this->_entities[$key] : null;
   }
   
/**
    * Check if the specified entity exists in the collection
   */
   
public function offsetExists($key)
   {
       return isset(
$this->_entities[$key]);
   }
}

The above classes are responsible for setting up a simple domain layer where user entities and collections of domain objects live in isolation.

Having listed the framework’s domain layer, it’s time to show the next one, which is charged with accessing the underlying persistence mechanism. In this case, a MySQL database:

(SampleApp/Library/Database/DatabaseAdapterInterface.php)

<?php
namespace SampleAppLibraryDatabase;
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();
}

(SampleApp\Library/Database/MysqlAdapter)

<?php
namespace SampleAppLibraryDatabase;
use 
SampleAppCommon;
class 
MysqlAdapter implements DatabaseAdapterInterfaceCommonAbstractResource
{
   protected 
$_config = array();
   protected 
$_link;
   protected 
$_result;
   
/**
    * Constructor
   */
   
public function __construct(array $config)
   {
       if (
count($config) !== 4) {
           throw new 
InvalidArgumentException('Invalid number of connection parameters.');
       }
       
$this->_config $config;
   }
   
/**
    * 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 
RunTimeException('Error connecting to the server : ' mysqli_connect_error());
        }
     unset(
$host$user$password$database);
       }
      return 
$this->_link;
   }
   
/**
    * Execute the specified query
   */
   
public function query($query)
   {
      if (!
is_string($query) || empty($query)) {
throw new 
InvalidArgumentException('The specified query is not valid.');
      }
      
// lazy connect to MySQL
       
$this->connect();
      if (!
$this->_result mysqli_query($this->_link$query)) {
throw new 
RunTimeException('Error executing the specified query : ' $query mysqli_error($this->_link));
      }
      return 
$this->_result;
   }
   
/**
    * 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
   */
   
public function fetch($mode MYSQLI_ASSOC)
   {
     if (
$this->_result === null) {
return 
false;
     }
      if (!
in_array($mode, array(MYSQLI_NUMMYSQLI_ASSOCMYSQLI_BOTH))) {
      
$mode MYSQLI_ASSOC;
     }
      if ((
$row mysqli_fetch_array($this->_result$mode)) === false) {
$this->freeResult();
     }
     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) {
       return 
false;
      }
      
mysqli_free_result($this->_result);
      return 
true;
   }
   
/**
    * Close explicitly the database connection
   */
   
public function disconnect()
   {
      if (
$this->_link === null) {
      return 
false;
      }
      
mysqli_close($this->_link);
      
$this->_link null;
      return 
true;
   }
   
/**
    * Close automatically the database connection when the instance of the class is destroyed
   */
   
public function __destruct()
   {
      
$this->disconnect();
   }
}

That was fairly easy to grasp wasn’t it? However, we’re not done listing the framework’s source classes and interfaces. Below are the elements that compose the service locator mentioned at the start of this article:

(SampleApp/Common/AbstractResource.php)

<?php
namespace SampleAppCommon;
interface 
AbstractResource
{}

(SampleApp/Common/RegistrableInterface.php)

<?php
namespace SampleAppCommon;
interface 
RegistrableInterface
{
   public function 
set($keyAbstractResource $resource);
   public function 
get($key);
   public function 
remove($key);
   public function 
exists($key);
}

(SampleApp/Common/ServiceLocator.php)

<?php
namespace SampleAppCommon;
class 
ServiceLocator implements RegistrableInterface
{
   protected 
$_resources = array();
   
/**
    * Set the specified resource
   */
   
public function set($keyAbstractResource $resource)
   {
      if (!isset(
$this->_resources[$key])) {
      
$this->_resources[$key] = $resource;
      }
       return 
$this;
   }
   
/**
    * Get the specified resource
   */
   
public function get($key)
   {
      if (isset(
$this->_resources[$key])) {
      return 
$this->_resources[$key];
      }
      throw new 
InvalidArgumentException('The requested resource does not exist.');
   }
   
/**
    * Remove the specified resource
   */
   
public function remove($key)
   {
      if (isset(
$this->_resources[$key])) {
      unset(
$this->_resources[$key]);
      return 
$this;
     }
      throw new 
InvalidArgumentException('The resource to be removed does not exist.');
   }
   
/**
   * Check if the specified resource exists
   */
   
public function exists($key)
   {
     return isset(
$this->_resources[$key]);
   }
}

As you can see, the above service locator is nothing but an array-based registry, which allows you to save and remove generic resources by means of some simple methods. Of course, in a situation where dependency injection is the workhorse that pulls out the chariots of an application or framework, such a locator won’t be needed at all.

In this case - and for demonstrative purposes - I decided to inject the locator into the constructor of the following mappers (yes, the evil guys that break up the Law of Demeter): 

(SampleApp/Model/Mapper/AbstractMapper.php)

<?php
namespace SampleAppModelMapper;
use 
SampleAppLibraryDatabase,
   
SampleAppModelCollection,
   
SampleAppModel,
   
SampleAppCommon;
abstract class 
AbstractMapper
{
   protected 
$_adapter;
   protected 
$_entityTable;
   protected 
$_entityClass;
   
/**
   * Constructor
   */
   
public function __construct(CommonServiceLocator $serviceLocator, array $entityOptions = array())
   {
     
// Get the database adapter via the service locator
      
$this->_adapter $serviceLocator->get('adapter');
    
// Set the entity table if the options has been specified
      
if (isset($entityOptions['entityTable'])) {
      
$this->setEntityTable($entityOptions['entityTable']);
     }
    
// Set the entity class if the options has been specified
      
if (isset($entityOptions['entityClass'])) {
      
$this->setEntityClass($entityOptions['entityClass']);
     }
     
// check if the entity options have been set
      
$this->_checkEntityOptions();
   }
   
/**
    * Check if the entity options have been set
   */
   
protected function _checkEntityOptions()
   {
      
// check if the entity table has been set
      
if (!isset($this->_entityTable)) {
      throw new 
InvalidArgumentException('The entity table has been not set yet.');
      }
      
// check if the entity class has been set
      
if (!isset($this->_entityClass)) {
      throw new 
InvalidArgumentException('The entity class has been not set yet.');
      }
   }
   
/**
    * Get the database adapter
   */
   
public function getAdapter()
   {
      return 
$this->_adapter;
   }
   
/**
    * Set the entity table
   */
   
public function setEntityTable($entityTable)
   {
      if (!
is_string($entityTable) || empty($entityTable)) {
      throw new 
InvalidArgumentException("The given entity table '$entityTable' is invalid.");
      }
       
$this->_entityTable $entityTable;
   }
   
/**
    * Get the entity table
   */
   
public function getEntityTable()
   {
     return 
$this->_entityTable;
   }
   
/**
    * Set the entity class
   */
   
public function setEntityClass($entityClass)
   {
      if (!
is_subclass_of($entityClass'SampleAppModelAbstractEntity')) {
      throw new 
InvalidArgumentException("The given entity class '$entityClass' is invalid. It must be a subclass of AbstractEntity.");
     }
     
$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 
null;
     }
      return new 
$this->_entityClass($data);
   }
   
/**
    * Find all the entities that match the specified criteria (or all when no criteria are given)
   */
   
public function find($criteria '')
   {
     
$collection = new CollectionEntityCollection;
      
$this->_adapter->select($this->_entityTable$criteria);
      while (
$data $this->_adapter->fetch()) {
      
$collection[] = new $this->_entityClass($data);
      }
      return 
$collection;
   }
   
/**
    * Insert a new row in the table corresponding to the specified entity
   */
   
public function insert($entity)
   {
      if (!
$entity instanceof $this->_entityClass) {
     throw new 
InvalidArgumentException("The entity to be inserted must be an instance of '$entityClass'.");
      }
      return 
$this->_adapter->insert($this->_entityTable$entity->toArray());
   }
   
/**
   * 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");
   }
}

 

(SampleApp/Model/Mapper/UserMapper.php)

<?php
namespace SampleAppModelMapper;
class 
UserMapper extends AbstractMapper
{
   protected 
$_entityTable 'users';
   protected 
$_entityClass 'SampleAppModelUser';
}

You don't have to be a genius to spot the flaw in the implementation of the previous mappers. Effectively, they use the service locator as a “middleman” to gain access to the database adapter, which is in turn stored in a class property. Even though subtle, this is a violation of the commandment “talk to your closest friends”, stated by the Law of Demeter, as the adapter should be directly passed in the constructor, instead of using an unneeded mediator.

Before I show you how simple it is to fix this issue, take a look at the following script, which shows how to use all of the classes (and interfaces) listed so far, in order to run a few CRUD operations in a “users” MySQL table.

Here’s how this script looks:  

<?php
use SampleAppLibraryLoaderAutoloader as Autoloader,
   
SampleAppCommonServiceLocator as ServiceLocator,
   
SampleAppLibraryDatabaseMysqlAdapter as MysqlAdapter,
   
SampleAppModelMapperUserMapper as UserMapper,
   
SampleAppModelUser as User;
// include the autoloader and create an instance of it
require_once 'Library/Loader/Autoloader.php';
$autoloader = new Autoloader;
// create an instance of the service locator and store the MySQL adapter in it
$serviceLocator = new ServiceLocator;
$serviceLocator->set('adapter', new MysqlAdapter(array(
     
'host',
     
'user',
     
'password',
     
'database'
   
))
);
// create an instance of the user mapper (the service locator is injected)
$userMapper = new UserMapper($serviceLocator);
// fetch all users from the storage and display their data
$users $userMapper->find();
foreach (
$users as $user) {
   echo 
' ID: ' $user->id
   
' Name: ' $user->name
   
' Email: ' $user->;email '<; >;';
}
// insert a new user into the storage
$user = new User(array(
   
'name' =>; 'Margaret Dennis',
   
'email' => <a href="mailto:'margaret@domain.com'">'margaret@domain.com'</a>
));
$userMapper->insert($user);
// delete an existing user from the storage
$userMapper->delete(1);

Even though the earlier script does a decent job when it comes to fetching, saving to and removing user entities from storage, this is done by using the service locator, which in this particular case isn’t required at all.

But fear not, as addressing this issue is a breeze. In the coming section I’ll be refactoring the troubling mappers, which this time will make use only of the dependencies that they require.

Fixing Things: Making the Data Mappers Adhere to the Law of Demeter

Turning the previous data mappers into classes that follow the commandments of the Law of Demeter is a simple process, reduced to eliminating the mediator (in this case the service locator), and injecting directly the corresponding database adapter.

As usual, the best way to demonstrate this is with concrete code. Thus, here’s how the abstract mapper looks (and hence its subclass), after providing it with the right collaborator:

(SampleApp/Model/Mapper/MapperInterface.php)

<?php
namespace SampleAppModelMapper;
interface 
MapperInterface
{
   public function 
findById($id);
   public function 
find($conditions '');
   public function 
insert($entity);
   public function 
delete($id);
}

 

(SampleApp/Model/Mapper/AbstractMapper.php)

<?php
namespace SampleAppModelMapper;
use 
SampleAppLibraryDatabase,
   
SampleAppModelCollection,
   
SampleAppModel;
abstract class 
AbstractMapper implements MapperInterface
{
   protected 
$_adapter;
   protected 
$_entityTable;
   protected 
$_entityClass;
   
/**
    * Constructor
   */
   
public function __construct(DatabaseDatabaseAdapterInterface $adapter, array $entityOptions = array())
   {
      
$this->_adapter $adapter;
      
// Set the entity table if the option has been specified
      
if (isset($entityOptions['entityTable'])) {
      
$this->setEntityTable($entityOptions['entityTable']);
     }
      
// Set the entity class if the option has been specified
      
if (isset($entityOptions['entityClass'])) {
      
$this->setEntityClass($entityOptions['entityClass']);
     }
     
// check if the entity options have been set
     
$this->_checkEntityOptions();
   }
   
/**
    * Check if the entity options have been set
   */
   
protected function _checkEntityOptions()
   {
      
// check if the entity table has been set
      
if (!isset($this->_entityTable)) {
      throw new 
InvalidArgumentException('The entity table has been not set yet.');
      }
      
// check if the entity class has been set
      
if (!isset($this->_entityClass)) {
      throw new 
InvalidArgumentException('The entity class has been not set yet.');
      }
   }
   
/**
    * Get the database adapter
   */
   
public function getAdapter()
   {
     return 
$this->_adapter;
   }
   
/**
    * Set the entity table
   */
   
public function setEntityTable($entityTable)
   {
     if (!
is_string($entityTable) || empty($entityTable)) {
      throw new 
InvalidArgumentException("The given entity table '$entityTable' is invalid.");
     }
      
$this->_entityTable $entityTable;
   }
   
/**
    * Get the entity table
   */
   
public function getEntityTable()
   {
    return 
$this->_entityTable;
   }
   
/**
    * Set the entity class
   */
   
public function setEntityClass($entityClass)
   {
     if (!
is_subclass_of($entityClass'SampleAppModelAbstractEntity')) {
      throw new 
InvalidArgumentException("The given entity class '$entityClass' is invalid. It must be a subclass of AbstractEntity.");
     }
     
$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 
null;
      }
      return new 
$this->_entityClass($data);    }
   
/**
    * Find all the entities that match the specified criteria (or all when no criteria are given)
   */
   
public function find($criteria '')
   {
     
$collection = new CollectionEntityCollection;
     
$this->_adapter->select($this->_entityTable$criteria);
      while (
$data $this->_adapter->fetch()) {
     
$collection[] = new $this->_entityClass($data);
      }
       return 
$collection;
   }
   
/**
    * Insert a new row in the table corresponding to the specified entity
   */
   
public function insert($entity)
   {
     if (!
$entity instanceof $this->_entityClass) {
     throw new 
InvalidArgumentException("The entity to be inserted must be an instance of '$entityClass'.");
      }
     return 
$this->_adapter->insert($this->_entityTable$entity->toArray());
   }
   
/**
    * 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");
   }
}

Aside from implementing a brand new segregated interface, which allows you to easily create different data mappers, now the above “AbstractMapper” class accepts the database adapter in its constructor. This subtle - yet relevant - not only sticks with the Law of Demeter, but it also breaks up the coupling created between the service locator and the mapper in question. In summary, we get the best of both worlds.

What’s more, if you’re curious and want to see how to consume the refactored version of the mapper, the following script will of help. Take a look at it:

<?php
use SampleAppLibraryLoaderAutoloader as Autoloader,
   
SampleAppLibraryDatabaseMysqlAdapter as MysqlAdapter,
   
SampleAppModelMapperUserMapper as UserMapper,
   
SampleAppModelUser as User;
// include the autoloader and create an instance of it
require_once __DIR__ '/Library/Loader/Autoloader.php';
$autoloader = new Autoloader;
// create an instance of the user mapper and inject the MySQL adapter into it
$userMapper = new UserMapper(
   new 
MysqlAdapter(array(
      
'host',
      
'user',
      
'password',
      
'database'
   
))
);
// fetch all users from the storage and display their data
$users $userMapper->find();
foreach (
$users as $user) {
   echo 
' ID: ' $user->id
    
' Name: ' $user->name
    
' Email: ' $user->email '< >';
}
// insert a new user into the storage
$user = new User(array(
   
'name'=> 'Margaret Dennis'
   
'email' => <a href="mailto:'margaret@domain.com'">'margaret@domain.com'</a>
));
$userMapper->insert($user);
// delete an existing user from the storage
$userMapper->delete(1);

There you have it. Now that the database adapter is injected directly into the user mapper’s internals, the whole code is easier to read and consume, not to mention the improvements achieved in terms of decoupling and testability. Moreover, even when in this case I implemented a mapping layer that deliberately violated the Law of Demeter and showed the bunch of side effects that this produces, the same concept can be applied to different use cases. So, be conscious with the dependencies that you provide to your classes.

Final Thoughts

In this programming series you learned the basic concepts that surround the application of the Law of Demeter, and how its breakage can lead to create strongly-coupled classes that don’t adhere to the principles of good object-oriented design.

In light of this, if you’re planning to use dependency injection in the construction of your own PHP classes, make sure to design them only to ask for the dependencies that they really need. By fitting this subtle - yet crucial -requirement, you’ll be able to build testable, highly-decoupled classes more easily, which will have fewer responsibilities as well. What more can you ask for?

See you in the next PHP development tutorial!


 
 
>>> More PHP Articles          >>> More By Alejandro Gervasio
 

blog comments powered by Disqus
escort Bursa Bursa escort Antalya eskort
   

PHP ARTICLES

- Hackers Compromise PHP Sites to Launch Attac...
- Red Hat, Zend Form OpenShift PaaS Alliance
- PHP IDE News
- BCD, Zend Extend PHP Partnership
- PHP FAQ Highlight
- PHP Creator Didn't Set Out to Create a Langu...
- PHP Trends Revealed in Zend Study
- PHP: Best Methods for Running Scheduled Jobs
- PHP Array Functions: array_change_key_case
- PHP array_combine Function
- PHP array_chunk Function
- PHP Closures as View Helpers: Lazy-Loading F...
- Using PHP Closures as View Helpers
- PHP File and Operating System Program Execut...
- PHP: Effects of Wrapping Code in Class Const...

Developer Shed Affiliates

 


Dev Shed Tutorial Topics: