Modern PHP development favors the use of inversion of control to keep software more configurable and flexible. This leads to the problem that one now has to create a big graph of objects to use the application. E.g. a Mailer object now needs an Transport object. The Transport object needs some other object.

At first you might just write new Mailer(new Transport($somethingOther)), but later you need the Mailer at several places and now you have to replicate the setup code. As a solution to avoid redundant setup code, service containers like the symfony2 dependency injection component are used.

The goal of a service container is to centralize the construction of big object graphs.

The Symfony2 Service Container

The symfony2 service container is probably the most used service container in the PHP world. Most of the other service containers in the PHP world are influenced by it and thus work similar to the symfony2 service container.

Now let’s look at an example. We want to use the symfony2 service container to setup a Newsletter object, which needs a Mailer object, which depends on a Transport object.

Just for easier understanding, here’s how the class interfaces look like:

<?php
class Newsletter {
    public function __construct(Mailer $mailer) {}
}
class Mailer {
    public function __construct(Transport $transport) {}
}
class Transport {
    public function __construct($host, $user, $password) {}
}

To set up a Newsletter object, we need to configure the service container first. We can use a yaml file to do that:

parameters:
    transport.host = "example.com"
    transport.user = "root"
    transport.password = "somepassword"

services:
    newsletter:
        class: "Newsletter"
        arguments: ["@mailer"]
    mailer:
        class: "Mailer"
        arguments: ["@transport"]
    transport:
        class: "Transport"
        arguments: ["%transport.host%", "%transport.user%", "%transport.password%"]

Now we just need to create a new service container:

<?php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('services.yml');

$newsletter = $container->get('newsletter');

Looks simple, right?

Actually it’s not. Commonly used service containers are complex solution for simple problems.

The Configuration

The configuration file basically is its own programming language. It has its own grammar and follows its own rules. When you as a programmer have not learned about the symfony2 service container configuration yet, next to understanding PHP, you now also have to understand this adhoc configuration format.

While the configuration format looks simple at first, once you have a more complex object graph, you need more complex features. Let’s say our Mailer now takes the Transport via a setter (setTransport) instead. You obvious know how to do it in PHP, just $mailer->setTransport($transport). But now you have to translate this to the configuration format.

Each time you have something different from the simple object graph case, you have to look it up in the documentation, even when you know that it can be done trivial with some PHP code.

Turns out that you cannot describe all services so well with this configuration format. The obvious solution: Let’s just invent a new programming language for expressions. No, I’m not kidding. Check it out here. It comes with its own grammar, parser, compiler and evaluator. It’s not like one could have used PHP for this.

So, when the configuration format is not flexible enough for your objects, just learn the new programming language. Then just translate how you would setup this in PHP to the new programming language. Also forget about IDE support for this custom programming language.

Static Analysis

Once your project gets bigger, you benefit from static analysis and smart IDE’s. E.g. with PhpStorm you can click on class names and it opens the class file, or you can use the automatic refactoring tools. This also includes stuff you might even take for granted now, like autocompletion or showing you type errors while editing. Static analysis allow for these features, and without them you would propably be less productive in large code bases.

By using a configuration file to define your object graphs, you loose all of this. You don’t get autocompletion for the class names. You cannot safely rename a class anymore. You cannot safely add a new parameter to the class. You will not see type errors while editing, e.g. you’ve configured the service container to pass a Transport to the Newsletter instead of the expected Mailer.

You also don’t get further autcompletion for expressions like $container->get('newsletter'), because the IDE has no idea what the return value might be. So, you now have to add PHP annotations everywhere to make sure you can still refactor thing automatically.

While there are plugins to add support for some of these features to IDEs, it’s always not second class support. It just does not work as well as editing simple PHP code.

All of this decreases your productivity, and many edit-time errors turn into runtime errors.

Other Service Containers

While most of the other service containers have not yet invented their own programming language, they still share many of the mentioned issues. E.g. many of them have the same issues with static analysis.

A Simple Approach To Service Containers

Let’s get back to the initial problem. We want to avoid redundant setup code of our large object graphs. This is a actually a pretty simple problem.

Let’s use our Newsletter example from above and model the PHP code required to use an Newsletter:

<?php
$transportHost = 'example.com';
$transportUser = 'root';
$transportPassword = 'somepassword';

$transport = new Transport($transportHost, $transportUser, $transportPassword);
$mailer = new Mailer($transport);
$newsletter = new Newsletter($mailer);

When we need to use a Newsletter at multiple points, we have to copy this code. This is why we have used a service container to begin with.

Turns out that there is another way to avoid redundancy without using a service container. Usually when you have redundant code, you move it into a function. We can apply the same strategy here:

<?php
function createNewsletter()
{
    $transportHost = 'example.com';
    $transportUser = 'root';
    $transportPassword = 'somepassword';

    $transport = new Transport($transportHost, $transportUser, $transportPassword);
    $mailer = new Mailer($transport);
    return new Newsletter($mailer);
}

Now let’s extract the Transport and Mailer logic too:

<?php
function createTransport()
{
    $transportHost = 'example.com';
    $transportUser = 'root';
    $transportPassword = 'somepassword';

    return new Transport($transportHost, $transportUser, $transportPassword);
}

function createMailer()
{
    return new Mailer(createTransport());
}

function createNewsletter()
{
    return new Newsletter(createMailer());
}

Because function autoloading is not working that well with PHP, let’s wrap this in a class:

<?php
class ServiceFactory
{
    public function transport()
    {
        $transportHost = 'example.com';
        $transportUser = 'root';
        $transportPassword = 'somepassword';

        return new Transport($transportHost, $transportUser, $transportPassword);
    }

    public function mailer()
    {
        return new Mailer($this->transport());
    }

    public function newsletter()
    {
        return new Newsletter($this->mailer());
    }
}

We can use it like this:

<?php
$serviceFactory = new ServiceFactory();
$newsletter = $serviceFactory->newsletter();

Now we have a solution which solves the same problem service containers solve: centralizing object graph setup code, while retaining simplicity.

But as the name ServiceFactory implies, the thing is not a real container. It is not pooling the objects. Let’s solve that by wrapping the ServiceFactory inside an object pool. And let’s call this object pool a service container (because it contains our services):

<?php
class ServiceContainer
{
    private $serviceFactory;
    private $pool;

    public function __construct(ServiceFactory $serviceFactory)
    {
        $this->serviceFactory = $serviceFactory;
        $this->pool = [];
    }

    public function __call($method, $arguments)
    {
        if (!isset($this->pool[$method])) {
            // E.g. for `$method = 'newsletter'`:
            // `$this->pool['newsletter'] = $this->serviceFactory->newsletter();`
            $this->pool[$method] = $this->serviceFactory->$method();
        }

        return $this->pool[$method];
    }
}

Now we just need to wrap our factory with the ServiceContainer and then multiple uses of the same service return the same instance:

<?php
$serviceFactory = new ServiceFactory();
$serviceContainer = new ServiceContainer($serviceFactory);
$newsletter = $serviceContainer->newsletter();

Because the interface of the ServiceContainer is equals to the interface of the ServiceFactory (they both respond to the same public API), we can tell our IDE that we have a ServiceFactory while we actually have just a ServiceContainer. This way we will gain autocompletion, even when working with the ServiceContainer object pool.

<?php
/**
 * @return ServiceFactory
 */
function createContainer() {
    $serviceFactory = new ServiceFactory();
    return new ServiceContainer($serviceFactory);
}

$serviceContainer = createContainer();
$newsletter = $serviceContainer->newsletter();

There is one thing left to do: Inside the ServiceFactory, calls like $this->transport() need to be replaced with something like $this->getContainer()->transport(), to avoid recreating the transport service on multiple calls. As this is pretty simple (just add a setContainer and getContainer on the ServiceFactory, and call $serviceFactory->setContainer($this) from the constructor of the ServiceContainer), I will leave out the code for this.

That, while looking different than commonly used service container designes, solves the problem of creating an object graph pretty well. In essence a service container is nothing more than a factory with an object pool.

Advantages

Compared to configuration files, you don’t have to learn a new set of rules for this. Our construct is just a plain old factory with a proxy object which pools repeated service requests. If you know PHP, you know how work with it. This also means that you will never have issues à la “I know how to do it in PHP, but I have no idea how to configure the service container to do it”.

We also don’t have to invent our own programming language for more flexibility. As our factory is just plain old PHP, we have all the flexibility we need.

The biggest advantage is that we retain static analysis features. The factory supports autocompletion, automatic refactorings, “find usages of this method”, “go to class”, etc. Objects we get from the factory are automatically typed, without any kind of PHP annotations. You even see type errors while editing.

Takeaways

While creating object graphs is a simple problem, commonly used service containers are overengineered and far to complex. Keep this accidental complexity in mind while doing design decisions on using service containers or not.

Thanks for reading :) Follow me on twitter if you’re interested in more of this stuff!