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.

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)

[code] $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; } } [/code]

(SampleApp/Model/User.php)

[code] 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) < 2 || 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($email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException('The email address of the user is invalid.'); } $this->_values['email'] = $email; } }[/code]

(SampleApp/Model/Collection/CollectionInterface.php)

[code](SampleApp/Model/Collection/EntityCollection.php)

[code]_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($key, ModelAbstractEntity $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]); } }[/code]

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)

[code](SampleAppLibrary/Database/MysqlAdapter)

[code]_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_NUM, MYSQLI_ASSOC, MYSQLI_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(); } }[/code]

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)

[code](SampleApp/Common/RegistrableInterface.php)

[code](SampleApp/Common/ServiceLocator.php)

[code]_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]); } }[/code]

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)

[code]_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"); } }[/code]

 

(SampleApp/Model/Mapper/UserMapper.php)

[code]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:  

[code]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' => 'margaret@domain.com' )); $userMapper->insert($user); // delete an existing user from the storage $userMapper->delete(1);[/code]

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)

[code] 

(SampleApp/Model/Mapper/AbstractMapper.php)

[code]_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"); } }[/code]

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:

[code]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' => 'margaret@domain.com' )); $userMapper->insert($user); // delete an existing user from the storage $userMapper->delete(1);[/code]

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!

[gp-comments width="770" linklove="off" ]

antalya escort bayan antalya escort bayan Antalya escort diyarbakir escort