Container-interop: where we are, where we go

If you have not been following the container-interop project, let me start by a quick introduction.

Edit (2014-08-10): added Solution 4 to the document

The Goal

The goal of container-interop is to achieve the interoperability of container objects (service locators, dependency injection containers, etc.). This project is bringing together many talented PHP developers in order to define a set of common interfaces, that might be later adopted as PSRs (we hope!)

To achieve this, we first tried to define a common interface used to retrieve objects from a container. This is the ContainerInterface that just reached v1.0.0.

This is really a good thing, because it will allow the components that rely on the DI container (usually the router that is in charge of instantiating the controllers) to be container-agnostic. This means that in the future, you could use Silex (a micro MVC framework) without having to use Pimple for instance. You could use it with PHP-DI instead. Or you could use Symfony 2 and rely on Mouf for the dependency injection, etc...

But this is a long term goal, and the frameworks will have to adapt to the ContainerInterface before. It might take a lot of time to get there... There is already Acclimate that solves a part of the problem. It wraps existing DI containers into an adaptor that is implements the ContainerInterface. So a vast majority of DI containers out there are already compatible with the ContainerInterface. But this is only one side of the problem. We must also make sure we have routers that know how to use the ContainerInterface.

Where are we now?

Now, there are 2 big players out there: Symfony 2 and Zend 2, and a myriad of other frameworks. And each framework comes with its own container tightly coupled with the router and other components. For us, container developers, it is not possible to take an existing framework and take the default container and replace it with our container. Future frameworks might not behave the same, but we need to fix the problem now.

In the rest of this article, I will assume I am a PHP developer and I want to work with Symfony 2, but that I want to use another DI container (Mouf, PHP-DI, whatever...). Since I cannot remove Symfony DI container from Symfony, we will have to make sure the new container we add can collaborate with Symfony. What does it mean?

  • From Symfony DI container, I should be able to reference any entry declared in my container
  • From my container, I should also be able to reference entries from Symfony (because all the bundles I install will add new entries in Symfony container and I want to be able to use those)

There is a very slight difference in behaviour between both containers we might want to introduce. Have a look at this sample:

// This should return an instance from my container
$symfonyContainer->get('an_entry_in_my_container')

// This should return true
$symfonyContainer->has('an_entry_in_my_container')

We might expect get calls to return instances from our container. Indeed, the Symfony router can only use the Symfony container to get controllers out of it. I want to be able to define controllers inside my container, not inside Symfony's one. Therefore, when I query Symfony's container, my container must be called (chained) if Symfony's container does not contain the controller.

However, the opposite might not be true:

// This should throw an exception
$myContainer->get('an_entry_from_symfony_container')

// This should return false
$myContainer->has('an_entry_from_symfony_container')

If I query directly an entry from Symfony's container into my container, I should get an exception. This is compulsory, otherwise, I have no idea where an instance is, and in the case of Composite containers, this will lead to infinite loops.

So let's sum it up:

  • Any call to Symfony's container should be forwarded to my container as a fallback mechanism
  • Any outer call to my container (calls made by the user) should be strictly scoped inside my container
  • In my container, inner calls (calls that are triggered by instances referencing other instances) should be forwarded to Symfony's container.
  • Therefore, my container and Symfony's container should behave differently

Interoperability between my container and SF container

A question of priority

At this point, it might be interesting to wonder what happens if there is a naming conflict. Should I first look into Symfony's container or should I look into my container?

Because we have 2 containers, we can have entry conflicts. So what should happen if both containers declare the same entry? (for instance if both containers declare an 'entityManager' instance?)

There are many ways to solve this problem.

Solution 1: the DI fallback solution (spoiler alert: this solution is bad)

TL;DR? Jump to the next session

  • The "main" container (Symfony in our example) should always have the priority.
  • The "fallback" container (my container) is called only if the main container does not have an instance. Always.

In pseudo-code, the get method would look like this:

In SF2 container (modified version to support "fallback" containers)

function get($entry) {
    if we have a local instance {
        return local instance;
    } else {
        if fallbackContainer->has($entry) {
            return fallbackContiner->get($entry);
        }
    }

    throw new \NotFoundException();
}

function has($entry) {
    if we have a local instance {
        return true;
    } else {
        if fallbackContainer->has($entry) {
            return true;
        }
    }

    return false;
}

In my container (pseudo-code)

function get($entry) {
    if (mainContainer->has($entry)) {
        return mainContainer->get($entry);
    } else {
        return local instance;
    }
}

This solution is actually pretty bad. We have an heavy problem regarding infinite loops. Let's imagine you request an entry 'foo' from my container that exists in my container. The first thing the get method of $myContainer will do is to call the has method of SF2. Even if 'foo' is not available in SF2's container, the has method will return true because it will query our container. Therefore, we are starting by calling SF2's get method, that will in turn call my get method. At this point, we've been through a loop and I'm screwed. So there is a deep flaw in this solution.

Furthermore, is this what we really want? For instance, if I want to replace Symfony's container with my own container, I might want to be able to override some entries of Symfony's container completely. And this "solution 1" does not solve that problem.

Solution 2: the prepended container solution (spoiler alert: this solution is bad, too)

TL;DR? Jump to the next session

Another solution would be to do the opposite. In this scenario,

  • The "main" container (Symfony in our example) starts by forwarding the call to our container. Always.
  • The "fallback" container (my container) forwards calls to Symfony only if it does not have the instance.

In SF2 container (modified version to support "prepended" containers)

function get($entry) {
    if fallbackContainer->has($entry) {
        return fallbackContainer->get($entry);
    } else if we have a local instance {
        return local instance;
    } else {
        throw new \NotFoundException();
    }
}

In my container (pseudo-code)

function get($entry) {
    if we have a local instance {
        return local instance;
    } elseif (mainContainer->has($entry)) {
        return mainContainer->get($entry);
    } else {
        throw new \NotFoundException();
    }
}

You are probably spotting a possible infinite loop. If we query an entry that does not exist, it will be forwarded to our container, then back to SF2 DI container, then back to our container, and so on...

Solution 3: using a composite container (hint: this is cool)

A composite container is a kind of container that aggregates several containers into one. When the get method of this container is called, it will forward the query to all the containers it contains, until one of this container returns a valid entry. Composite containers are cool, because they allow us to define a clear precedence order between all containers.

The perfect world scenario

In this scenario, we will assume that we have a MVC framework that is clever enough to ask instances of the controller directly to the composite container (so we have a MVC framework that does not rely on a specific container... this does not exist yet but might exist in the future!).

Composite container perfect scenario

  1. The router is querying the composite container for the "myController" instance.
  2. The composite container is calling all the container it contains in turn. It calls the has method of each container.
  3. "MyContainer" has the "myController" instance. So the composite container will call the $myContainer->get("myController"). The "myController" instance is linked to an "entityManager" instance. Instead of looking for this instance in the "MyContainer" container, the container will be clever enough to ask directly to the "compositeContainer".
  4. The composite container is asking for the "entityManager" to all instances in turn. The Another container is having this instance and is returning it.
  5. We are done.

The real world scenario (!dirty hacks included!)

In the real world, things are a bit more complex. We are using Symfony 2. This means we are certainly using the Symfony 2 router. And when Symfony 2 is configured to use a controller as a service, the router retrieves the controller from the Symfony 2 DI Container. So the scenario looks like this:

Composite container real life scenario

  1. The router is querying the Symfony 2 container for the "myController" instance: $sf2->get('myController').
  2. At this point, the Symfony 2 container should normally throw an exception because it does not contain the controller. But if we want to achieve our goal, we must ensure that the controller will forward the call to the composite container.
  3. Let's assume the call is forwarded. The composite container queries each container to ask for the controller. The controller is returned by "My container".
  4. "My container" needs to instantiate the "entityManager" that is needed by the controller. It forwards the call to the composite container.
  5. The composite container asks each container for the "entityManager". In particular, if will ask Symfony 2: $sf2->has('entityManager'). Remember step 1? At this step we said that unfulfilled calls had to be forwarded to the composite container. But right now, this is the last thing we want to do. Because if we forward the call to the composite container, we will enter in an infinite loop. What we need is the call $sf2->has('entityManager') to return false.

So basically, the SF2 container must have 2 different behaviours:

  • When it is called by the router, it must forward the calls to the composite container.
  • When it is called by the composite container, it must "act normal".

We could modify the signature of the get method to add an additional parameter specifying the behaviour to adopt, but that would be complex to add for a somewhat specific use case. What we can do on the other end is to use a counter to avoid the infinite loop. Here is a solution. It is not perfect, but it is working:

  • The first time the get method of the SF2 container is called, the call is forwarded to the composite container, no matter what, and a counter is incremented.
  • We might enter the get method of the SF2 container several times again (in order to construct all the dependencies). Each time, the counter is incremented. If the counter is different from 0, we do not forward missing instances calls to the composite container (we 'act normal').
  • Each time we leave the get method, we decrement the counter. So when the final instance is constructed, the counter is back to 0 and we can start all over again.

In the rest of this document, I will call this behaviour the "master" mode as opposed to the "standard" mode presented in the previous chapter.

Testing this proposal in real-life

In order to play with all these concepts, I have developed a modified version of Pimple that implements both ContainerInterface a ParentAwareContainerInterface (that exposes a setParentContainer method that can be used to declare that the container should bind to a composite container. I called the package "Pimple-interop". You can check it out here: https://github.com/moufmouf/pimple-interop

There is an initialization flag that can be configured to put PimpleInterop in "standard" mode, or in "master" mode.

I also worked on a modified version of the Symfony 2 DI container that implements the ParentAwareContainerInterface (that exposes a setParentContainer method that can be downloaded here: https://github.com/thecodingmachine/interop.symfony.di.

Just like "Pimple-interop", it features a setMode method that can be set to "standard" or "master" mode. The "counter" trick is used, you can see its implementation here

Solution 4: a simpler solution (hint: this is cool AND simple)

I came to this solution some time ago and Matthieu Napoli kindly reminded me it answers a number of problems. Solution 3 is pretty cool, but if we want to implement it with today's frameworks, we need some hack to have it working. Solution 4 is a simpler solution that solves the same problem.

In this solution, we change the Symfony DI container to make it a composite container.

Composite container simpler scenario

The SF2 container does not implement the ParentAwareContainerInterface. Instead, it implements a set of methods that can prepend or append containers to the SF2 container. In this scenario, the SF2 container is not a container like the others, it clearly acts as a master container and it delegates calls to the prepended or appended containers. The prepended and appended containers do implement the ParentAwareContainerInterface.

Testing solution 4 in real-life

I have tested solution 4 in an early version of interop.symfony.di (v0.1.0).

Solution 4 is much easier to implement than solution 3 and is certainly the way to go while we wait for major routers to have native support for the ContainerInterface.

However, there is a limitation. So far, we assumed we were using only Symfony 2. We could as well be using Zend framework 2. With scenario 4, both ZF2 and SF2 containers would act as composite containers. This means neither SF2 nor ZF2 containers implement the ParentAwareContainerInterface, and therefore, we have no way to have ZF2 and SF2 working together (that would be fun!). If we want to write an application that relies on both ZF2 and SF2 containers, we need to go back to the more hacky solution 2.

Naming convention

So far, I have developed an interface that can be implemented by DI containers to let the world know that they have the ability to call a parent/root container when referencing an instance.

I have developed a ParentAwareContainerInterface that you can find here: https://github.com/container-interop/container-interop/pull/8

The naming is certainly not OK and if you have any better idea, please post your idea on that thread: https://github.com/container-interop/container-interop/pull/8

At least, using this long post, I hope you get the idea of what we are trying to achieve.

Here is the short list of possible choices:

  • ParentAwareContainerInterface
  • WrappableContainerInterface
  • ChainableContainerInterface
  • AggregatableContainerInterface

Please help us find more :)

There is also the question of the name of the method provided by the container. Here are some ideas:

  • setParentContainer
  • setRootContainer
  • setCompositeContainer

The need for an interface

In the previous chapter, we spoke about searching a name for this interface, but at this point, I have come to ask myself this question: is there a need for an interface?

The real question is "who would use it?".

If the DI containers are configured by hand, in the "index.php" file of your application, the developer can simply look into the documentation of each container, and call the setParentContainer method (or whatever its name...) on each container he uses.

On the other hand, we might write a version of the CompositeContainer that automatically calls the setParentContainer method on any container implementing the interface (thus saving the need to let the developer perform the call). I have a feeling this could be very interesting and yet, a feeling this could lead to trouble down the road (especially in a scenario where a CompositeContainer is itself composed into another CompositeContainer...)

As you can see, I haven't pushed the thinking far enough yet. Still, I wanted to share all those findings with you, to gather feedback from the community at large before diving too deep into the subject. Any idea about this article? Any points I have missed? Does it make sense? Should we add the interface? What should be its name?

Please provide feedback into this thread: https://github.com/container-interop/container-interop/pull/8

... and if you have made it this far, thanks for reading! ;)