[PSR-11] An in-depth view at the Delegate lookup feature

PSR-11 is currently in "review" phase. In this article, I'm taking an in-depth look about one often overlooked feature: the Delegate Dependency Lookup.
[PSR-11] An in-depth view at the Delegate lookup feature
Article écrit par David Négrier

What is it?

The delegate dependency lookup feature (let’s call it DDL) is a design pattern.

It is the only way we have found to compose containers.

What problem does it solve?

Let’s admit you want to compose/chain containers (more on why you might want to do this later).To do this, you would typically use a « CompositeContainer » (a container without any entry whose role is to ask each « child » container in turn « do you contain the entry I’m looking for? »)The CompositeContainer is not enough. Let’s admit container 1 contains your controller. You fetch your controller. Your controller has a dependency on your ORM’s entity manager. If the entity manager is part of container 2, container 1 will be unable to fetch it (because internally, it will perform a call « $this->get() » to fetch the entityManager dependency. Similarly, the entityManager should be able to fetch a dependency (for instance the dbConnection) inside another container…

The delegate lookup feature simply states that containers should not fetch their dependencies locally, but instead, they should fetch their dependencies from the top-most container (in this example, this is the CompositeContainer).

In our example, this would go like this:

– The framework asks for the « controller » entry to the CompositeContainer

– The CompositeContainer asks Container 1: « do you have « Controller ». Container 1 answers yes

– The CompositeContainer asks Container 1: « give me « Controller ».

– Container 1 needs to fetch the « EntityManager » dependency, so Container 1 calls $compositeContainer->get(‘EntityManager’); (here! container 1 is « delegating » the dependency lookup to the composite container)

– The CompositeContainer asks Container 1: « do you have « EntityManager »? => response is no.

– The CompositeContainer asks Container 2: « do you have « EntityManager »? => response is yes. The CompositeContainer asks Container 2: « give me « EntityManager ».

– … and do on

Do we want this?

To be honest, PSR-11 is already useful without the DDL feature. The ContainerInterface is enough to allow users to swap container implementations.

Being able to compose containers is a nice feature, but it’s optional. PSR-11 can be useful without DDL.

What are valid use cases for this?

First of all, I do not expect major full-stack frameworks to use this. It is obvious from the example above that there is a performance hit when using a composite container (you have to ask each container in turn whether it contains the instance you are looking for or not).

Frameworks very focused on performance (like Symfony full-stack) should stay away from CompositeContainers (more on performance below).

Yet, here are a few use cases where composite containers are valuable:

1- Migration!

Let’s admit you started a small app with Slim3 and Pimple. Your app is getting bigger. You now have more than 200 services declared in Pimple and you want to migrate away to something more powerful (maybe you want to benefit from Autowiring or maybe you need lazy services for performance issues…)

The DDL feature allows you to put Pimple side-by-side with your new container of choice and migrate entries slowly, one by one. This is really cool because it makes a daunting task a lot easier.

2- I don’t care about performance, give me features!

Running containers side-by-side is a great away to enhance a container with the features of another container. For instance, you could enhance your existing container with a sidekick containers dedicated to creating aliases or serving a « lazy » version of your services, etc…

Not everybody cares for container performance. For instance, if you are doing async PHP (with ReactPHP or another lib), container performance is not a concern at all (since services are created once at the beginning of the script and are reused accross requests).

By the way, what is the real performance impact of using a CompositeContainer and DDL?

I’ve been doing some tests using a modified version of Symfony.

You can read my old article about the performance impact on DDL here. Spoiler alert: impact is quite low, I was not able to measure it.

How do we implement this?

DDL support is quite easy to add in any container out there. From my experience with container-interop, I’ve never seen a case where it took more than a few lines of code to add support.

Typical implementation goes like this:

1- You modify the constructor to accept an additional parameter: the root container. This parameter is optional. If not passed, the root container is the container itself.

So your container constructor now looks like this:

public function __construct($param1, $param2, ..., ContainerInterface $rootContainer = null) {

$this->rootContainer = $rootContainer ?: $this;

}

In your container code, when you perform a call to get on a dependency, instead of calling $this->get, you call $this->rootContainer->get.

Aaaaaand you’re done 🙂

Important: As you can see, if you are not using a composite container, the impact on performance of a container is null (it is almost the same code executed). So a container can add support for DDL without impacting its average performance.

Is it used somewhere?

The ContainerInterface has been mostly designed by looking at what common methods were supported by containers out there.

On the contrary, the dependency delegate lookup design pattern has been « invented » by container-interop and does not stem from previous work from one container or another. This is of course to be expected, because without the ContainerInterface, it is not possible to envision composing containers.

The delegate dependency lookup feature is already supported by a number of containers out there (see: https://github.com/container-interop/container-interop#projects-implementing-the-delegate-lookup-feature )

I don’t know if it is wildly used or not, but I know at least one place where this was useful to me: the laravel <=> container-interop/service-provider bridge. I used it here to add support for container-interop’s service providers into Laravel. Rather than forking Laravel container to add support for service providers in it, I decided to add another container (Simplex) that already had support for service providers next to Laravel’s one.

Summary:

Is it essential to PSR-11? No

Is it useful? Yes, in some specific cases (I do not expect it to be wildly used)

Is it easy to implement? Dead easy

Does it have an impact on performance? No if only one container is used (business as usual), yes if many containers are used (but this is to be expected)

Should it be part of the PSR?

My personal opinion is yes. It does not hurt, it is optional, it is the only way we could put containers side-by-side and let them share entries. I believe advertising this design pattern in the PSR will make it more wildly adopted. This, in turn, will increase framework interoperability.

image description