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.

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)

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

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)

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

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)

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

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)

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

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)

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

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

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!

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

chat sex hikayeleri Ensest hikaye