In previous article, we've see that the magician $app produced the $kernel out of gloved hands
with a spell Illuminate\Contracts\Http\Kernel::class. Let's reveal the secrets of the magic.
The $app is a instance of Illuminate\Foundation\Application, which extends Illuminate\Container\Container.
The magic is inherited from here, the container, or recognized as IoC container.
How it works? It's absolutely not to simply call new on the given class. In fact, you'll find it's a interface rather then a class:
We've also find some code seems like binding a instantiable class with the interface:
So is the $app just call new on this class? You can try it, an ArgumentCountError will be thrown.
The constructor of class App\Http\Kernel requires 2 parameters.
For instantiating, the container need to know this. Therefore, there must be metaprogramming inside, must be reflecting inside. Let's trace the code:
We call singleton() on bind() with an $abstract and a concrete class.
The $abstract is just act as a string, you can replace Illuminate\Contracts\Http\Kernel::class
both in public/index.php and bootstrap/app.php to 'This is the HTTP Kernel!'.
It'll work fine as before. For magical, let's just call it the 'Spell'.
There is benefit to use the class or interface name as the Spell, We'll see later.
The container keeps a map from each Spell to a anonymous function for instantiation: $this->bindings.
When you call make() or get() on the container, these instantiation functions will be call.
There are some recurrent calls, but in the very end, it will call the build() with the $concrete you first given.
It end up as the code like this:
You can modify the public/index.php in this way. It'll work well as well.
Why don't Laravel just take this simple way? Because we may access the kernel in other places, a singleton is necessary.
What's more, functions added by extend() will not work if we use build() directly instead of make().
We will talk about extend() later.
Let's back to the build() called with a class name.
The metaprogramming magic is here. They reflect the class and get the dependencies from constructor.
Then make the dependencies, and call the constructor with these dependencies to instantiate.
If we add a class or interface as type hint of a parameter. The container will use it as the Spell.
Now you see, that's the benefit of using class or interface name as Spell we've talk about.
We only need to write the Spell as type hint in __construct(), and the container will call the Spell automatically.
That's why Laravel using Illuminate\Contracts\Http\Kernel::class rather then something like 'This is the HTTP Kernel!'.
If the type hint is not suitable as a Spell, such as the dependency is a string or a resource, or the same Spell for different concretes in different place.
We can use a feature named contextual concrete:
Or you can use more readable syntax like the case in documentation:
For readable and efficiency, we can set aliases to Spells.
If you can use 'Ex' instead of 'Expelliarmus', Noseless Voldemort will not have enough time to call the 'Avada Kedavra'.
It's a joke, but app for Illuminate\Contracts\Foundation\Application,
http-kernel for Illuminate\Contracts\Http\Kernel etc. will be helpful.
We also need take care of these code:
The object returned by resolve() may not be the one from build().
Follow the getExtenders(), we find:
That's means we can use extend() to add modifiers to end up modify or change the object.
If you bind with abstract and concrete class, like our kernel example,
the resolve will recurrently called with both the abstract and concrete.
Therefore, you can both call extend with abstract and concrete.
For extendable, understandable and maintainable reason, it's better only to modify the object but not to change it in the extender.
Now, let's talk about the IoC. IoC is short for Inversion of Control. It's made up with Dependence Inversion Principle (DIP) and Dependency Injection (DI). The DIP says: a module should not depend on another module, but both them should depend on abstraction. For example:
As we see, class App is not depend on class FooConnection,
but both two classes depend on the interface Connection.
So when we change FooConnection to BarConnection, class App need not be modified.
And when we test the class App, we can pass a MockConnection to prevent a real connection.
So far, we also need face to a problem: every time we instantiate App, we need instantiate a implement of Connection first.
Different implement of Connection may requires different dependencies, dependency may has it's own dependencies...
Now, DI is what we need, it is a feature that detecting the dependencies then instantiating them and injecting into the constructor.
This just happen to be what container acts.
So we have a overview of IoC, decoupling dependent between modules, and instantiating automatically with container. Your module code and module using code become more declarative, and standalone, not caring about details about other modules but only interfaces.
I just have a question about the naming 'Inversion'. That's not invert anything, that's just the original way to design things. Do you agree?
After touching the textbook knowledge, let's back to see our example:
The \Illuminate\Contracts\Foundation\Application is a interface but \Illuminate\Routing\Router is not.
So this is not fully follow the DIP. So the defect is we cannot change the router freely. But we still can extends it.
We can write a MyRouter extends \Illuminate\Routing\Router, then register it in the App\Providers\RouteServiceProvider:
Now, you can make creative works in MyRouter. If you register MyRouter in bootstrap/app.php,
you will find your routes will not work, and all request will responded with 404. That's some reason about the service providers.
We will talk about this on next article.
DI is so powerful, we can expect it's not only for constructing. By searching public function in the container class,
we can find to methods that official documentation not talk about: call() and wrap().
Here is some examples for call():
The method wrap() however, returns a anonymous function for calling later or higher order functions.
unfortunately, the wrap() is not as powerful as call() because of a improper type hint.
Summing up
In this article, we dig into Container, find out how it works. Then learn what is IoC, DIP and DI, know why the container designed like this. And show a example about how to extend a framework module with the container.