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
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 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.
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
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
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
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
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.
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.
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!
If you liked this post feel free to subscribe to my mailing list.