Hand-written service containers

Posted on by Matthias Noback

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 in Yaml (or any other meta-language for that matter), if I write them in PHP, I get all the support from static analysis tools. Even if I don't use a separate tool like PHPStan or Psalm, PhpStorm will point out any issues early on. Missing classes, missing import statements, too few or too many arguments, everything will be pointed out to me when I'm editing the ServiceContainer class in my IDE. This is a huge advantage.

Easy to refactor

Because analysis is easy, we can also expect all the help there is when refactoring our code. If we change production code, opening the ServiceContainer in your IDE will show you any issues you've produced. Furthermore, because there's no special service definition format, your IDE doesn't need a special extension to deal with it. It's just plain PHP code. So any refactoring tool that you use (e.g. rename method, move to different namespace, etc.) will also deal with any existing usages inside the ServiceContainer class.

Easy to make a distinction between public entry points and private dependencies

I like how the Symfony service container allows users to make a distinction between private and public services. Public ones can be fetched using a call to get($id), private ones can only be used as dependencies for other (public or private) services. Some services indeed deserve to be public (mostly the services we earlier called "entry points"), most should remain private. Of course, the distinction between public and private services reminds us of the way we can have public and private methods too, and in fact, if you write your own service container, you will use these method scopes to accomplish the same thing.

If you hand-write the service container you can do some optimizations too, just like the Symfony container does them. For instance, if you have a private service that's only used in one place, you can inline its instantiation. As an example, consider the private invoiceService() method:

public function finalizeInvoiceController(): FinalizeInvoiceController
{
    return new FinalizeInvoiceController(
        $this->invoiceService()
    );
}

private function invoiceService(): InvoiceService()
{
    return new InvoiceService(
        new InvoiceRepository(
            $this->dbConnection()
        )
    );
}

This method is only used by finalizeInvoiceController(), so we can safely inline it:

public function finalizeInvoiceController(): FinalizeInvoiceController
{
    return new FinalizeInvoiceController(
        new InvoiceService(
            new InvoiceRepository(
                $this->dbConnection()
            )
        )
    );
}

If, due to refactoring efforts, a private service is no longer needed, PhpStorm will tell you about it.

No need to define partial service definitions to assist auto-wiring

Auto-wiring has become quite popular, but I'm not convinced it's the way to go. I'm sure most scenarios have been covered by now, so I'm not afraid that things won't work out between me and auto-wiring. However, we'll always have to do tricks to make it work. We have to give the wirer hints about which implementations to use. This means that we may be able to delete many service definitions, but we also have to keep some around, since there are some things that won't work without them. You need to have in-depth knowledge about the dependency injection tool you use, and you need to learn the syntax for helping the auto-wirer. Worse, you may decide to adopt your production code so that the wirer can understand it.

Needless to say: if you write your service definitions in your own PHP class you'll never need custom syntax. In fact, you don't need to look up specific documentation at all, because you don't have to worry about failures to resolve dependencies; you make all the decisions yourself when you write your ServiceContainer class.

No need to inject primitive-type values by their parameter name

A downside of auto-wiring containers is that they need special instructions when the injected constructor arguments aren't objects, but primitive-type configuration values.

namespace App\Db;

final class Connection
{
    private $dsn;

    public function __construct(string $dsn)
    {
        // ...

        $this->dsn = $dsn;    
    }
}

Object-type constructor arguments can usually be resolved, but the service definition needs an extra hint for the $dsn argument:

    App\Db\Connection:
        arguments:
            $dsn: 'mysql:host=localhost;dbname=testdb'

This exposes an implementation aspect of the service class itself, which would normally remain hidden behind its public interface. To a client that instantiates an instance of Connection, only the parameter types should be relevant, not their names. In fact, a developer should be able to rename the parameter $dsn to something else, without breaking the application. Of course, a smart container builder will warn you about it when you rename a parameter, but this comes with some extra indirection that wouldn't be needed at all if we'd just write the instantiation logic inside a manual ServiceContainer class, where parameter names are irrelevant (as they should be).

No magic effects on the service container

Talking about auto-wiring, I have to say I dislike the fact that the location of a file containing a class has an influence on it being defined as a service in the container. I'd want to be able to create a new class anywhere in the project, and decide for myself whether or not it gets defined as a service. Needless to say, you won't have any magical effects like this if you write a custom service container, but personally I won't miss them.

Easier to distinguish between services and singletons

Most service containers will automatically share instances of a service; if a service has been instantiated once, the next time you ask for it, you'll get the exact same object instance. This is important for things like a database connection; you want to reuse the connection, instead of connecting to the database every time you need something from it. However, most services should be stateless anyway, and in that case it isn't really necessary to retrieve the exact same instance.

A service that's instantiated once and then shared between different clients is traditionally called a "singleton" service. Singleton services were usually implemented using the Singleton design pattern, which actually protects them from being instantiated more than once. Nowadays a service container manages service instances, and although it doesn't use the Singleton design pattern, it still makes every service effectively a singleton service: there's at most one instance of every service.

What I like about using a hand-written service container is that you can make a clear distinction between services for which it's actually important to have only one instance, and services for which it doesn't matter. Using the same example as earlier, note that the controller service can be re-instantiated every time a client needs it, and the connection service will be stored in a static variable, so that it can be reused:

final class ServiceContainer
{
    public function finalizeInvoiceController(): FinalizeInvoiceController
    {
        return new FinalizeInvoiceController(/* ... */);
    }

    private function dbConnection(): Connection
    {
        static $connection;

        return $connection ?: $connection = new Connection(/* ... */);
    }
} 

What about performance? Well, if it starts to hurt, you can always add some more shared services. But in most cases, I don't think it'll be needed. In part because of the fire-and-forget nature of the PHP server, but also because most services will be instantiated and used only once or twice anyway.

Easier to override parts for testing

With a service container based on a meta-language, like Yaml service definitions, you have to build in a mechanism for modifying the service definitions for use in different environments. With Yaml you can load in multiple service definition files and override service definitions and parameters. But just like "public" and "private" services, the concept of overriding services is also built into the programming language itself, namely by overriding methods. For example, if you want to use a fake/catch-all mailer or something while in development, you can do something like this:

abstract class ServiceContainer
{
    abstract protected function mailer(): Mailer;
}

final class DevelopmentServiceContainer extends ServiceContainer
{
    protected function mailer(): Mailer
    {
        return new FakeMailer();
    }
}

final class ProductionServiceContainer extends ServiceContainer
{
    protected function mailer(): Mailer
    {
        return new SmtpMailer(/* ... */);
    }
}

Optionally testable

If you want, you can even write an integration test for your hand-written service container. Then you could prove that the DevelopmentServiceContainer actually uses a FakeMailer. Or you can verify that all the public entry-points can be instantiated without producing any runtime errors (although static analysis will catch most of the issues already).

To be honest, this is also possible when you use a meta-language for defining services; you can always run tests against the compiled service container. However, I don't see this happening often, so I just wanted to mention the possibility here.

Composing containers? Separating containers?

A big issue with maintaining a hand-written container is that you wouldn't want to rewrite all of the framework's service definitions yourself. In fact, this isn't what I'm suggesting here. If you use a framework which ships with a service container, you just don't have to use it for your own service definitions. For instance, you can define your service container as a service in your framework's service container, and have access to it in the usual ways the framework supports.

You can even have multiple containers, for each of the modules/contexts you distinguish in your application. This could help keeping them actually separated (however, there's no technical way to enforce a context to protect the integrity of a context, it'll always be a people issue too).

Note that composition of containers is something that service containers don't usually offer, but the programming language itself is capable of. You only have to inject containers as constructor arguments of other containers.

Conclusion

In an educational setting I found that one of the biggest advantages of having your own hand-written service container for your Application and Domain layer services is that it allows you to write acceptance tests using these services. You can freely instantiate the container in your Behat FeatureContext. You can then write tests which talk to the Application layer (instead of the Infrastructure layer as they usually do). These tests will run in a very short time, but most importantly: they will be less brittle, because they don't (and can't) rely on all kinds of infrastructure-level peculiarities.

In a project setting, I haven't been fortunate enough to be able to use a hand-written service container. I'll just wait for the next opportunity to do so. If in the meantime you find yourself agreeing with the thought experiment which is this article, and have even applied the idea in practice, let me know how it worked out for you!

Finally, some suggestions for further reading:

  • Mathias Verraes has a nice explanation-in-a-gist where he argues that "We don't need no DIC libs", written after a discussion on Twitter about this topic. The gist also includes some implementation examples.
  • Marijn Huizendveld has an interesting blog post about how you can deal in a better way with environment variables (which works well with hand-written service containers too).
PHP dependency injection service container
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Mirko Rapisarda | PED Technology

Hi Matthias,

I have just finished to read the post and I have found it very interesting :)

In the past I found myself in the condition to develop my hand-written service container (I did it for a project with Magento 1 :'(), so I experimented by myself how is to use a "custom" service container. Initially was good, but then I found hard (and boring :-D) to construct classes over classes (expecially with Magento 1). Then I found a great library that I found very simple to use, the name is PHP-DI. It's very good because use PHP to declare the dependencies and it resolves a lot of problems that you could have using meta-language such as YAML. At summary I have decided to use this library for all "old" projects without a framework such as Symfony or Laravel. I have never been tried to use a php file to define my services in Symfony, maybe this could be my next experiment :D.

Pieter Eggink

Hi Mathias, I’m using value objects for configuration of primitive types in Symfony. Then I can use auto wiring for my services, except for configuration value objects.

Wojtek Międzybrodzki

But then they (the value objects) are registered as Services in the container?
I thought about such thing, but found the idea of my configuration "variables" available as services a bit odd.
Or is it ok? :)

Pieter Eggink

You can use Value Objects or use local service binding which can be used since version 3.4 (https://symfony.com/blog/ne...

Global binding

services:
_defaults:
autowire: true
autoconfigure: true
public: false
bind:
$projectDir: '%kernel.project_dir%'

Service binding

App\Some\Service1:
$projectDir: '%kernel.project_dir%'

App\Some\Service2:
$country: 'nl'

Mathias Verraes

A DIC can return values or value objects, I don't see a problem there. But if you use that value in say an if statement, that's a smell that you can refactor: instead of returning values, return a preconfigured or polymorphic service.

Wojtek Międzybrodzki

I don't know how to configure parameter of type other than string /
array in the stereotypical Symfony's "parameters.yml", so I assumed I
must register said value object as "service". But now I see it can be done,
at least by configuring the container in php code.

Maybe it's also my wrong point of view, of container containing two separate concepts - parameters - "values" and services - "stateless objects with some behaviour".

Mathias Verraes

The main point is dependency injection, not containers. Containers are just an implementation detail. Therefore I don't think a container must be restricted in terms of what types it can return. We can however ask ourselves what kind of things are good to inject and what not. If my class depends on a configuration, say themeColour, how should that get into my class? We could inject a Configuration service, but that's like going out to buy some bread and coming back with the entire supermarket. We really only want to depend on themeColour (of the type Colour). Value objects are, because they're immutable, very safe to pass around like that.

However, to reiterate on my previous point, if my class depends on themeColour to decide in what colour to render something, perhaps it lacks a ThemeRendering service, which is configured by the DIC using themeColour, so that my class doesn't need to care about it.

Wojtek Międzybrodzki

True, so I learned that my view of DI containers was too tightly coupled with Symfony DI container. Value objects make much more sense for me than string parameters. Guess I took the sometimes-helpful heuristic division between "parameters" and "services" all too seriously.

Of course, your point is valid and I agree with it completely - that's SoC and SRP for me.

Matthias Noback

Oh, that's a very cool trick!

Roman O Dukuy

Looks greate! And what about response? I am not understand who responsobility for create user Response. Can you show it in this article? Thanks

Matthias Noback

Thanks; I don't know what you mean by response, please elaborate.

Roman O Dukuy

I am about http response.

Stephan Hochdörfer

Excellent article ;) Most of this led to the creation of Disco which is a very lightweight DI library: https://github.com/bitexper...

I'd still prefer to use a simple library and use some extra features like the management of singletons or other object lifetime specific options rather than copying that portion into each & every of my projects.

Matthias Noback

Thanks Stephan, Disco is definitely a very cool library! And since it doesn't have many of the problems described in this article, it could certainly be a nice thing to use. However, I don't have personal experience with that, so I can't really recommend it here.

Maik

This may be slightly off topic but since a few weeks i am thinking about dependency injection in PHP.
I'd like to know if you think it's cracy or might be a good idea.

I think of dependencies allways as objects. So an dependency injection container is nothing else than an store for objects. In this store the objects are sorted by their type (which should be interfaces) and their name. An object of the same type could be contained multiple times with different names.

Within such an container we could run our complete application.
The invocation could look similar to this.
retreive(RoutingRuleInterface::class(),'MyRoutingRule');

$Os->callObjectByRoutingRule($Rule);

// Lets assume $Rule tells the Store to run method test()
...
public function test(MyDependency $Crazy) {
// In this case the Store would look if it contains an object named $Crazy of type MyDependency
}
...

In reality the store would only contain the factories of the objects. Which would be written in php and could be
easily tested. If an object should be retrieved
it will be initiated and returned. Did i mention, i am a big fan of immutable objects ;-)
What do you think? Is it a good idea to use an object store as foundation of an application? I heard smalltalk does that somehow similar.

Matthias Noback

I'm not a fan of automatically resolving method arguments, but I agree that the services in an app can be seen as one large object graph. You only need to find the entry point that should be called for a given request or CLI command.

gggeek

Nice thinking-out-of-the-box exercise, thx! A few points are worth discussing, though.

I agree with Alex about the 'no need for naming conventions' remark. You generally _want_ to use naming conventions for ease of maintenance as soon as you have more than 3-4 services in use - or 3-4 devs on the project ;-), whether the names are class names or service names. And refactoring services by changing the class name makes it harder to keep things clean.

About readability, taking Symfony as an example, I find it often easier to understand the "scope" (role? dependencies?) of a service by looking at its Yml definition than it would be if I was to have to check the constructor of each service, as they often exhibit different patterns: singleton/factory/crazystupthing/...

...and tbh, in your example I would have no way to guess which service is a singleton and which one not without going through the dic codebase - which as service consumer I don't want to do, unless of course we adopt a naming convention ;-)

About private/public services and inlining private ones: quite often what starts out as a private service becomes a public one in the long run. Making it inline when 1st coding it makes the refactoring harder later on.

In my experience, one of the greatest benefits of the DIC is that it allows me to _swap out existing classes for new ones_ when the needs arise. This happens a lot, both when evolving the custom code of the app at hand, as well as when creating a codebase on top of existing libraries/bundles. In both scenarios, having an interface to depend on for each service rather that typehinting to the existing implementor class is vital.
Forgetting the autowiring, I'd say that having a custom-written DIC is not a step forward in either scenario...

From 10 km above, the DIC in the end looks just like a calling-convention that everybody has to adhere to when sharing functionality across the codebase. It often helps exactly because it introduces separation between parts as well as the convention.

gggeek

ps: I like the idea of container composition: kiss for your own app code, and flexibility and patterns for the framework code.

Alas, often what starts out as an 'app service' becomes a 'framework service' as soon as a new developer joins the project, as it will be foreign code to her, and smell and look exactly like imported framework code, less the good docs and quality and reliability of course.

Matthias Noback

I think that whether or not this approach can be successful will also depend on the kinds of services you put into this hand-written container, and which ones you leave to the framework's container.
There's an interesting similarity with how you can (should?) use your own event dispatcher as opposed to dispatching your domain events through the framework's event dispatcher (mixing them with infrastructure-related events).
I'm sure there will be a maintenance issue when your app becomes really large, and probably composition becomes more important (just like having separate Yaml files). Honestly, I'm looking for some real-world experience with this idea, to find out where the bottlenecks are.

Paul M. Jones

I have been using hand-written service containers for a while now, with very positive effects. Cf. the one in Bookdown https://github.com/bookdown... and the one in Atlas 2 https://github.com/atlasphp... .

What's interesting is that you can combine the hand-written service container rather easily with another one, if you want to offload the creation of specific objects to other containers. The Atlas ones do so in v3 via a factory callable; cf. the Atlas 3 TableLocator https://github.com/atlasphp... (itself a container) specifically the newTable() factory method at the end.

Matthias Noback

Cool, thanks for sharing!

Tetracyclic

I raised this on the reddit thread, but I'd be interested in your thoughts on the Laravel DI container, as as far as I can tell, most of the criticisms in this article don't apply to it.

• Configuration is all done using plain, type-safe PHP in a similar manner to their own examples of a hand-written container, as such it's easy to refactor with an IDE

• There are no specific service ID requirements and most will simply use the name of the class or interface through the use of ::class

• I've yet to find a case where you need to adjust your service definitions to aid the auto-wirer

• As it's plain PHP, there are no issues with more complex configuration when passing a service scalar values

• There's no inherent binding between file location and whether or not it's a service

• Binding different implementations in different environments is very straightforward and the configuration for this can remain in the same location as the general service configuration

• Making a service act like a singleton is optional

• You can inline instantiation of other dependencies if it's appropriate

Matthias Noback

Thanks for mentioning this here; I have no experience with the Laravel DI container, but based on what you write about it here, I think this solves a significant number of the usual issues. Looking at the docs now, I still think the indirection of the configuration isn't necessary (well, it is, because it supports auto-wiring), but at least it doesn't leave too many aspects of the configuration implicit.

Alex Rock

Very interesting post, I agree with some terms, but I probably need more details about some others…


No service ID naming conventions


Well, you do: $serviceContainer->finalizeInvoiceController


It’s a name you must remember that name, and if you change the class name one day, it won’t be consistent anymore with the method name (unless you remember to change it).

Indeed it will change the class referenced inside the container, but not the method name.


Something like $serviceContainer->get(FinalizeInvoiceController::class) would need some “magic” but in fine it’s perfectly fine as you know the container should return you an instance of the FinalizeInvoiceController class.

If you do this and rename the class, instead of “Method finalizeInvoiceController not found”, you’ll directly get “Class FinalizeInvoiceController not found”, which explains better the issue. And all good IDEs will automatically change the ->get(...) argument with the right class if you refactor it (PHPStorm does it perfectly, for instance).


For instance, if you have a private service that’s only used in one place, you can inline its instantiation.


Yes, it’s nice, but what about automatically do this? That’s what the new DI workflow introduced in Symfony 3.3 and 3.4 is about: make us focus on our code, and not the container. Injection is totally automated then. And since I started using this new way of handling DI, I never had any issue with my container that is related to “how I configure it”. It’s really straightforward and now it’s almost like it doesn’t exist and all my classes are just glued together by the component without me thinking about it.


No need to define partial service definitions to assist auto-wiring


Yep, auto-wiring needs an information about “types”, because it allows our classes to depend on abstractions, not implementations, making them SOLID.


No need to inject primitive-type values by their parameter name


I don’t see in what it becomes a problem. $dsn: '%env(DATABASE_DSN)%' will also hide implementations behind the app’s configuration, exactly like new Connection($_SERVER['DATABASE_DSN']).

Easier to override parts for testing


Well, I think this is also pretty straightforward:


# services.yaml
services:
mailer:
class: App\Mailer\SmtpMailer

# services_test.yaml, overrides services.yaml
services:
mailer:
class: App\Tests\FakeMailer
Mathias Verraes

>> It’s a name you must remember that name, and if you change the class name one day, it won’t be consistent anymore with the method name (unless you remember to change it).

>> Indeed it will change the class referenced inside the container, but not the method name.

1. Changing a class name doesn't impact existing code: I call that a benefit :-)

2. It's trivial to make aliases, so you can keep the old name and the new.

3. That said, I agree that in general you'd prefer to make it consistent. In 2019, no human should ever rename a class or method by hand, use an IDE with proper refactoring tooling. That way you don't have to rely on runtime errors.