吃透 Laravel 的 Ioc 容器

用户头像
书旅
关注
发布于: 2020 年 08 月 19 日
吃透Laravel的Ioc容器

引言

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



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

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



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



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



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

<?php
// 定义写日志的接口规范
interface Log
{
public function write();
}
// 文件记录日志
class FileLog implements Log
{
public function write(){
echo 'file log write...'.PHP_EOL;
}
}
// 数据库记录日志
class DatabaseLog implements Log
{
public function write(){
echo 'database log write...'.PHP_EOL;
}
}
// 程序操作类
class User
{
protected $fileLog;
public function __construct()
{
//在调用者(User)中创建被调用者的实例
$this->fileLog = new FileLog();
}
public function login()
{
// 登录成功,记录登录日志
echo 'login success...'.PHP_EOL;
$this->fileLog->write();
}
}
$user = new User();
$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中容器是个什么之后,对后边更深入的学习laravel是非常有帮助的,希望大家看完之后能真正的有所收获!





发布于: 2020 年 08 月 19 日 阅读数: 27
用户头像

书旅

关注

公众号:IT猿圈 2019.04.11 加入

还未添加个人简介

评论

发布
暂无评论
吃透Laravel的Ioc容器