0 x00 preface

Looking through the Laravel documentation, I found that the next chapter of the getting started guide was core Architecture, which was an exculpation for those of me who read it step-by-step. The container is one of those unfamiliar concepts that make people dizzy. However, after reading a few articles, I gradually understand the role of containers, so I hereby summarize.

0x01 Why a container?

This question can and should be replaced with “What problem does the container solve?” . Before we do that, we need to understand the concept of dependency injection. Check out this article: a brief explanation of dependency injection and inversion of control. There is a problem we run into when we practice dependency injection, which I will explain with sample code, which looks like this:

class Bread
{}class Bacon
{}class Hamburger
{
    protected $materials;

    public function __construct(Bread $bread, Bacon $bacon)
    {
        $this->materials = [$bread, $bacon]; }}class Cola
{}class Meal
{
    protected $food;

    protected $drink;

    public function __construct(Hamburger $hamburger, Cola $cola)
    {
        $this->food  = $hamburger;
        $this->drink = $cola; }}Copy the code

The Meal class relies on Hamburger and Cola, and the Hamburger class in turn relies on Bread and Bacon. Loose coupling can be achieved through dependency injection, but it can also make it cumbersome to instantiate a class with multiple dependencies. Here is an example of instantiating a package class:

$bread = new Bread();
$bacon = new Bacon();

$hamburger = new Hamburger($bread, $bacon);
$cola = new Cola();

$meal = new Meal($hamburger, $cola);
Copy the code

As you can see, in order to get a package object, we need to instantiate the dependencies of that object first. If the dependencies still exist, we need to instantiate the dependencies of the dependencies as well… In order to solve this problem, containers came into being. Containers are positioned as “tools for managing class dependencies and performing dependency injection”. With containers we can automate the instantiation process, for example we can get the package object directly with one line of code:

$container->get(Meal::class);
Copy the code

0x01 Simple container implementation

The following code is an implementation of a simple container:

class Container
{
    / * * *@var Closure[]
     */
    protected $binds = [];

    /**
     * Bind class by closure.
     *
     * @param string $class
     * @param Closure $closure
     * @return $this
     */
    public function bind(string $class, Closure $closure)
    {
        $this->binds[$class] = $closure;

        return $this;
    }

    /**
     * Get object by class
     *
     * @param string $class
     * @param array $params
     * @return object
     */
    public function make(string $class, array $params = [])
    {
        if (isset($this->binds[$class])) {
            return ($this->binds[$class])->call($this.$this. $params); }return new $class(...$params);
    }
}
Copy the code

Bind binds a class name to a closure. Make then executes the closure of the specified class name and returns the value of that closure. Let’s take a look at the container usage example:

$container = new Container();

$container->bind(Hamburger::class, function (Container $container) {
    $bread = $container->make(Bread::class);
    $bacon = $container->make(Bacon::class);

    return new Hamburger($bread, $bacon);
});

$container->bind(Meal::class, function (Container $container) {
    $hamburger = $container->make(Hamburger::class);
    $cola      = $container->make(Cola::class);
    return new Meal($hamburger, $cola);
});

/ / output Meal
echo get_class($container->make(Meal::class));
Copy the code

As you can see from the above example, bind passes a closure that returns the instantiated object of the class name. The closure also takes the container as an argument, so we can also use containers to get dependencies within the closure. The above code may seem more complicated than using the new keyword, but we only need bind once for each class. In the future, every time you need the object to directly use the make method, it will certainly save a lot of code in our project.

0x02 Strengthens the container by reflection

“Reflection” official manual php.net/manual/zh/b…

In the simple container example above, we also need to use bind to write the “script” for the instantiation. Is there a way to directly generate the instance we need? By “reflecting” and specifying a “type-hint class” in the constructor, we can automatically resolve dependencies. Because reflection allows us to get the parameters and parameter types needed to specify class constructors, our container automatically resolves these dependencies. Example code is as follows:

/**
 * Get object by class
 *
 * @param string $class
 * @param array $params
 * @return object
 */
public function make(string $class, array $params = [])
{
    if (isset($this->binds[$class])) {
        return ($this->binds[$class])->call($this.$this. $params); }return $this->resolve($class);
}

/**
 * Get object by reflection
 *
 * @param $abstract
 * @return object
 * @throws ReflectionException
 */
protected function resolve($abstract)
{
    // Get the reflection object
    $constructor = (new ReflectionClass($abstract))->getConstructor();
    // The constructor is undefined and instantiates the object directly
    if (is_null($constructor)) {
        return new $abstract;
    }
    // Get the constructor argument
    $parameters = $constructor->getParameters();
    $arguments  = [];
    foreach ($parameters as $parameter) {
        // Get the type hint class for the parameter
        $paramClassName = $parameter->getClass()->name;
        // Argument has no type hint class, throws an exception
        if (is_null($paramClassName)) {
            throw new Exception('Fail to get instance by reflection');
        }
        // Instantiate parameters
        $arguments[] = $this->make($paramClassName);
    }

    return new$abstract(... $arguments); }Copy the code

The code above is based on simply modifying the make method of the original container class, and executing the resolve method when no closure of the binding specified class is found in the Binding array. The resolve method simply takes the constructor of the specified class by reflection, instantiates its dependencies, and finally instantiates the specified class. At this point we really only need one line of code to instantiate the package class, not even configuration: -d.

$container->make(Meal::class);
Copy the code

Now the container is quite rough, of course, because if the specified class dependence scalar value (such as strings, arrays, numerical and other object types) can throw an exception, directly specified part cannot rely on and if they make mistakes relies on the interface/(ㄒ o ㄒ) / ~ ~, but these features are in the library has some mature container. If you’re interested, check out the source code, and I recommend the Pipmle project.

0 x03 summary

This article mainly introduces the container application scenarios and the implementation of a simple container, through the use of containers we can be very convenient to solve the dependency injection problems. Containers are not without their drawbacks, however, as most use reflection technology, which incurs a significant performance cost and an instance IDE generated indirectly through the container often does not recognize its type, so there is no auto-prompt (which can be solved by writing documentation comments). However, the personal feelings of introducing containers actually outweigh the disadvantages (purely personal feelings)!

PHP dependency injection container implementation