Container interoperability PSR: possible use cases

In the past few days, the entrance vote for PSR-11 (the container interface) has been all the rage on the PHP-FIG mailing list (and on Twitter).

We have had a lot of great feedback from very interested people, and generally, the response to this proposal has been very positive.

There has also been a lot of negative reactions from a part of the community that I want to start addressing in this blog post. Negative feedback was generally not about the content of the PSR, but rather about whether we need this PSR at all, and whether there is a good use case for it. Top notch developers like Fabien Potencier, Antony Ferrara, Bernhard Schussek or Larry Garfield have expressed their dismay at this proposal, and often with very valid comments. Guys, this blog post is for you. I do not expect you to fully agree with me, but at least, I want you to see where we are headed.

In this blog post, I’ll focus on use cases.

We have come to great length giving a bunch of use cases where this PSR might be helpful. These use cases are a bit scattered all over the PHP-FIG mailing list, so I'll try to summarize all those use-cases in this blog post.

As you read, you may not agree with one use case or another use case. You might even be completely right, and maybe some of those use cases are simply bad ideas (tm). In short, I do not expect you, reader, to agree with me on all this.

However, please note that I only need one of those use-cases to be valid for PSR-11 to be worthwhile!

Let's get started!

Use case 1: container agnostic libraries

Some libraries rely on a container to run:

  • some routers need to instantiate the controller they are using
  • a library like "silly" can rely on the container to inject parameters into a closure (dependency injection the way Angular does it)
  • some libraries are complex enough to need the ability to configure a container ( Jackalope or Doctrine come to mind).

Most of the negative feedback about this come from the fact that this means injecting the container into the library, which is "bad".

Now, let me say I completely agree that injecting the container in user-land is a very bad practice (this means advocating the service locator design pattern). But this is not user-land we are injecting the container into! This is a third-party library! As stated by Larry, if 99% of the libraries don’t need a container, there could be very valid use-cases where a container is needed. For instance here, in Symfony router, or here, in Silex, or here, in PHP-DI/Invoker.

Related Twitter thread here

Use case 2: universal module system

The idea here is to develop framework agnostic meta-packages (i.e. Bundles/Modules/Extensions)

Most meta-packages are providing instances or routes. Thanks to PSR-7, a package can provide routes in a framework agnostic way (using a Middleware). Thanks to PSR-11, we could do the same for instances.

Rather than providing a configuration file (services.yml, module.config.php…), you could instead provide a container loaded with entries.

The container can be automatically added to a composite container containing all the containers used in the application.

Bernhard Shussek pointed out that this could have adverse effects on performance. Larry Garfield also pointed out the problem of performance. It is true that several containers cannot perform better than a single container, but there are optimizations techniques that could be applied to improve performance. For instance, a composite container (usually the « root » container) could build a map of all entries with their associated container (instead of calling the has method on each container). This would be kind of similar to what happens when you build a classmap in Composer with the « -o » parameter.

Possible alternative

Jan Jakeš and Larry Garfield made a very valid point: for this use case, we should instead focus on building a compiler extension that adds entries during the compilation of the container. First of all, thank you guys for proposing this alternative. I must admit I am pretty fond of this solution. Now, the truth is that most containers do not have a compilation pass mechanism. I would be interested in feedback about this, but so far, I think I saw support for compilation in Zend DI and Symfony only.

Also, this means that we should standardize the API for defining entries in containers. This could be much more difficult than the current PSR-11 proposal. This has a number of advantages too (performance-wise). Those 2 proposals (container-interop and compiler extension standardization) only overlap on this particular use case, so I would tend to view those proposals as complementary rather than opposite.

I honestly don’t think I have enough expertise to design a PSR for container compilation extensions, but I would gladly like to see one.

I'm also planning to write a longer answer on this particular issue sometime later, because I want to speak here of all use cases, and not focus too much on this particular one. Still, it needs more attention, so expect a longer answer from me some time later.

Use case 3: extending existing containers with additional features

By having a generic interface to read entries from containers, you can start developing generic « wrappers » or « middlewares ».

This can be tremendously useful to extend an existing container with new features.

Here are a few samples:

Just imagine being able to do something like this:

Adding lazy-loading capabilities to Pimple

$pimple = new Pimple();
$pimple[‘entry’] = $pimple->share(function() { //… });

$myContainer = LazyLoadingWrapper($pimple);

// By adding "lazy." in front of an entry, it gets wrapped into a proxy object.
$lazyLoadedObject = $myContainer->get(‘lazy.entry’);
// Now, the $lazyLoadedObject will only be fetched in Pimple when it is used for the first time.

I’m not completely sure but I don’t think I read criticisms of this use case so far.

Use case 4: side-by-side frameworks

This is a copy from this thread

Picture yourself in 2017: you have this great website developed using Drupal 8. It is a mortgage comparator. In this website, you have a special page that allows visitors to find the cheapest mortgage. Therefore, since this is Drupal 8, you probably have in your code a "MortgageService" and a services.yml file with a "mortgageService" instance configured in it.

Now, suddenly, your boss wants to expose this service as an API. Of course, you could do it using Drupal/Symfony tools, but you have seen this super-cool tool named Apigility with its sleek UI that streamlines most of the work to build an RPC API.

How great would that be to use Apigility. Hopefully, both Drupal 8 and Apigility 2 are PSR-7 compatible (remember this is 2017 :) ). So using a good deal of middlewares (maybe zendframework/stratigility ?), you can have Apigility and Drupal 8 working side-by-side.

At this point, you have installed Apigility. In Apigility, you create a "RPC resource" (that is an instance of a class), and you want to inject into this resource your "mortgageService".... Ho but wait! "mortgage" service is defined in Drupal 8 which relies on Symfony container, and Apigility is using Zend Container! Damn!

Without container interoperability, that's fairly simple: you are screwed! However, with container interoperability, all you have to do is edit Zend DI container (the module.config.php file) and add a reference to "mortgageService" to the constructor of the RPC resource class.

Most of the criticism here stems from the fact that one should never have 2 frameworks living side-by-side.

See here and here

To this, I would like to point out that my use case is perfectly valid, that I might want to use both Apigility and Drupal 8 that are great tools, and that no, I don't want to spend 1 week rewiring Apigility into Drupal 8 using Drupal 8 container. This would simply be plainly stupid since I'm pretty sure I would break a bunch of things when I upgrade. I live in the real world. I find tools, I want them to work together. I don't need to be a framework specialist to do so.

Use case 5: leaving some room for innovation in container space

This reason is quite personal, and maybe the reason why I got involved with container interop in the first place.

There is a huge place for innovation in DI containers. Containers answer the same problem (bootstraping an application) in very different ways (autowiring, configuration files, annotations, closures...) I believe there are other very valid techniques out there that have not been found yet.

I am the lead developer of Mouf. Mouf is a dependency injection framework. It lacks a ton of features: it has no autowiring, no lazy-loading, no aliasing, no annotation support... But it has one feature that no other framework has: a graphical user interface with instances drag'n'drop support. I do truly believe that this is quite innovative, and I'm fairly proud of it. Trust me, it rocks. A typical container in any PHP application will contain about 30 to 40 services, because it is quite inconvenient to put more objects into it. Using Mouf, I’m not only putting services and controllers, I’m also putting views, widgets, forms, any fields making a form, etc… into the container. I have applications with containers containing happily 1000+ instances. Believe me, there IS room for innovation.

So... I've been developing this great tool, I want to share it with the rest of the world. What are my options?

If I go out there and say "Hey! Come and see this brand new DI tool!", how many people will come and use it? The answer is easy... noone! Because in order to build an application, you have to wire a router, a db connection, an ORM, a templating system, .... How many developers out there are qualified enough to do such a thing? Probably only a tiny fraction of the PHP developer base. And how many will be willing to rewire everything to test this new DI tool? Probably none.

You might have built a fairly innovative DI container, the chances that your ideas get adopted are null unless you manage to lower the barrier of adoption. How do you do that? By plugging your container to an existing full-stack framework that people know (Laravel, Symfony, ZF, Yii, etc...). The fact is this PSR makes it possible.

To sum it up:

=> PSR-11 = More innovation

Once more, having 2 containers side-by-side is not an ideal situation, especially performance-wise. Yet, without this possibility, I really feel we are hindering innovation. It is almost impossible to innovate in this field if you are not a part of a big framework... and big frameworks tend to be quite conservative when it comes to their core. Opening the game (thanks to the delegate lookup of dependencies) will lead to innovation.

Final word: what this PSR is not about

This PSR is NOT about advocating using the service locator pattern. As you probably noticed in the 5 use cases I have been presenting, none of them is about having a standard service locator to be used in user-land.

Tags :