PHP
Article
By Alejandro Gervasio

Managing Class Dependencies: An Introduction to Dependency Injection, Service Locators, and Factories, Part 2

By Alejandro Gervasio

Managing Class Dependencies: An Introduction to Dependency Injection, Service Locators, and Factories

In the previous installment of this two-part series, I went though the development of a few straightforward examples exploring in a fairly approachable fashion a couple of methodologies new to PHP when it comes to handling class dependencies. In this primer, I covered the inclusion of sinful “new” operators in constructors, a method that should be quickly thrown in the trash can with no trace of guiltiness, as well as the use of injected factories.

While it’s fair to admit that factories do have a neat niche in a number of special use cases, I’m not so merciless as to condemn Service Locators and plain Dependency Injection to an unfair exile. In this final part we’ll take a closer look at the implementation of these popular patterns so that you can pick up the one that best suits the need at hand.

The Middle Man – Getting Class Collaborators via a Service Locator

While a Service Locator is considered in many cases a fancy, mind-blowing approach in the world of PHP, the truth is that the pattern, with some creative slants of course, has enjoyed of a long and venerable life within the language’s domain. At its roots, a Service Locator is nothing but a centralized registry, most of the time static (although dynamic ones are appearing in some popular frameworks), filled with a bunch of objects. Nothing more, nothing less.

As usual, one didactical approach to understand what’s going on under the hood of a Service Locator is by example. If we wanted to appeal to the pattern’s virtues for giving the FileStorage object from last time its dependency, the locator could be implemented like this:

<?php
namespace LibraryDependencyInjection;

interface ServiceLocatorInterface
{
    public function set($name, $service);
    public function get($name);
    public function has($name);
    public function remove($name);
    public function clear();
}
<?php
namespace LibraryDependencyInjection;

class ServiceLocator implements ServiceLocatorInterface
{
    protected $services = array();
    
    public function set($name, $service) {
        if (!is_object($service)) {
             throw new InvalidArgumentException(
                "Only objects can be registered with the locator.");
        }
        if (!in_array($service, $this->services, true)) {
            $this->services[$name] = $service;
        }
        return $this;
    }
    
    public function get($name) {
        if (!isset($this->services[$name])) {
            throw new RuntimeException(
                "The service $name has not been registered with the locator.");
        }
        return $this->services[$name];
    }

    public function has($name) {
        return isset($this->services[$name]);
    }
    
    public function remove($name) {
        if (isset($this->services[$name])) {
            unset($this->services[$name]);
        }
        return $this;
    }

    public function clear() {
        $this->services = array();
        return $this;
    }
}

Take my opinion as a form of catharsis if you want to, but I must confess that I’m rather reluctant to use a service locator over plain dependency injection, even if the locator is dynamic, rather than a static registry plagued with mutable global access issues. In either case, it’s worth looking at and seeing how it can be passed along into the FileStorage class. Here we go:

<?php
namespace LibraryFile;
use LibraryDependencyInjectionServiceLocatorInterface;

class FileStorage
{
    const DEFAULT_STORAGE_FILE = "data.dat";
    protected $serializer;
    protected $file;
    
    public function __construct(ServiceLocatorInterface $locator, $file = self::DEFAULT_STORAGE_FILE) {
        $this->setFile($file);
        $this->serializer = $locator->get("serializer");
    }
    
    public function setFile($file) {
        if (!is_file($file)) {
            throw new InvalidArgumentException(
                "The file $file does not exist.");
        }
        if (!is_readable($file) || !is_writable($file)) {
            if (!chmod($file, 0644)) {
                throw new InvalidArgumentException(
                    "The file $file is not readable or writable."); 
            }
        } 
        $this->file = $file;
        return $this;
    }
    
    public function read() {
        try {
            return $this->serializer->unserialize(
                @file_get_contents($this->file));
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
    
    public function write($data) {
        try {
            return file_put_contents($this->file,
                $this->serializer->serialize($data));    
        }
        catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }
}

To make things clear, I dropped the FileStorage class again from top to bottom, as this probably makes it easier to understand how its driving logic remains untouched with regards to its read()/write() methods. The constructor is by far the most relevant block as it consumes the locator which is then charged with the responsibility of getting in a serializer object.

While its implementation is straightforward, this approach is a far cry away from being innocent. First, FileStorage now has a strong dependency with the locator, even when it’s possible to pass around different implementations of it. Second, since the locator is inherently an intermediate provider of the class’ dependency, it infringes on the Law of Demeter at some point as well. This is an unavoidable artifact tied to the roots of the pattern. we should either learn to live with the issue or just forget about the pattern altogether. There’s no middle ground to ponder to here.

Here’s the code that shows how to get things finally rolling with the locator:

<?php
$locator = new ServiceLocator;
$locator->set("serializer", new Serializer());
$fileStorage = new FileStorage($locator);
$fileStorage->write("This is a sample string.");
echo $fileStorage->read();

Although rather primitive, the example shows the locator resembles at some point the structure of a basic Dependency Injection Container (DIC). The main difference is that the locator is usually injected or statically consumed inside the client classes, while a DIC always lives and breaths outside of them.

So far, we’ve covered a decent amount of common approaches used for managing class dependencies. Still, we haven’t swum in the waters of the simplest one of all… yep, the sweet creek of raw Dependency Injection!

--ADVERTISEMENT--

The Greatest and Simplest Finale – using Plain Dependency Injection

It might sound obvious, I know, but the most efficient and easiest way to provide FileStorage with a serializer object is with plain ol’ Dependency Injection, thus moving away from any coupling issues or breaking the commandments imposed by the Law of Demeter. Of course, I assume you’re clever enough and already knew that from the very beginning. Even so, it doesn’t hurt to show how the class in question would look when hooked up to this approach:

<?php
namespace LibraryFile;

class FileStorage
{
    const DEFAULT_STORAGE_FILE = "data.dat";
    protected $serializer; 
    protected $file;
    
    public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) {
        $this->setFile($file);
        $this->serializer = $serializer;
    }

    // the remaining methods go here
}

$fileStorage = new FileStorage(new Serializer);
$fileStorage->write("This is a sample string.");
echo $fileStorage->read();

That’s ridiculously easy to assimilate. In this case, the whole object graph is so anemic that appealing to the nuts and bolts of an external DIC to create it would be just pretty much overkill. On behalf of an instructive cause, though, we could build a primitive container, similar to the swift, lightweight Pimple, and see in a jiffy how to use it for wiring up all the objects that compose the file storage module:

<?php
namespace LibraryDependencyInjection;

interface ContainerInterface
{
    public function set($name, $service);
    public function get($name, array $params = array());
    public function has($name);
    public function remove($name);
    public function clear();
}
<?php
namespace LibraryDependencyInjection;

class Container implements ContainerInterface
{
    protected $services = array();
    
    public function set($name, $service) {
        if (!is_object($service)) {
             throw new InvalidArgumentException(
                "Only objects can be registered with the container.");
        }
        if (!in_array($service, $this->services, true)) {
            $this->services[$name] = $service;
        }
        return $this;
    }
    
    public function get($name, array $params = array()) {
        if (!isset($this->services[$name])) {
            throw new RuntimeException(
                "The service $name has not been registered with the container.");
        }
        $service = $this->services[$name];
        return !$service instanceof Closure 
            ? $service : call_user_func_array($service, $params);
    }
    
    public function has($name) {
        return isset($this->services[$name]);
    }
    
    public function remove($name) {
        if (isset($this->services[$name])) {
            unset($this->services[$name]);
        }
        return $this;
    }
    
    public function clear() {
        $this->services = array();
    }
}

The similarities between the DIC and the service locator coded before are anything but casual. The former, however, implements a pinch of extra functionality as it’s capable of storing and calling closures on request, something that closely mimics the forces behind Pimple.

With this naïve DIC in place, the whole file storage object graph could be assembled on demand:

<?php
$container = new Container();
$container->set("filestorage", function() {
    return new FileStorage(new Serializer());
});
$fileStorage = $container->get("filestorage");
$fileStorage->write("This is a sample string.");
echo $fileStorage->read();

It’s clear that the DIC is (or in theory it should be) an element stepping beyond the boundaries of the client classes, which are completely agnostic about its rather furtive and silent existence. This form of unawareness is quite possibly one of the biggest differences that exist between a DIC and a service locator, even though it’s possible to inject a DIC into other classes as well through a sort of “recursive” injection.

In my opinion, this process not only unnecessarily degrades the DIC to the level of a plain service locator, but corrupts its natural “outsider” condition. As a rule of thumb, regardless of if you’re using a DIC or a service locator, make sure the elements will be playing the role they’re supposed to play without stepping on each other toes.

Closing Remarks

It seems that the old bad days when managing class dependencies was just a matter of dumping a few “new” operators into fat, bloated constructors are finally fading away. By contrast, an emerging combination of patterns, with Dependency Injection leading the charge, are strongly pushing through to every corner of PHP, a paradigm shift that has already had a beneficial impact on the quality of several existing codebases out there.

Still, the big question keeps floating around in circles: DICs, service locators, injected factories… which ultimately fits the bill the best? As I said before, making the right decision largely depends on what you’re dealing with in the first place. In all cases, they’re just variants of Inversion of Control, decorated with some nice refinements and fancy mixtures. And you know that IoC is the way to go with polymorphism, hence with testability. Let your personal needs be your most confident advisors; they won’t disappoint you.

Image via SvetlanaR / Shutterstock

More:
Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account