文章大纲

Laravel源码解析系列——依赖注入的类是如何自动实例化的

2019年11月08日 23:54

本文不会讲解什么是依赖注入。

如果不清楚依赖注入的,可以先看看这个链接:http://www.zhai14.com/blog/6801e8176b4df0db4e4069fbead45cc6.html


想大概了解一些php反射函数的用法,可以先看看这个链接:http://www.zhai14.com/blog/db283f8383839a9d376796578fb6a77e.html


如果你用过Laravel框架,我想你应该有写过类似如下的代码:

class UserController extends Controller
{
private $mainRepository;

public function __construct(Request $request, UserRepository $mainRepository)
{
parent::__construct($request);
$this->mainRepository = $mainRepository;
}

public function register(Request $request)
{
return $this->response($this->mainRepository->register($request->all());
}
}

class UserRepository
{
private $mainModel;
public function __construct(UserModel $model){
$this->mainModel = $model;
}

public function register($userId){
//code to register
}
}

你有没想过,UserController和UserRepository构造函数里的UserRepository、UserModel类,它们怎么没有实例化呢?

没有实例化,代码就执行到UserController的register方法里,不是应该会报错吗?本文主要就是解惑此问题的。


先抛开Laravel框架,我们自己来实现一下类似上面的一个小程序。

如下是一个组装电脑的简单程序,其中电脑由键盘、鼠标、显示屏这些设备组成,所以组装电脑之前,需要先将这些设备准备好。

class Computer
{
private $keyboard;
private $mouse;
private $screen;

public function __construct(Keyboard $keyboard, Mouse $mouse, Screen $screen)
{
$this->keyboard = $keyboard;
$this->mouse = $mouse;
$this->screen = $screen;
}
   
    //电脑组装
public function assemble(){
$this->keyboard->prepare();
$this->mouse->prepare();
$this->screen->prepare();
echo "OK, the work of ASSEMBLING a computer has been finished<br>";
}
}

//键盘
class Keyboard
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

//鼠标
class Mouse
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

//显示屏
class Screen
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

现在我们要组装电脑,我们一开始都这样写:

$cp = new Computer(new Keyboard(), new Mouse(), new Screen());
$cp->assemble();

就是将依赖注入的Keyboard、Mouse、Screen类都实例化一遍,然后再作为参数传到构造函数里去。


可我已经在本文开头用代码说明了,Laravel里这些依赖注入的类,并不需要我们自己去一个个的实例化。

那么,它是如何做到的呢?

这里用到了php提供的反射函数库,进入php手册,搜索Reflect,你会看到更详细的文档介绍,这里就不赘述了。


下面是实现代码:

$cp = new ReflectionClass("Computer");
$constructor = $cp->getConstructor();
$argu = $constructor->getParameters();
$objArr = [];
foreach($argu as $item){
    //这里你可以var_dump($item), 可以看到$item的结构
    //这里实例化了构造函数的每一个注入类参数
    $objArr[] = new $item->name;
}
$cpObj = $cp->newInstanceArgs($objArr);
$cpObj->assemble();


上面是构造函数依赖注入类实例化的代码。如果是除了构造函数外的其它函数依赖注入了其它类呢?类程序代码如下所示:

class Computer
{
public function putTogether(Keyboard $keyboard, Mouse $mouse, Screen $screen){
$keyboard->prepare();
$mouse->prepare();
$screen->prepare();
echo "OK, the work of PUTTING TOGETHER a computer has been finished<br>";
}
}

class Keyboard
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

class Mouse
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

class Screen
{
public function prepare(){
$name = strtolower(self::class);
echo "prepare ".$name."...<br>";
}
}

实现代码如下:

$m = new ReflectionMethod("Computer", "putTogether");
$argu = $m->getParameters();
$objArr = [];
foreach($argu as $item){
$objArr[] = new $item->name;
}
$cpObj = new $m->class;
$m->invokeArgs($cpObj, $objArr);


跟构造函数的那种相比较,就是ReflectionMethod无法实例化Computer类,所以需要如下这行代码来实例化Computer类:

$cpObj = new $m->class;


OK,依赖注入类的自动实例化,我们已经知道如何实现了。现在让我们回到Laravel框架,看看它是在哪里完成的这些工作。

为了减小篇幅,这里就只分析Controller的构造函数里的注入参数是怎么实例化的,也就是上面一开始我粘贴的部分框架代码里的Request类和UserRepository类。


由于请求走到Controller控制器之前还做了很多工作,我打算再另开一篇专门分析请求的执行过程,这里也就大概提一下。


为了方便追踪代码,建议大家安装xdebug,并在PhpStorm里配置好,方便看程序执行的流程。

配置好了,然后用Postman调用接口触发调试机制。以下是我本地php关于xdebug的配置,仅供参考:

[xdebug]
zend_extension="E:/wamp64/bin/php/php7.2.18/zend_ext/php_xdebug-2.7.2-7.2-vc15-x86_64.dll"
xdebug.remote_enable = 1
xdebug.profiler_enable = on
xdebug.profiler_enable_trigger = on
xdebug.profiler_output_name = cachegrind.out.%t.%p
xdebug.profiler_output_dir ="E:/wamp64/tmp"
xdebug.show_local_vars=1
xdebug.auto_trace = On
xdebug.idekey = "PHPSTORM"
xdebug.remote_port=9000

postman触发调试,在接口里加上如下参数即可,其中PHPSTORM关键词是取自于上面的配置idekey。

XDEBUG_SESSION_START=PHPSTORM

更加具体的,建议大家百度喽。


本文是翟码农个人博客蓝翟红尘里php分类下的关于laravel源码解析系列的文章,转载请注明出处:http://www.zhai14.com/blog/09968919756f9e0a8b7280845a4e96f4.html


ok,让我们开始分析代码吧。

从public/index.php开始,实例化了一个Kernel类,主要执行handle方法。

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);


handle方法里又会执行sendRequestThroughRouter方法,sendRequestThroughRouter方法代码如下:

//Illuminate\Foundation\Http\Kernel.php
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);

Facade::clearResolvedInstance('request');

$this->bootstrap();

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

then方法是主要处理中间件执行流程的,中间件执行完后将会接着执行dispatchToRouter方法的,从这个方法开始,后面就是把请求引向控制器Controller的工作。


dispatchRouter方法代码如下所示:

//Illuminate\Foundation\Http\Kernel.php
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);

return $this->router->dispatch($request);
};
}

然后就将流程引导Illuminate\Routing\Router.php里的Router类去了,这里代码就不贴了,执行过程大概如下:

dispatchToRoute-》runRoute-》runRouteWithinStack-》$route->run()

其中$route->run()将流程又引导到Illuminate\Routing\Route.php(跟上面不是同一个文件,一个Router,一个Route)里去了。


$route->run()方法代码如下:

//Illuminate\Routing\Route.php
public function run()
{
$this->container = $this->container ?: new Container;

try {
if ($this->isControllerAction()) {
return $this->runController();
}

return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}

这里就出现Controller控制器这个词了,看来曙光就要来临了,让我们继续前进。


在当前文件的runController方法里,有调用getController方法。

getController方法代码如下:

public function getController()
{
    if (! $this->controller) {
        $class = $this->parseControllerCallback()[0];

        $this->controller = $this->container->make(ltrim($class, '\\'));
    }

    return $this->controller;
}

可以看出,又接着调用container的make方法去了。


借助PhpStorm IDE,鼠标单击make,它把我们带到Illuminate/Container/Container.php这个文件来了。

由make方法追到resolve方法,由resolve方法,再追到build方法,就可以看到我上面讲的反射相关的代码了。

//Illuminate/Container/Container.php
public function build($concrete)
{
   
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    $reflector = new ReflectionClass($concrete);

    
    if (! $reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $this->buildStack[] = $concrete;

    $constructor = $reflector->getConstructor();

    
    if (is_null($constructor)) {
        array_pop($this->buildStack);
        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);
        throw $e;
    }

    array_pop($this->buildStack);
    return $reflector->newInstanceArgs($instances);
}

这里传进来的$concrete就是路由里的控制器controller的类名称,$dependencied就是获取控制器的构造函数里的参数,追踪后面的resolveDependencies方法,你会发现追到resolveClass方法,里面主要是下面这行代码:

$this->make($parameter->getClass()->name);

跟上面自我实现的程序对比,上面的make方法,无非就是相当于foreach遍历参数时new $item->name这块。


这和追踪Controller进到的make方法一样,都是根据类名称来实例化类,只不过此处实例化的是控制器构造函数的那些参数,也正是本篇文章要分析的目标:UserController的构造函数的参数Request类和UserRepository类是如何自动实例化的。




  • 2019年10月23日 12:09文章创建
  • 2019年11月08日 23:54文章发布
我要评论
«-必填,限2-20个字符,中文/字母/字母数字组合
«-评论后,邮箱会收到激活链接,未激活邮箱的留言,将无法显示
评论列表
暂无评论,期待你的评论哦!
回到顶部