Creation of di php container

Keywords: PHP

Let's first determine the most basic functions of the container. First, the container needs to be able to store and extract objects.
The objects in the container can be managed uniformly in an array. The class name can be used as the key, the object as the value, and the object can be stored in the array.
When extracting an object, you only need to pass in the class name (key) to get the object.
Sometimes it is inconvenient to directly enter the class name to get the object. For example, the name of some classes is very long after adding the namespace, and the semantics of directly entering the class name is not obvious.
Therefore, you can consider using an array to record the alias corresponding to the class name.
Therefore, two arrays $bind and $instance / / $bind are required to record the mapping of class alias and class name, which is similar to this:
$bind = ['Container' => '\think\Container', 'Event' =>'\think\Event'];
The mapping of $instance record class name and object is similar to this:
$instances = ['\think\Container' => new \think\Container, '\think\Event' => function(){return new \think\Event}];
The main function of the container class is to add, delete, modify and query these two arrays
Now to start writing code, you first need to create a container class. You can use singleton mode to save container objects

class Container
{
  protected static $instance;

 protected $instances = [];

 protected $bind = [];

 public static function getInstance()
 {
    if (is_null(static::$instance)) {
          static::$instance = new static;
      }
      if (static::$instance instanceof Closure) {
          return (static::$instance)();
      }
      return static::$instance;
  }

  public static function setInstance($instance)
  {
    static::$instance = $instance;
  }
}

Containers should have the most basic methods available for external use:
bind($abstract, $concrete); ID of the binding class.
$abstract is understood as the alias of the class, $concrete is understood as the class name
instance(string $abstract, $instance) binding class name - > object
get($abstract) gets the object by class name

Judge whether there are classes and identifications in the container

  public function bound(string $abstract):bool
  {
    return isset($this->bind[$abstract]) || isset($this>instances[$abstract]);
  }

Get true class name according to alias

  public function getAlias(string $abstract):string
  {
      if(isset($this->bind[$abstract])) {
          $bind = $this->bind[$abstract];
         if (is_string($bind)) {
              return $this->getAlias($bind);
          }
     }
    return $abstract;
  }

Determine whether there are instances of objects in the container

 public function exists(string $abstract)
 {
    $abstract = $this->getAlias($abstract);
     return isset($this->instances[$abstract]);
  }

Bind a class, closure, instance and interface implementation to the container
If the input of $abstract is an array, the key and value of the array are recursively bound
If $concrete passes in a closure, it is directly put into the $bind array
If $concrete is an object, it is placed in the $instances array
If it is a string, put it in $bind

 public function bind($abstract, $concrete = null)
 {
    if (is_array($abstract)) {
          foreach ($abstract as $key => $val) {
              $this->bind($key, $val);
          }
     }
    elseif ($concrete instanceof Closure) {
          $this->bind[$abstract] = $concrete;
      } elseif (is_object($concrete)) {
          $this->instances[$abstract] = $concrete;
      }
    else {
          $abstract = $this->getAlias($abstract);
         if ($abstract != $concrete) {
              $this->bind[$abstract] = $concrete;
          }
     }
}

Enter the class name (or alias of the class, mapped to the $instances array), $concreate is the instance of the class

  public function instance(string $abstract, $concrete)
 {
     $abstract = $this->getAlias($abstract);
      $this->instances[$abstract] = $concrete;
     return $this;
  }

The following are the procedures for resolving dependencies through the api reflected by php. For example, when creating class A objects
Class A objects need to rely on class B in the constructor. At this time, containers are needed to solve the problem of dependency
First, use the information of the live class provided by the ReflectionClass system, and then use the getConstructor method to obtain the constructor information. After obtaining the constructor information, use the getNumberOfParameters method to obtain the number of parameters that the constructor depends on. If the number of dependent parameters is 0, it can be created directly.
getParameters obtains specific parameter information. Find the corresponding type of parameter (getType) in the container according to the parameter type. If not, check whether the function has a default value (getDefaultValue). If none, an error is reported and the object cannot be created

  public function getObjectParam(string $className, array &$vars)
 {
    $array = $vars;
      $value = array_shift($array);

     if ($value instanceof $className) {
         array_shift($vars);
         return $value;
      } else {
          return $this->make($className);
      }
 }
  protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = []): array
  {
      if ($reflect->getNumberOfParameters() == 0) {
          return [];
      }
     reset($vars);
      $type = key($vars) === 0 ? 1 : 0;
      $params = $reflect->getParameters();
      $args = [];
     foreach ($params as $param) {
          $name = $param->getName();
          $reflectionType = $param->getType();
         if ($reflectionType && $reflectionType->isBuiltin() === false) {
              $args[] = $this->getObjectParam($reflectionType->getName(), $vars);
          } elseif ($type == 1 && !empty($vars)) {
              $args[] = array_shift($vars);
          } elseif ($type == 0 && array_key_exists($name, $vars)) {
              $args[] = $vars[$name];
          } elseif ($param->isDefaultValueAvailable()) {
              $args[] = $param->getDefaultValue();
          } else {
              throw new InvalidArgumentException('method param miss:' . $name);
          }
     }
    return $args;
  }

  public function invokeFunction($function, array $vars = [])
 {
    try {
          $reflect = new ReflectionFunction($function);
      } catch (\ReflectionException $e) {
          throw new ReflectionException("function not exists: {$function}()", $function, $e);
      }
      $args = $this->bindParams($reflect, $vars);
     return $function(...$args);
  }

  public function invokeClass(string $class, array $vars = [])
 {
    try {
          $reflect = new ReflectionClass($class);
      } catch (ReflectionException $e) {
          throw new ReflectionException('class not exists: ' . $class, $class, $e);
      }
      $constructor = $reflect->getConstructor();
      $args = $constructor ? $this->bindParams($constructor, $vars): [];
      $object = $reflect->newInstanceArgs($args);
     return $object;
  }

  public function make(string $abstract, array $vars = [], bool $newInstance = false)
 {
    $abstract = $this->getAlias($abstract);
     if (isset($this->instances[$abstract]) && !$newInstance) {
          return $this->instances[$abstract];
      }
  if ($this->bind[$abstract] && $this->bind[$abstract] instanceof Closure) {
      $object = $this->invokeFunction($this->bind[$abstract], $vars);
  } else {
      $object = $this->invokeClass($abstract, $vars);
  }
  if (!$newInstance) {
      $this->instances[$abstract] = $object;
  }
      return $object;
  }

  public function get($abstract)
 {
    if ($this->bound($abstract)) {
          return $this->make($abstract);
      }
      throw new \Exception('class not exists: ' . $abstract, $abstract);
  }
}

Posted by bender on Sun, 28 Nov 2021 09:11:34 -0800