Hand-written service containers
You say "convention over configuration;" I hear "ambient information stuck in someone's head." You say "configuration over hardcoding;" I hear "information in a different language that must be parsed, can be malformed, or not exist."
— Paul Snively (@paul_snively) March 2, 2019
Dependency injection is very important. Dependency injection containers are too. The trouble is with the tools, that let us define services in a meta-language, and rely on conventions to work well. This extra layer requires the "ambient information" Paul speaks about in his tweet, and easily lets us make mistakes that we wouldn't make if we'd just write out the code for instantiating our services.
Please consider this article to be a thought experiment. If its conclusions are convincing to you, decide for yourself if you want to make it a coding experiment as well.
The alternative: a hand-written service container
I've been using hand-written service containers for workshop projects, and it turns out that it's very nice to work with them. A hand-written service container would look like this:
final class ServiceContainer
{
public function finalizeInvoiceController(): FinalizeInvoiceController
{
return new FinalizeInvoiceController(
new InvoiceService(
new InvoiceRepository(
$this->dbConnection()
)
)
);
}
private function dbConnection(): Connection
{
static $connection;
return $connection ?: $connection = new Connection(/* ... */);
}
}
The router/dispatcher/controller listener, or any kind of middleware you have for processing an incoming web request, could retrieve a controller from the service container, and call its main method. Simplified, the code would look this:
$serviceContainer = new ServiceContainer();
if ($request->getUri() === '/finalize-invoice') {
return $serviceContainer->finalizeInvoiceController()->__invoke($request);
}
// and so on
We see the power of dependency injection here: the service won't have to fetch its dependencies, it will get them injected. The controller here is a so-called "entry point" for the service container, because it's a public service that can be requested from it. All the dependencies of an entry-point service (and the dependencies of its dependencies, and so on), will be private services, which can't be fetched directly from the container.
There are many things that I like about a hand-written dependency injection container. Every one of these advantages can show how many modern service containers have to reinvent features that you already have in the programming language itself.
No service ID naming conventions
For starters, service containers usually allow you to request services using a method like get(string $id)
. The hand-written container doesn't have such a generic service getter. This means, you don't have to think about what the ID should be of every service you want to define. You don't have to come up with arbitrary naming conventions, and you don't have to deal with inconsistent naming schemes in a legacy single project.
The name of a service is just the name of its factory method. Choosing a service name is therefore the same as choosing a method name. But since every method in your service container is going to create and return an object of a given type, why not use that type's name as the name of the method? In fact, this is what most service containers have also started doing at some point: they recommend using the name of the class you want to instantiate.
Type-safe, with full support for static analysis
Several years ago I was looking for a way to check the quality of the Symfony service definitions that I wrote in Yaml. So I created a tool for validating service definitions created with the Symfony Dependency Injection Component. It would inspect the service definitions and find out if they had the right number constructor arguments, if the class name it referenced actually existed, etc. This tool helped me catch several issues that I would only have been able to find out by clicking through the entire web application.
Instead of doing complicated and incomplete analysis after writing service definitions
Truncated by Planet PHP, read more at the original (another 13461 bytes)