Understanding Dependency Injection and Handwriting Simple IOC Containers in PHP

Keywords: PHP Database network

Preface

Good design improves the reusability and maintainability of programs, and indirectly improves the productivity of developers. Today, let's talk about dependency injection, which is used in many frameworks.

Some concepts

To understand what dependency injection is and how dependency injection is, we first need to clarify some concepts.

DIP (Dependence Inversion Principle) relies on the inversion principle:

Programs depend on abstract interfaces, not on concrete implementations.

IOC (Inversion of Control) control inversion:

A code design scheme that follows the principle of dependency inversion, in which dependency creation (control) changes from active to passive (inversion).

DI (Dependency Injection) Dependency Injection:

A concrete implementation method of control inversion. By introducing dependencies from the outside by means of parameters, the dependency creation is changed from active to passive (control inversion is realized).

It's a little hard to understand the theory. Let's use code as an example.

First, let's look at a piece of code that relies on no inversion:


class Controller
{
    protected $service;

    public function __construct()
    {
        // Actively create dependencies
        $this->service = new Service(12, 13); 
    }       
}

class Service
{
    protected $model;
    protected $count;

    public function __construct($param1, $param2)
    {
        $this->count = $param1 + $param2;
        // Actively create dependencies
        $this->model = new Model('test_table'); 
    }
}

class Model
{
    protected $table;

    public function __construct($table)
    {
        $this->table = $table;
    }
}

$controller = new Controller;

The dependencies of the above code are Controller dependency service and Service dependency Model. From the point of view of control, Controller actively creates dependency service and Service actively creates dependency Model. Dependence is generated within the demand side, which needs to be concerned about the specific realization of dependency. This design improves code coupling, and every time the underlying layer changes (such as parameter changes), the top layer must modify the code.

Next, we use dependency injection to invert control, inverting dependencies:

class Controller
{
    protected $service;
    // Depend on passive input. An instance of the Service class declared (abstract interface)
    public function __construct(Service $service)
    {
        $this->service = $service; 
    }       
}

class Service
{
    protected $model;
    protected $count;
    // Dependent on passive input
    public function __construct(Model $model, $param1, $param2)
    {
        $this->count = $param1 + $param2;
        $this->model = $model; 
    }
}

class Model
{
    protected $table;

    public function __construct($table)
    {
        $this->table = $table;
    }
}

$model = new Model('test_table');
$service = new Service($model, 12, 13);
$controller = new Controller($service);

Dependency is transferred from external (i.e. dependency injection) by means of parameters, and dependency generation from active creation to passive injection from the perspective of control, and dependency relationship becomes dependent on abstract interface rather than specific implementation. At this time, the code is decoupled and maintainability is improved.

How to Depend on Injection and Automatically Inject Dependency

With the above theoretical basis, we have a general understanding of what dependency injection is and what it can do.

However, although the above code can be used for dependency injection, dependencies still need to be created manually. Can we create a factory class to help us with automatic dependency injection? OK, we need an IOC container.

Implementing a simple IOC container

Dependency injection is introduced in the form of constructor parameters and wants to inject automatically:

  • We need to know what dependencies the demand side needs and use reflection to get them.
  • Only instances of classes are injected, and other parameters are unaffected.

How to inject automatically? Of course, PHP has its own reflection function!

Note: The answer is yes as to whether reflection affects performance. However, compared with the delay of database connection and network request, the performance problems caused by reflection will not become the bottleneck of application performance in most cases.

1. prototype

First, create the Container class, the getInstance method:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // Getting Reflection Instances
        $reflector = new ReflectionClass($class_name);
        // Constructing Method for Obtaining Reflective Instances
        $constructor = $reflector->getConstructor();
        // Obtaining the Formal Parameters of the Method for Constructing Reflective Instances
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // If the parameter is a class, create an instance
                    $di_params[] = new $class->name;
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // Create examples
        return $reflector->newInstanceArgs($di_params);
    }
}

Here we use it to get the parameters of the construction method. ReflectionClass Class, you can go to the official documents to learn about the methods and usages contained in this class, which will not be repeated here.

ok, with the getInstance method, we can try to inject dependencies automatically:

class A
{
    public $count = 100;
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 110

2. advanced

Although the above code can be used for automatic dependency injection, the problem is that only one layer can be constructed for injection. What if class A also has dependencies?

ok, we need to modify the code:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // Getting Reflection Instances
        $reflector = new ReflectionClass($class_name);
        // Constructing Method for Obtaining Reflective Instances
        $constructor = $reflector->getConstructor();
        // Obtaining the Formal Parameters of the Method for Constructing Reflective Instances
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // If the parameter is a class, create the instance and inject the dependency into the instance
                    $di_params[] = self::getInstance($class->name);
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // Create examples
        return $reflector->newInstanceArgs($di_params);
    }
}

Test it:


class C 
{
    public $count = 20;
}
class A
{
    public $count = 100;

    public function __construct(C $c)
    {
        $this->count += $c->count;
    }
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 130

The above code uses recursion to complete the injection relationship of multi-layer dependencies. In general, the dependency hierarchy in the program is not particularly deep, and recursion does not cause memory leaks.

3. single case

Some classes are frequently used throughout the program life cycle. In order to avoid new instances in dependency injection, we need IOC containers to support singleton patterns, which are already singleton dependencies that can be obtained directly and save resources.

Increase the single case correlation method for Container:

class Container
{
    protected static $_singleton = []; 

    // Add an instance to a singleton
    public static function singleton($instance)
    {
        if ( ! is_object($instance)) {
            throw new InvalidArgumentException("Object need!");
        }
        $class_name = get_class($instance);
        // singleton not exist, create
        if ( ! array_key_exists($class_name, self::$_singleton)) {
            self::$_singleton[$class_name] = $instance;
        }
    }
    // Get a single instance
    public static function getSingleton($class_name)
    {
        return array_key_exists($class_name, self::$_singleton) ?
                self::$_singleton[$class_name] : NULL;
    }
    // Destroy a single example
    public static function unsetSingleton($class_name)
    {
        self::$_singleton[$class_name] = NULL;
    }

}

Modify the getInstance method:


public static function getInstance($class_name, $params = [])
{
    // Getting Reflection Instances
    $reflector = new ReflectionClass($class_name);
    // Constructing Method for Obtaining Reflective Instances
    $constructor = $reflector->getConstructor();
    // Obtaining the Formal Parameters of the Method for Constructing Reflective Instances
    $di_params = [];
    if ($constructor) {
        foreach ($constructor->getParameters() as $param) {
            $class = $param->getClass();
            if ($class) { 
                // If dependency is a singleton, get it directly
                $singleton = self::getSingleton($class->name);
                $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
            }
        }
    }
    
    $di_params = array_merge($di_params, $params);
    // Create examples
    return $reflector->newInstanceArgs($di_params);
}

4. Running methods in a dependency injection manner

Dependency injection between classes solves the problem. We also need the ability to run methods in a dependency injection manner, which can inject dependencies of any method. This function is useful in implementing routing distribution to controller methods.

Adding run Method

public static function run($class_name, $method, $params = [], $construct_params = [])
{
    if ( ! class_exists($class_name)) {
        throw new BadMethodCallException("Class $class_name is not found!");
    }

    if ( ! method_exists($class_name, $method)) {
        throw new BadMethodCallException("undefined method $method in $class_name !");
    }
    // Get examples
    $instance = self::getInstance($class_name, $construct_params);

    // Getting Reflection Instances
    $reflector = new ReflectionClass($class_name);
    // Acquisition method
    $reflectorMethod = $reflector->getMethod($method);
    // Find the parameters of the method
    $di_params = [];
    foreach ($reflectorMethod->getParameters() as $param) {
        $class = $param->getClass();
        if ($class) { 
            $singleton = self::getSingleton($class->name);
            $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
        }
    }

    // Operation method
    return call_user_func_array([$instance, $method], array_merge($di_params, $params));
}

Test:

class A
{
    public $count = 10;
}

class B
{
    public function getCount(A $a, $count)
    {
        return $a->count + $count;
    }
}

$result = Container::run(B::class, 'getCount', [10]);
var_dump($result); // result is 20

ok, a simple and easy-to-use IOC container has been completed. Let's try it!

Complete code

See the complete code for IOC Container wazsmwazsm/IOCContainer Originally in my framework wazsmwazsm/WorkerA It is now used as a separate project, with well-developed unit testing, and can be used in production environments.

Posted by Flames on Mon, 22 Apr 2019 20:48:35 -0700