Accidental Complexity Caused By Service Containers In The PHP World
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:
To set up a Newsletter
object, we need to configure the service container first. We can use a yaml file to do that:
Now we just need to create a new service container:
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
:
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:
Now let’s extract the Transport
and Mailer
logic too:
Because function autoloading is not working that well with PHP, let’s wrap this in a class:
We can use it like this:
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):
Now we just need to wrap our factory with the ServiceContainer
and then multiple uses of the same service return the same instance:
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.
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!
Hey! I’m Marc, founder of digitally induced. I love playing with exciting new technology. Feel free to contact me.
If you liked this post feel free to subscribe to my mailing list.