How does the Service Container work?
Found a typo? Edit meA Service Container is basically a class that behaves like a box, we can think of it as a singleton object (but it is not, it is simply an object that is instantiated by the framework while it is being bootstrapped), on which we can declare all of our dependencies to be resolved automatically by the framework.
The role of a Service Container is to define which classes (with their inner dependencies) should be resolved automatically.
Usually, frameworks like Symfony or Laravel brings already a bunch of interfaces that will resolve automatically
specific classes. So, for \Psr\Log\LoggerInterface
there is somewhere a class that implements this interface
like Monolog
or any other.
Let's imagine we have a service whose dependency is a UserRepository
(this dependency belongs to our use case, so it
is not defined in the framework), additionally, depending on the environment, we want to inject
a PostgreSQLUserRepository
or a InMemoryUserRespository
instance.
This post will be based on the two most popular PHP frameworks (Symfony & Laravel), but the idea works for any other programming language/framework.
Symfony
We can define our services as php
or yaml
file extension (you can define this configuration directly in
the src/Kernel.php
file).
# config/services.php
;
return ;
Alternatively, we can define a different instance for the testing environment:
# config/services_test.yml
;
return ;
Laravel
In Laravel, however, you can do this in the AppServiceProvider
class.
;
Injecting the dependencies in our services
So, from now on, the next time we try to inject our interface UserRepository
as a dependency in the constructor of
another class, eg:
The framework will handle it, checking firstly if we defined in our service provider the injected class and creating a new instance of it. In case this class has any dependency, the framework will resolve them automatically recursively by reflection until the class is ready, in case any dependency cannot be resolved or the dependency is a primitive, and we didn't define the value, it will throw an exception.
Let’s go deeper into the technical details, how could you create such a dependency resolver? This is how we did in Gacela (link), and this is a simplified version of it:
This is the idea of what a framework does under the hood; ideally, we should use a cache layer. Using reflection takes a lot of resources, and it is very slow; additionally, we are not taking into consideration different scenarios like what to do depending on whether the resolved parameter is a primitive or even a callable.