本文由 简悦 SimpRead 转码, 原文地址 xie.infoq.cn

引言

学习 laravel 而不了解容器的知识,那谈不上会 laravel。本文从一个 laravel 的初学者角度,一步一步了解容器是在什么样的场景下产生的,以及 laravel 中是如何使用容器的。在看本文之前,如果有反射和匿名函数的基础,会更容易理解

一、控制反转 (Ioc)、依赖注入 (DI)

学习 laravel 的容器,首先需要了解,依赖注入 (Dependency Injection) 和控制反转 (Inversion of Control) 这两个概念,以及他们的关系。

依赖注入是控制反转的一种实现方式

先了解一下什么是控制反转。当调用者需要被调用者的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例,但在这里,创建被调用者的工作不再由调用者来完成,而是将被调用者的创建移到调用者的外部,从而反转被调用者的创建,消除了调用者对被调用者创建的控制,因此称为控制反转

上边的文字描述有点抽象,下边看一个场景,帮助我们了解控制反转。假设用户登录的时候,系统会提供记录登录日志的功能,可以选择使用【文件】或者【数据库】的方式来记录日志。实现方案如下:

  1. <?php
  2. // 定义写日志的接口规范
  3. interface Log
  4. {
  5. public function write();
  6. }
  7. // 文件记录日志
  8. class FileLog implements Log
  9. {
  10. public function write(){
  11. echo 'file log write...'.PHP_EOL;
  12. }
  13. }
  14. // 数据库记录日志
  15. class DatabaseLog implements Log
  16. {
  17. public function write(){
  18. echo 'database log write...'.PHP_EOL;
  19. }
  20. }
  21. // 程序操作类
  22. class User
  23. {
  24. protected $fileLog;
  25. public function __construct()
  26. {
  27. //在调用者(User)中创建被调用者的实例
  28. $this->fileLog = new FileLog();
  29. }
  30. public function login()
  31. {
  32. // 登录成功,记录登录日志
  33. echo 'login success...'.PHP_EOL;
  34. $this->fileLog->write();
  35. }
  36. }
  37. $user = new User();
  38. $user->login();

上边的写法可以实现通过文件的方式记录日志,【假设现在需要通过数据库的方式记录日志的话】我们就需要修改 User 类,这样的话,代码就没达到解耦合的目的

要实现控制反转,通常的解决方案是将创建被调用者实例的工作交由 IoC 容器来完成,然后在调用者中注入被调用者(通过构造器 / 方法注入实现),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入 依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合(loose coupled)、可维护、可测试的代码和程序 。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程

现在我们按照上边说的【依赖注入】的方式,对 User 类进行修 (在调用者中注入被调用者 (通过构造器 / 方法注入实现)),这样我们就实现了调用者与被调用者的解耦,该过程被称为依赖注入。):


class User 
{
    protected $log;
    //这样写其实是有问题的,因为Log是接口类型,不能被实例化,后边会通过绑定的方式,解决这个问题
    public function __construct(Log $log)
    {
        $this->log = $log;   
    }

    public function login()
    {
        // 登录成功,记录登录日志
        echo 'login success...';
        $this->log->write();
    }

}

$user = new User(new DatabaseLog());//通过这种,在调用者中注入被调用者,能帮助我们解耦
$user->login();

刚开始接触 laravel 的时候,就特别好奇很多对象实例通过方法的参数定义就能传进来,而调用的时候也不需要我们手动的去传入,比如说 Request,所以这个时候就需要知道 larave 是怎么实现的。要想了解 laravel 是怎么实现的,需要先了解 php 中的反射,因为 laravel 容器的实现借助了反射,所以先大致介绍一下反射

二、反射

反射的概念其实可以理解成根据类名返回该类的任何信息,比如该类有什么方法,参数,变量等等。反射官方文档:https://www.php.net/manual/zh/book.reflection.php

就拿刚才的 User 类来举例:


// 获取User的reflectionClass对象
$class = new reflectionClass(User::class);

// 拿到User的构造函数
$constructor = $class->getConstructor();

// 拿到User的构造函数的所有依赖参数
$dependencies = $constructor->getParameters();

//创建user对象
$user = $reflector->newInstance();

// 创建user对象,需要传递参数的
$user = $reflector->newInstanceArgs($dependencies = []);

现在创建一个 make 方法,将 User 类的名字作为参数传给 make 方法,在 make 中通过反射机制拿到 User 的构造函数,进而得到构造函数的参数对象,然后通过递归的方式创建参数的依赖,最后就是通过 newInstanceArgs 方法生成 User 实例:


//我们这里需要修改一下User的构造函数,如果不去修改,反射是不能动态创建接口的,如果非要用接口,后边会通过Ioc容器去解决
class User 
{
    protected $log;

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

    public function login()
    {
        // 登录成功,记录登录日志
        echo 'login success...';
        $this->log->write();
    }

}

function make($concrete){

$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
// 为什么这样写的? 主要是递归。比如创建FileLog不需要传入参数。
if(is_null($constructor)) {
    return $reflector->newInstance();
}else {
// 构造函数依赖的参数
$dependencies = $constructor->getParameters();
// 根据参数返回实例,如FileLog
$instances = $this->getDependencies($dependencies);
return $reflector->newInstanceArgs($instances);
 }

}

function getDependencies($paramters) {
    $dependencies = [];
    foreach ($paramters as $paramter) {
        $dependencies[] = make($paramter->getClass()->name);
    }
    return $dependencies;
}

$user = make('User');
$user->login();

如果不熟悉反射,上边这段代码可能有点难理解,但是如果啃明白了,会觉得特别有意思,成就感满满!上边介绍了依赖注入、控制反转、反射,下边进入本文重点,Ioc 容器

三、IoC 容器和服务提供者

我们上边通过反射的方式,其实还没有达到解耦的目的,假如现在要换别的方式记录日志,还是需要修改 User。现在我们就借助容器来实现真正的解耦

先借助一个容器,提前将 log、user 都绑定到 Ioc 容器中。然后 User 的创建就交给容器去做

实现思路:

1、IoC 容器维护 binding 数组记录 bind 方法传入的键值对如: log=>FileLog, user=>User。也就是说我们提前把我们需要用到的类,都绑定到这个数组中,并给类一个别名

2、在 ioc->make(‘user’) 的时候,通过反射拿到 User 的构造函数,拿到构造函数的参数,发现参数是 User 的构造函数参数 log, 然后根据 log 得到 FileLog。

3、这时候我们只需要通过反射机制创建 $filelog = new FileLog();

4、通过 newInstanceArgs 然后再去创建 new User($filelog);

大致长下边这样:


//实例化ioc容器
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();

这个容器就指 Ioc 容器,这个 User 可以理解成服务提供者。上边说到了,如果 User 的构造函数参数是接口该如何处理,其实就是通过 Ioc 容器提前绑定好

核心实现代码:


interface log
{
    public function write();
}

// 文件记录日志
class FileLog implements Log
{
    public function write(){
        echo 'file log write...';
    }
}

// 数据库记录日志
class DatabaseLog implements Log
{
    public function write(){
        echo 'database log write...';
    }
}

class User
{
    protected $log;
    public function __construct(Log $log)
    {
        $this->log = $log;
    }
    public function login()
    {
        // 登录成功,记录登录日志
        echo 'login success...';
        $this->log->write();
    }
}
class Ioc
{
    public $binding = [];

    public function bind($abstract, $concrete)
    {
        //这里为什么要返回一个closure呢?因为bind的时候还不需要创建User对象,所以采用closure等make的时候再创建FileLog;
        $this->binding[$abstract]['concrete'] = function ($ioc) use ($concrete) {
            return $ioc->build($concrete);
        };

    }

    public function make($abstract)
    {
        // 根据key获取binding的值
        $concrete = $this->binding[$abstract]['concrete'];
        return $concrete($this);
    }

    // 创建对象
    public function build($concrete) {
        $reflector = new ReflectionClass($concrete);
        $constructor = $reflector->getConstructor();
        if(is_null($constructor)) {
            return $reflector->newInstance();
        }else {
            $dependencies = $constructor->getParameters();
            $instances = $this->getDependencies($dependencies);
            return $reflector->newInstanceArgs($instances);
        }
    }

    // 获取参数的依赖
    protected function getDependencies($paramters) {
        $dependencies = [];
        foreach ($paramters as $paramter) {
            $dependencies[] = $this->make($paramter->getClass()->name);
        }
        return $dependencies;
    }

}

//实例化IoC容器
$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');
$user = $ioc->make('user');
$user->login();

现在不需要关心是用什么方式记录日志了,哪怕后期需要修改记录日志的方式,只需要在 ioc 容器修改绑定其他记录方式日志就行了

那么 laravel 中的服务容器和服务提供者长啥样呢?

可以在 config 目录找到 app.php 中 providers, 这个数组定义的都是已经写好的服务提供者


$providers = [
    Illuminate\Auth\AuthServiceProvider::class,
    Illuminate\Broadcasting\BroadcastServiceProvider::class,
    Illuminate\Bus\BusServiceProvider::class,
    Illuminate\Cache\CacheServiceProvider::class,
    ...
]
...
// 随便打开一个类比如CacheServiceProvider,这个服务提供者都是通过调用register方法注册到ioc容器中,其中的app就是Ioc容器。singleton可以理解成我们的上面例子中的bind方法。只不过这里singleton指的是单例模式。

class CacheServiceProvider{
    public function register()
    {
        $this->app->singleton('cache', function ($app) {
            return new CacheManager($app);
        });

        $this->app->singleton('cache.store', function ($app) {
            return $app['cache']->driver();
        });

        $this->app->singleton('memcached.connector', function () {
            return new MemcachedConnector;
        });
    }
}

具体服务提供者 register 方法是什么时候执行的,后边会大致说一下 Laravel 的生命周期

三、Facade 外观模式的原理

我们经常会在 laravel 中通过这样的方式来调用方法:

User::query()->where()

这种写法要比我们刚才需要先通过 $ioc->make(‘user’) 拿到 User 的实例,然后再使用 $user->login()

那上边那种简单的方式是如何实现的呢?

Facade 的工作原理:

1、定义一个服务提供者的外观类,在该类中定义一个容器类的变量,跟 ioc 容器绑定的 key 一样,

2、通过静态魔术方法__callStatic 可以得到当前想要调用的 login

3、使用 static::$ioc->make(‘user’);

现在通过这种外观类的方式去修改一下我们上边的那个记录日志的类(即给 User 类写一个外观类):


class UserFacade
{
    // 维护Ioc容器
    protected static $ioc;

    public static function setFacadeIoc($ioc)
    {
        static::$ioc = $ioc;
    }

    // 返回User在Ioc中的bind的key
    protected static function getFacadeAccessor()
    {
        return 'user';
    }

    // php 魔术方法,当静态方法被调用时会被触发
    public static function __callStatic($method, $args)
    {
        $instance = static::$ioc->make(static::getFacadeAccessor());
        return $instance->$method(...$args);
    }

}

$ioc = new Ioc();
$ioc->bind('log','FileLog');
$ioc->bind('user','User');

UserFacade::setFacadeIoc($ioc);

UserFacade::login();

可能大家感觉加了这个 User 的外观类更加麻烦了,需要注入容器,还需要使用魔术方法,其实 laravel 在运行的时候都将这些工作做好了。我们直接使用 UserFacade::login() 就可以了。最主要的就是 Facade 提供了简单易记的语法,从而无需配置长长的类名。像 laravel 中的 Redis、Log 等都用的是这种外观模式

四、Laravel 的生命周期

要研究 laravel 的生命周期,肯定是要看入口文件的


// 定义了laravel一个请求的开始时间
define('LARAVEL_START', microtime(true));

// composer自动加载机制
require __DIR__.'/../vendor/autoload.php';

//这句话你就可以理解laravel,在最开始引入了一个ioc容器。
$app = require_once __DIR__.'/../bootstrap/app.php';

打开__DIR__.'/../bootstrap/app.php';你会发现这段代码,绑定了Illuminate\Contracts\Http\Kernel::class,这个你可以理解成之前我们所说的$ioc->bind();方法。

// 这个相当于我们创建了Kernel::class的服务提供者
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

// 获取一个 Request ,返回一个 Response。以把该内核想象作一个代表整个应用的大黑盒子,输入 HTTP 请求,返回 HTTP响应。
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);

// 就是把我们服务器的结果返回给浏览器。
$response->send();

// 这个就是执行我们比较耗时的请求,
$kernel->terminate($request, $response);

上边其实还是比较抽象的,在网上看见一张把 laravel 的生命周期画的非常清楚的图,分享给大家:

吃透 Laravel 的 Ioc 容器 - 图1

我觉得了解了在 laravel 中容器是个什么之后,对后边更深入的学习 laravel 是非常有帮助的,希望大家看完之后能真正的有所收获!

吃透 Laravel 的 Ioc 容器 - 图2