Home  >  Article  >  PHP Framework  >  ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

藏色散人
藏色散人forward
2019-08-16 14:28:513588browse

ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

ThinkPHP 6 separated the Http class from the original App class, which is responsible for the initialization and scheduling of the application, and The App class focuses on container management and conforms to the single responsibility principle.

The following source code analysis, we can understand how the class realizes automatic instantiation and how dependency injection is realized from the instantiation process of the App, Http class .

Start from the entry file


When visiting a site built by ThinkPHP, the framework first starts from the entry file, then application initialization and route resolution , controller calls and response outputs and other operations.

The main code of the entry file is as follows:

// 引入自动加载器,实现类的自动加载功能(PSR4标准)
// 对比Laravel、Yii2、Thinkphp的自动加载实现,它们基本就都一样
// 具体实现可参考我之前写的Laravel的自动加载实现:
// @link: https://learnku.com/articles/20816
require __DIR__ . '/../vendor/autoload.php';
// 这一句和分为两部分分析,App的实例化和调用「http」,具体见下文分析
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);

App instantiation


When executing new App() for instantiation, it will be called first 's constructor.

public function __construct(string $rootPath = '')
{
    // thinkPath目录:如,D:\dev\tp6\vendor\topthink\framework\src\
    $this->thinkPath   = dirname(__DIR__) . DIRECTORY_SEPARATOR;
    // 项目根目录,如:D:\dev\tp6\
    $this->rootPath    = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
    $this->appPath     = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
    $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
    // 如果存在「绑定类库到容器」文件
    if (is_file($this->appPath . 'provider.php')) {
        //将文件里的所有映射合并到容器的「$bind」成员变量中
        $this->bind(include $this->appPath . 'provider.php');
    }
    //将当前容器实例保存到成员变量「$instance」中,也就是容器自己保存自己的一个实例
    static::setInstance($this);
    // 保存绑定的实例到「$instances」数组中,见对应分析
    $this->instance('app', $this);
    $this->instance('think\Container', $this);
}

The constructor implements the initialization of various basic paths of the project, reads the provider.php file, and incorporates the binding of its class into the $bind member variable. The default content of the provider.php file is as follows:

return [
    'think\Request'          => Request::class,
    'think\exception\Handle' => ExceptionHandle::class,
];

After merging, the value of the $bind member variable is as follows:

ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

The value of $bind is a mapping of a set of class identifiers to classes. It can also be seen from this implementation that we can not only add identification to class mapping in the provider.php file, but also overwrite its original mapping, that is, replace some core classes with self-defined classes.

static::setInstance($this) implements the function as shown in the figure:

ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

The $instance member variable of the think\App class points to the think\App class An instance of , that is, the class itself saves an instance of itself.

Implementation of the instance() method:

public function instance(string $abstract, $instance)
{
    //检查「$bind」中是否保存了名称到实际类的映射,如 'app'=> 'think\App'
    //也就是说,只要绑定了这种对应关系,通过传入名称,就可以找到实际的类
    if (isset($this->bind[$abstract])) {
        //$abstract = 'app', $bind = "think\App"
        $bind = $this->bind[$abstract];
        //如果「$bind」是字符串,重走上面的流程
        if (is_string($bind)) {
            return $this->instance($bind, $instance);
        }
    }
    //保存绑定的实例到「$instances」数组中
    //比如,$this->instances["think\App"] = $instance;
    $this->instances[$abstract] = $instance;
    return $this;
}

The execution result is probably like this:

ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class

Instantiation of the Http class And the principle of dependency injection


Here, $http = (new App())->http, the first half is easy to understand, but the second half is a bit confusing at first glance. The App class does not have http member variables, so why do you boldly call something that does not exist?

It turns out that the App class inherits from the Container class, and the Container class implements the __get() magic method. In PHP, when the accessed variable does not exist, the __get() magic method will be triggered. The implementation of this method is as follows:

public function __get($name)
{
    return $this->get($name);
}

Actually calls the get() method:

public function get($abstract)
{
    //先检查是否有绑定实际的类或者是否实例已存在
    //比如,$abstract = 'http'
    if ($this->has($abstract)) {
        return $this->make($abstract);
    }
    // 找不到类则抛出类找不到的错误
    throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
}

However, in fact, it is mainly the make() method:

public function make(string $abstract, array $vars = [], bool $newInstance = false)
    {
        //如果已经存在实例,且不强制创建新的实例,直接返回已存在的实例
        if (isset($this->instances[$abstract]) && !$newInstance) {
            return $this->instances[$abstract];
        }
        //如果有绑定,比如 'http'=> 'think\Http',则 $concrete = 'think\Http'
        if (isset($this->bind[$abstract])) {
            $concrete = $this->bind[$abstract];
            if ($concrete instanceof Closure) {
                $object = $this->invokeFunction($concrete, $vars);
            } else {
                //重走一遍make函数,比如上面http的例子,则会调到后面「invokeClass()」处
                return $this->make($concrete, $vars, $newInstance);
            }
        } else {
            //实例化需要的类,比如'think\Http'
            $object = $this->invokeClass($abstract, $vars);
        }
        if (!$newInstance) {
            $this->instances[$abstract] = $object;
        }
        return $object;
    }

However, However, the make() method mainly relies on invokeClass() to implement class instantiation. Detailed analysis of this method:

public function invokeClass(string $class, array $vars = [])
    {
        try {
            //通过反射实例化类
            $reflect = new ReflectionClass($class);
            //检查是否有「__make」方法
            if ($reflect->hasMethod('__make')) {
                //返回的$method包含'__make'的各种信息,如公有/私有
                $method = new ReflectionMethod($class, '__make');
                //检查是否是公有方法且是静态方法
                if ($method->isPublic() && $method->isStatic()) {
                    //绑定参数
                    $args = $this->bindParams($method, $vars);
                    //调用该方法(__make),因为是静态的,所以第一个参数是null
                    //因此,可得知,一个类中,如果有__make方法,在类实例化之前会首先被调用
                    return $method->invokeArgs(null, $args);
                }
            }
            //获取类的构造函数
            $constructor = $reflect->getConstructor();
            //有构造函数则绑定其参数
            $args = $constructor ? $this->bindParams($constructor, $vars) : [];
            //根据传入的参数,通过反射,实例化类
            $object = $reflect->newInstanceArgs($args);
            // 执行容器回调
            $this->invokeAfter($class, $object);
            return $object;
        } catch (ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
        }
    }

It can be seen from the above code that if the __make() method is added to a class, it will be called first when the class is instantiated. The most worth mentioning above is the bindParams() method:

protected function bindParams($reflect, array $vars = []): array
{
    //如果参数个数为0,直接返回
    if ($reflect->getNumberOfParameters() == 0) {
        return [];
    }
    // 判断数组类型 数字数组时按顺序绑定参数
    reset($vars);
    $type   = key($vars) === 0 ? 1 : 0;
    //通过反射获取函数的参数,比如,获取Http类构造函数的参数,为「App $app」
    $params = $reflect->getParameters();
    $args   = [];
    foreach ($params as $param) {
        $name      = $param->getName();
        $lowerName = self::parseName($name);
        $class     = $param->getClass();
        //如果参数是一个类
        if ($class) {
            //将类型提示的参数实例化
            $args[] = $this->getObjectParam($class->getName(), $vars);
        } elseif (1 == $type && !empty($vars)) {
            $args[] = array_shift($vars);
        } elseif (0 == $type && isset($vars[$name])) {
            $args[] = $vars[$name];
        } elseif (0 == $type && isset($vars[$lowerName])) {
            $args[] = $vars[$lowerName];
        } elseif ($param->isDefaultValueAvailable()) {
            $args[] = $param->getDefaultValue();
        } else {
            throw new InvalidArgumentException('method param miss:' . $name);
        }
    }
    return $args;
}

And among these, the most worth mentioning is the getObjectParam() method:

protected function getObjectParam(string $className, array &$vars)
{
    $array = $vars;
    $value = array_shift($array);
    if ($value instanceof $className) {
        $result = $value;
        array_shift($vars);
    } else {
        //实例化传入的类
        $result = $this->make($className);
    }
    return $result;
}

getObjectParam() method is glorious again Call the make() method to instantiate a class, and this class is exactly the parameter extracted from the Http constructor, and this parameter is exactly an instance of a class - an instance of the App class. At this point, the program not only instantiates the Http class through PHP's reflection class, but also instantiates the dependent App class of the Http class. If class App depends on class C, and class C depends on class D... No matter how many layers there are, the classes that the entire dependency chain depends on can be instantiated.

In general, the whole process is like this: you need to instantiate the Http class ==> extract the constructor and find that it depends on the App class ==> start instantiating the App class (if there are dependencies found , then continue to extract until the end of time) ==> Pass the instantiated dependency (an instance of the App class) into the Http class to instantiate the Http class.

This process is called "Dependency Injection" if you give it a pretentious name, or "Inversion of Control" if you give it a confusing name.

This process, if we go back to ancient times, we need to instantiate the Http class, which is probably implemented like this (if there are many layers of dependencies):

.
.
.
$e = new E();
$d = new D($e);
$c = new D($d);
$app = new App($c);
$http = new Http($app);
.
.
.

How tiring this must be. As for modern PHP, just leave it to the "container". Containers also have many functions, which will be explained in detail later.

The above is the detailed content of ThinkPHP6 source code: See how dependency injection is implemented from the instantiation of the Http class. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:learnku.com. If there is any infringement, please contact admin@php.cn delete