At work I’m currently modernising a legacy project without composer into an application with less code and thus less bugs. The first composer package I required was an application container to add Dependency Injection to the project. The container I used is called orno/di, which is now moved to to league/container.
I bootstrapped the container in the index file. After that I registered some basic stuff to it like an object of the Symfony Request class, a Monolog logger with rotating log files, … Finally we were able to adapt the existing old router to resolve the controllers and their dependencies from the IoC container.
In a couple of hours I had enabled the project to adopt and inject composer loaded classes within minutes. Moral of the story is, in any application it is necessary to have a service container which allows for binding resolving callbacks that configure the objects you need, when you need them.
The rest of this blog post will mostly handle about Laravel’s IoC container as I’ve been contributing to that one lately.
Auto resolving
But how does this container, often called application container or service container, know what to inject? Some containers allow you to configure a hierarchical tree of dependencies in config files or bootstrap code.
Since PHP 5 there is a Reflection extension available. This extension allows you to request meta data about extensions, classes, properties, objects, methods, functions and arguments. All of these have Reflector classes like ReflectionExtension, ReflectionClass, etc. This Reflection interface allows containers to inspect type hinted arguments of methods and thus constructors.
Constructor Injection
One way to inject a dependent object into a depending object is to pass it as an argument with the constructor.
You can type hint arguments of a constructor of a Controller to indicate what type of object this needs.
If the Reflection API tells the container that this constructor needs a \GuzzleHttp\Client
as the first argument, the
container can create one by recursively resolving it. If the depending class has depending classes of its own this action of
resolving dependencies can be executed recursively.
Interface type hinting
Some type hints can refer to interfaces. By definition Interfaces can’t be instantiated. In order to allow the container to know which type to use, that implements the interface, we manually need to hint that to the container. For example with the Laravel container you would do this:
Where \Vendor\Client
implements \Vendor\Contracts\Client
.
Method injection
The same action that is used to retrieve the constructor argument types can be used to get the type hints of a callable.
A callable is everything that can be passed as the first argument of call_user_func
and call_user_func_array
.
The ReflectionFunctionAbstract
class allows you to inspect all of these forms of a callable:
Laravel’s Class@method syntax
Laravel’s IoC container also allows you to call Class@method
, this will internally not only resolve an object of
type Class
but also resolve the method
’s dependencies and call it with the right arguments. This effectively does
two recursive dependency lookups. It is used in several places like Event::fire('Handler@method', [])
but it’s probably
most used for the routes definitions: Route::get('/about', 'Controller@about')
.
What got added?
In the first version of App::call(...)
it was only possible to use Laravel’s Class@method
syntax and
anonymous functions (App::call(function (\Vendor\Client $client) {...})
).
After my pull request a few weeks ago it is now possible to use every type of PHP callable, including [$object, 'method']
.
Enjoy!
The power to inject dependencies into callables
Now that it is possible to add dependencies to every callable and not having to initialize them from where you call it, there are a ton of new possibilities. One of them is the new type of validation that got introduced at Laracon EU last summer:
If your controller method depends on a Request
object that also implements the ValidatesWhenResolved
interface, the
FormRequest
’s validate
method will be called even before the controller method is called. This helps you to
keep your controllers clean.
Other callables
ServiceProvider
When an application that runs on Laravel gets booted it will make use of ServiceProvider
s to register stuff to the
container. All (non-deferred) ServiceProvider
’s register
methods will be executed first to bind callables to the
container. After that all boot
methods will be called.
The callables that get registered to the container using the $app->bind(...)
and $app->singleton(...)
methods
should be able to use the advantage of method dependency injection. For example this is currently a typical way to bind a callable to the container inside the boot
method.
If the callables that are registered in the register
methods of the ServiceProvider
are called with injected dependencies, this could be rewritten to:
Theoretically everything should be bound to the container using the register
methods.
That’s why theoretically the boot method could be called using the App::call(...)
functionality to automatically inject dependencies that need extra bootstrapping inside the boot
method.
For example in the boot method of the DatabaseServiceProvider
one could inject both the Illuminate\Database\DatabaseManager
and the Illuminate\Events\Dispatcher
.
Edit apparently the boot
method of ServiceProviders are already being called with method dependency injection. The ServiceProviders just didn’t use this yet.
Other use cases
Following are some other use cases for method injection. These are also not enabled by Laravel. Probably because
every time you depend on App::call
it involves adding a dependency on illuminate/container
to another Illuminate component.
And that’s generally not a good thing.
Cache::remember
Schema::table
Wrapping
Laravel’s container also features a wrap method.
This wrap method literally wraps any passed anonymous function into a new anonymous function.
When the returned anonymous function gets called the original function is called with
App::call()
. This enables you to add method dependency injection everywhere you want on your own.
In the case of Cache::remember
:
Other use cases
When plain PHP expects a callable, one could use App::wrap to generate a callable which in turn executes a callable
via the App::call
functionality. This could be used by Laravel for example to register exception handlers which
instantiate Whoops in a more lazy way.
Available containers
There are a number of Containers available in Composer packages. Here are a couple of them that come to my mind:
There are probably a ton of other containers. If you know one that is not on the list: @hannesvdvreken