Standardizing the way we put things in a container VS standardizing the way we read things from a container

While discussing the entrance vote for PSR-11 (container interoperability), a number of comments have been made. I would like here to focus on one comment, fairly well expressed by Larry Garfield here (thanks Larry), and supported by Jan and Bernhard.

So the whole point is that PSR-11 should be written the other way around and we should standardize the way we put things into a container rather than standardize the way we pull things out of a container.

The soundest way to do this is certainly to standardize a CompilerPass mechanism, as can be found in Symfony, although there could be other options like standardizing a configuration file format. Larry makes a pretty good work at explaining why it is a better idea, and it comes down to this: performance and optimization possibilities.

The fact is that when we first started thinking about container-interop (2 years ago), we thought about this and we went down this path (see https://github.com/container-interop/container-interop/issues/1) When I say "we", I mean all the great people who contributed to container-interop. I may be the only one advocating a container-interop PSR here, but be sure I am not a lone wolf :)

So let's go down the path of standardizing how we put things into a container, and let's see where this leads.

Configuration VS runtime containers

You can divide containers into 2 categories:

  • The ones that rely on configuration files (Symfony 2, ZF2, Nette, Mouf). Usually, you cannot put anything in these containers at runtime.
  • The ones that are filled at runtime by PHP code... usually using closures or autowiring (Pimple, Aura, Laravel...)

Container types

The line is not completely clear with some containers crossing boundaries (like PHP-DI).

Runtime containers cannot be compiled. It makes no sense for these.

Standardizing a configuration file or a compilation pass

On the other hand, containers based on configuration files can be compiled. Not all of those have the capability though. SF2, ZF2 and Nette have support for compilation. Mouf has not (yet) support for it. If we were to standardize how a compilation pass is performed, then that would force all configuration-based frameworks into implementing compilation. I'm not saying it is bad (compilation is great for performance), yet, we would dictate a feature.

Let's continue down that path.

So now, we need to write an interface that describes an object stored in a container (what Symfony calls a "Definition"). For the sake of this post, let's call this interface the DefinitionInterface. Obviously, we need to be able to put a class name in that description, and also constructor parameters. Then, we need to be able to set any public property (at this point, you can expect a very long thread on the PHP-FIG mailing list with lots of people explaining that you should not standardize setting public properties because that's bad (tm) ) Then, we need to be able to set any setter. Have a look at the frameworks around. Mouf does only support 'setters' (functions starting by setXXX). If I'm correct, Nette only support methods starting with "injectXXX". ZF2 and Symfony support any method call, with any number of parameters. This is clearly the best option, because it is the most flexible. So we take this solution. Which means that both Mouf and Nette will have to adapt to support method calls.

Now, you realize that some frameworks have support for public or private entries, or even a notion of "context". If you do add these concepts to the DefinitionInterface, you are forcing more features into frameworks that do not have this concept. On the other hand, if you do not put these features, you are hindering advanced DI containers from their full power.

And then, there is the question of supporting tags, and aliasing, and feature X, Y, Z..... At the end of the day, there is a real risk that your DefinitionInterface will contain all the features of all the existing frameworks.

And then, you realize something wrong happened. The DefinitionInterface does not dictate what a container should provide, it dictate HOW it should provide it. Such a PSR would force a number of features into containers. At the end of the day, all containers will look the same, supporting the same features! This PSR would not promote diversity. Instead, it would make all containers look like Symfony DI container. Which is pointless of course.

An interface should be a contract. It should not matter how your class fulfills the contract. In this regard, I feel that the ContainerInterface proposed for PSR-11 is great. But in the case of the DefinitionInterface, my feeling is that we are dictating too much.

What makes a container different from another container is the way you put things into it! There is no way you can standardize that without making all containers look the same.

Standardizing how to set things in a runtime container

At this point, we only spoke about configuration based container. The other containers (runtime containers) would actually be way easier to standardize. Almost all of them has a "set" method that takes a callback in parameter.

Yeah, I know, this is not the best you can do in terms of performance because you have to call "set" on each instance at runtime. Some PHP-FIG members will call it a bad practice (tm), and will tell you that compilation is the only way to go. Yet, there are tons of containers out there that actually work this way (hello Pimple!). And a number of micro-frameworks actually expect to be able to put things in the container at runtime (Silex and Slim for instance). And it is way easier to standardize "set" than a compiler pass or configuration file... so what about this?

And what about containers that are purely based on auto-wiring, or containers that are using annotations? Are we completely ruling them out?

Summary

Approaches differences

Regarding performance, I plan to do some tests to see if this is a real issue or not.

One last thing: I would absolutely love to see a common container format for many packages that is not bound to a particular framework. But I feel this format should be open to competition, not dictated by a PSR. Basically, if ContainerInterface is accepted, it will be possible for PHP developers to write containers that scan your application and packages for configuration files, that will compile those files, and that will make them available to the main container. Each container can have its own format.

At the end of the day, maybe one format would win over the others, just like Composer won over PEAR. This format would become a de-facto standard (until a better one appears), and this is way better than trying to dictate a format in the PHP-FIG mailing list. The ContainerInterface is the glue you need to make this competition possible.

Tags :