概述
官方文档中关于服务容器的介绍,主要是针对于这个服务容器提供的绑定,解析的语法上。是建立在读者对服务容器,依赖注入,控制反转有一定的认知的基础上进行说明的。本篇注解的主要目的就是在官方文档的基础上,补充上服务容器设计上的的内容,便于理解 Laravel 提供的语法。
Laravel 的应用 Application 的实现就是一个服务容器,目的是用来管理 Laravel 框架中各种对某些对象的依赖关系。体现的设计思想是控制反转。目的是尽可能降低类对象间的耦合度。我们一步步的看。
一个基本的服务容器
先搞清楚服务的概念,不要想多了,任何一个功能,任务都可以叫做服务 service。所以说功能类对象,就是服务。
再看容器,把这些服务装在一起,装在哪?就是一个容器。其实就是一个可以找到这些服务的一个对象。
通常一个容器要具有绑定和解析两个操作。
- 绑定,指的是将获取服务对象的方法在容器中进行注册。相当于将服务装入到了容器中。
- 解析,指的是将绑定到容器中的服务从容器中提取出来,注意通常我们绑定的不是对象本身,而是生成对象的代码,因此解析时通常是执行代码来得到对象。
看代码演示,本段代码与 Laravel 无关:可以在 github 上下载得到示例代码:
# 1, 服务容器定义
/**
* Class Application
* 服务容器类,类名参考Laravel
*/
class Application
{
// 已绑定(注册)的服务
private $services = [];
/**
* 绑定(注册)
* @param $class string
* @param $generator Closure
*/
public function bind($class, $generator)
{
$this->services[$class] = $generator;
}
/**
* 解析
* @param $class string
*/
public function make($class)
{
return call_user_func($this->services[$class]);
}
}
# 2, 服务类示例
/**
* Class Kernel
* 内核服务类
*/
class Kernel
{
}
/**
* Class Request
* 请求服务类
*/
class Request
{
}
# 3, 绑定服务到容器,通常在程序初始化阶段完成
$app = new Application();
$app->bind('Kernel', function() {
return new Kernel();
});
$app->bind('Request', function() {
return new Request();
});
# 4, 需要时从容器中解析
$kernel = $app->make('Kernel');
var_dump($kernel);
$request = $app->make('Request');
var_dump($request);
观察上面的代码,Application 类就是服务容器类,实例化的 $app 就是服务容器对象。服务都绑定在这个容器上。
Kernel 和 Request 就是具体的某个服务。别忘了任何功能都可以是服务。
$app->bind (),就是将服务绑定到容器中,注意,绑定的是对象生成代码,而不是对象本身。因此绑定时,并没有去实例化对象。
$app->make (),就是从服务容器中解析服务,其实就是调用对应类的生成代码,得到对应的服务对象。
以上就是一个基本的服务容器。提供服务容器这种架构的目的,就是将项目中各种各样复杂多样,需要复用的功能整理到一起来管理。
Laravel 的服务容器
有了基本的服务容器概念,Laravel 文档中本章的大部分就可以看懂了。服务容器这篇文档中,大部分内容都在说,laravel 的服务容器提供了哪些绑定和解析的相关方法。我们先看看 Laravel 服务容器的实现代码。
服务容器实现
Laravel 中的服务容器主要由 Illuminate\Foundation\Application 和其父类 Illuminate\Container\Container 来实现。其中 Illuminate\Container\Container 类实现了一个容器应该有的绑定,解析等功能,而 Illuminate\Foundation\Application 类继承 Container,完成了基础服务的绑定,和一些基础的初始化操作。
可以看出,容器的和新方法都是 Container 实现。Application 利用该容器完成了初始绑定的一些列工作。还可以参考代码,Illuminate\Foundation\Application::__construct() 来看看初始化绑定的内容:
# 构造方法
public function __construct($basePath = null)
{
if ($basePath) {
$this->setBasePath($basePath);
}
// 注册基础绑定
$this->registerBaseBindings();
// 注册基础服务容器
$this->registerBaseServiceProviders();
// 注册核心容器别名
$this->registerCoreContainerAliases();
}
再看看基础绑定都有些啥,参考代码 Illuminate\Foundation\Application::registerBaseBindings():
protected function registerBaseBindings()
{
static::setInstance($this);
// 将$app容器对象绑定到容器,标识为app
$this->instance('app', $this);
// 将$app容器对象绑定到容器,标识为Container
$this->instance(Container::class, $this);
}
看一下这个方法的原因,是你要留意,Laravel 的服务容器,将服务容器本身也作为服务,绑定在容器中。这个比较狠…
还有一个方法:registerBaseServiceProviders () 注册基本的服务提供者。这个服务提供者,其实就是用来完成服务绑定的独立功能类,我们在《服务提供者 - 注》中再详细讨论。
回头看看,Laravel 提供的服务容器,功能更多,适应场景也就更多。但本质不要迷失了!
绑定语法索引
Laravel 实现的服务容器,提供了多种绑定方法,核心思路都是一样的绑定,提供了不同的效果支持,下面做一个索引。具体请参考官方文档。
解析语法索引
批量绑定解析
Laravel 还提供了批量解析的方法。思路是,为多个绑定的服务,打上相同的标签,然后通过标签标识进行批量解析,用到的方法:
// 绑定服务
$this->app->bind('SpeedReport', function () {});
$this->app->bind('MemoryReport', function () {});
// 打标签
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
// 批量解析
$app->tagged('reports');
自动依赖注入
<?php
namespace App\Http\Controllers;
use App\Users\Repository as UserRepository;
class UserController extends Controller
{
/**
* 用户存储库实例。
*/
protected $users;
/**
* 创建一个新的控制器实例。
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users) # 注意参数
{
$this->users = $users;
}
}
上面代码中,__construct 构造方法中,可以直接使用 UserRepository 类的对象了,不用再去实例化等操作。也就是在构造方法执行时,UserRepository 类的对象已经实例化好,并传递(注入)到了构造方法中。这种语法,就称之为自动依赖注入。
我们分两步来理解:1,依赖注入。2,自动注入。
依赖注入
依赖注入,DI, Dependency Injection,指的是将依赖的资源由外部注入到方法内部。是相对于在方法内部实例化来说的。说白了就是在调用方法前,将对象(或其他资源)准备好了,再传递给方法。就叫依赖注入。
这么做的目的是什么呢?
解耦。如果在方法内部去实例化一个对象,那么该方法就和这个对象完全紧耦合在一起了。而如果在方法外去实例化对象,这个对应就和方法没有直接关系了,也就是降低了对象和方法间的耦合度。举个例子,如果一个方法需要一个日志处理对象,如果在方法内部实例化了一个 LogFile 对象,这样当我们需要其他方式处理日志例如 LogDB 处理日志时,这个方法就好重写。而如果采用注入依赖的方式,方法本身不会发生改变。仅仅是在调用前注入时选择不同的 Log 对象即可。这就是 DI 的目的,降低耦合度。
在 Laravel 中,控制器、事件监听器、队列任务、中间件都支持依赖注入。
自动注入
自动注入,就是自动依赖注入,是 laravel 中最常用的依赖对象注入方式。语法上,就是通过参数的类型约束来确定参数的类型,实现自动化注入。
实现自动依赖注入通常需要反射机制来确定参数类型,然后从服务容器中解析需要的服务,最后作为实参传递到方法中。
还是使用我们的代码进行演示:
# 5, 需要注入依赖的控制器动作
class Controller
{
// 依赖于Request对象
public function show(Request $request)
{
var_dump($request);
}
}
# 6, 自动依赖注入
$class = new ReflectionClass('Controller');
$method = $class->getMethod('show');
// 全部参数类型
$arguments = [];
foreach($method->getParameters() as $parameter) {
$type = $parameter->getClass()->getName();
$arguments[] = $app->make($type);
}
// 调用方法,同时注入依赖对象
$method->invokeArgs($class->newInstance(), $arguments);
代码是继续上面的服务容器的测试代码。
一个控制器动作,依赖 Request 对象。通过反射调用该方法,之前先判断参数类型,从服务容器中解析对象,然后注入到方法中完成调用。
以上就是一个基本的自动注入依赖的演示。
在 Laravel 中,建议使用这种方式来完成,可以简洁化我们的代码,优雅!
控制反转
最后再说一个概念,控制反转,IoC, Inversion of Control。IoC 控制反转是一种设计模式,用来解决对象间的过度依赖问题。解决思路是,设法不在依赖对象中去获取 (new) 被依赖对象。最典型的实现方式就是 DI 依赖注入了.