Home arrow PHP arrow PHP and the Law of Demeter

PHP and the Law of Demeter

In this PHP programming tutorial, we will be looking at the Law of Demeter and learn how to avoid violating its strict rules.

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

print this article
SEARCH DEV SHED

TOOLS YOU CAN USE

advertisement

This might just be my opinion, but if I had to choose the flashiest star in the sky that is PHP, I'd have to mutter two words: Dependency Injection. Seated in solid logic, DI (or Inversion of Control) is the new kid on the block, which allows you to create modular, highly-testable classes that ask for their collaborators - either in their constructors or setters - instead of looking for them.

While I have to confess that I'm a big fan of DI, I admit that there are cases where it can be difficult to figure out what dependencies to inject into a given class. Further more, sometimes we're not careful enough when it comes to defining the responsibilities that classes will have in the context of an application, and end up passing them the wrong collaborators. Or - even worse - we pass in dependencies that are used internally as bridges or mediators for acquiring other objects, which makes classes "aware" of functionality that's completely unnecessary.

When this happens, it's a clear symptom of a common issue known as the "Law of Demeter" breakage. In case the name doesn't ring any bells, the "Law of Demeter" (http://en.wikipedia.org/wiki/Law_of_Demeter) - or the Principle of Least Knowledge - is a paradigm that allows to create loosely-coupled classes, based on a simple concept: each class should be designed to work properly using only the dependencies that it really needs.

This has more to do with common sense than any obscure programming principle. With a little willpower and planning, however, it's possible to create classes that adhere to the Law of Demeter.

I'll be demonstrating how the violation of this law can cause some undesired coupling effects in PHP classes, and how these artifacts can be fixed with minor hassle.

Recreating a Real Scenario: Moving Data Between a Domain Model and a Database

According to the formal definition excerpted from Wikipedia, a method of an object that doesn't invoke the following objects:

1) The object itself.
2) The method's parameters.
3) Any objects created/instantiated within the method. 4) The object's direct component objects. 5) A global variable, accessible by the object, in the scope of the method.

flagrantly infringes the Law of Demeter. While understanding the theoretical concepts is all well and fine, the best manner to understand the side effects of violating the law is by example. Keeping with this, I'm going to show you a use case where this occurs quite frequently: let's say that we need to implement a mapping layer that moves data between a domain model and a MySQL database, while keeping both isolated from one another.

The set of classes that compose the model are as follows:

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

    
/**
     * 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;
    }         
}

You may be familiar with the above classes, as Ive used them in some previous articles. In any case, all the earlier abstract parent does is define the structure and constraints of generic entities, while its subclass does something similar, only with users.

So far, these entity classes are pretty easy to grasp, so let's make the domain model a bit more functional by adding a class that handles the collections of domain objects. Here's the interface implemented by this collection, followed by its originating class:

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

As shown in the above code snippet, the "EntityCollection" class does exactly what its name suggests: it traverses, counts, and accesses a collection of entities by using an array-like notation (along with a few concrete methods). I assume that you must have coded a class like this several times before, so I'm not going to waste time explaining its inner workings.

Instead, let's recap what's been achieved so far: at this time there's a simple model domain, where user entities live in happy isolation, completely ignorant of any type of persistence mechanism that might exist out of their boundaries.

Although this is well and fine, there's still no single sign of a violation of the Law of Demeter. Well, be patient and let me show you the layer that will be responsible for accessing the underlying database and persisting the previous model.

Here are the elements that comprise this brand new tier:

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

As shown above, this data access layer is a no-brainer, as it's made up of a segregated interface and one single implementer, which turns out to be a MySQL abstraction class that executes a few common database operations, such as running queries, counting and retrieving table rows.

While the driving logic of the previous class (and its associated interface) is pretty easy to follow, it's valid to point out that it doesn't violate the Law of Demeter either. As I said before though, my goal here is to implement a set of data mappers that interconnect the earlier domain model with the persistence layer just defined.

To do so, I could take a more "relaxed" approach and build a service locator, which would be responsible for providing the mappers with their collaborators. It makes sense, doesn't it? Based on this idea, here're the interfaces that are implemented by the mentioned service locator, along with its spawning class:

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

The above "ServiceLocator" class implements a couple of trivial interfaces, allowing you to save and remove generic resources from an array-based registry. While the functionality of this locator seems to be acceptable when analyzed in isolation, it's relatively easy to take it to the next level. But the question that comes up here is: how can this be done?

Well, the class could be used for providing the aforementioned mappers with the dependencies that they need to work as intended. Sounds like a decent approach that would be implemented in a few easy steps, right?

As usual, the best manner to demonstrate this concept is with a concrete example. Thus, in the following section I'll be constructing the pertaining mappers, which will exploit the functionality offered by the earlier service locator to acquire their collaborators.

Moving Data Between the Data Access and Persistence Layers: Building a Set of Mappers

If you've come to this point in the article, it's because you're really interested in seeing what's so wrong with breaking the Law of Demeter. Again, the best way to demonstrate this is a concrete code sample, so be sure to check out the definition of the following data mappers, something that hopefully will make things clear for you:

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

At a glance, the mappers look pretty good, as the first one is an abstract parent that encapsulates the functionality required to perform CRUD operations in a MySQL database. This makes it possible to fetch one or multiple entities from storage, insert new domain objects, and delete existing ones as well. On the other hand, its subclass "User" does something similar, but specifically with user entities. Again, what's the big issue with these classes?

If you look closer at the parent's constructor, you'll realize that it takes an instance of the earlier service locator, which is used to get the database adapter. While admittedly this falls within the "while list" of the Law of Demeter, from a pragmatic standpoint, it's a clear violation of it. After all, why does the mapper have to use a mediator to obtain the adapter, when it can get it directly?

Even if the locator were saved as a protected/private property, this wouldn't make any difference. In both cases, the mapper is asking for the wrong collaborator, thus gaining access to functionality that it doesn't need at all. Not to mention that this approach has introduced a strong coupling, which makes testing the mappers a painful and inflexible process.

And if all of these reasons still don't convince you from the explosion of side effects generated by the breakage of the Law of Demeter, let me show you a short script, which uses all the classes developed previously to fetch a few users from the following MySQL table:

That's the table in question. Now, here's the mentioned script:


<?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;</p>
// 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 '<br />'
}

// 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);

If you run this script, (and assuming that you created the previous "users" table), it will nicely retrieve the corresponding domain objects, then insert a new user, and finally delete the first row from the corresponding table. To do so, however, first the database adapter needs to be saved in the service locator, and finally this one injected into the user mapper.

This approach not only requires you to write more lines of code, (which is the least of the damages), but it makes the whole API confusing, as it's not clear why the mapper needs the service locator to do its business. Of course, it's relatively easy to fix these issues and make the mapper to follow the commandments of the Law of Demeter. However, the solution will be implemented in the final part of this tutorial.

Final Thoughts

In this first post, I attempted to provide you with a humble introduction to what the Law of Demeter is and how its infringement can turn our PHP classes into heavily-coupled structures that are hard to test in isolation. As I said before, though, it's fairly simple to construct classes that stick to the law's requirements. Thus, if you're interested in seeing the fix up, don't miss the final part!


 
 
>>> 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: