Container作为Laravel框架的核心功能支持,理解服务容器对阅读Laravel源码和追踪框架问题都有极大的提升。
文档上称Container为『服务容器』,顾名思义通过维护一个类容器来实现类的IoC控制反转。
ioc实例
在解读源码前需要了解IoC。Ioc全称Inversion of Control,意为控制反转。 这里用一个最常见的支付场景来举例。
//支付类
class Pay{
private $payway;
function __construct($payway){
$this->payway = $payway;
}
function payTo($someone){
$this->payway->pay($someone);
}
}
//具体支付sdk
class AliPay{
function pay($someone){
echo '支付宝发起支付给'.$someone;
}
}
//实例化支付宝sdk
$alipay = new AliPay();
//实例化支付类 传入支付宝sdk
$pay = new Pay($alipay);
//发起支付
$pay->payTo('someone');
在上面的代码中,支付逻辑依赖于支付宝的sdk类。需要将支付宝类作为初始化参数算入支付类,这样的弊端是当有一天我们需要换支付sdk将不得不更改逻辑代码。
控制反转就是为了解决这样的问题。解耦类之间的依赖关系。由下游的使用方来决定引入什么依赖。来看下使用实例
//支付类
class Pay{
private $payway;
//注意这里 声明了传入的数据类型
function __construct(AliPay $payway){
$this->payway = $payway;
}
function payTo($someone){
$this->payway->pay($someone);
}
}
//具体支付sdk
class AliPay{
function pay($someone){
echo '支付宝发起支付给'.$someone;
}
}
//获取一个服务容器
$c = Container::getInstance();
//生成支付实例
$t = $c->make(Pay::class);
//发起支付
$t -> payTo(11);
在上面的代码里通过服务容器实例化了一个支付类后,就可以直接调用支付方法而不需要手动传入依赖。
这里就用到了Container中的一个核心方法make。稍后再根据源码详细解读。
这里看上去不用直接传入依赖但是又引出另外一个问题,这个代码永远只能使用Alipay类作为自动依赖注入。
解决这个问题的方法是使用接口。约定好传入Pay类内所需的支付SDK必须实现的方法。只要实现了该接口的类都可以作为支付SDK传入支付类。
我们来看看使用服务容器的写法:
interface Payway{
//需要实现pay方法
function pay($someone);
}
//支付类
class Pay{
private $payway;
function __construct(Payway $payway){
$this->payway = $payway;
}
function payTo($someone){
$this->payway->pay($someone);
}
}
//具体支付sdk
class AliPay implements Payway {
function pay($someone){
echo '支付宝发起支付给'.$someone;
}
}
//获取一个服务容器
$c = Container::getInstance();
//在容器内绑定 Alipay 到 Payway接口
$c->bind(Payway::class,AliPay::class);
//实例化Pay
$t = $c->make(Pay::class);
$t -> payTo(11);
在上面的代码中。我们定义了一个Payway接口来作为Pay传入的依赖类。然后通过服务容器将AliPay 绑定到 Payway接口。
最后直接实例化Pay类就可以是实现调用。此时已经完美的解耦了Pay与AliPay类的依赖关系。
这里使用了Container中的另一个核心方法bind。
源码解读
通过上面的完整代码演示我们知道了实现控制反转的整个流程依赖了服务容器内的2个核心方法make()与bind();
make()
make函数用来产生接口或者类的实例化对象
【划重点】。
摘录 make()函数的部分核心代码
public function make($abstract, array $parameters = [])
{
$abstract = $this->getAlias($this->normalize($abstract));
// 如果容器内已经存在该实例 则直接返回
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 从容器内的绑定关系(bind方法)中找到接口对应的可实例化类
$concrete = $this->getConcrete($abstract);
//找的这个可实例化类是否可以被实例化?
if ($this->isBuildable($concrete, $abstract)) {
//执行实例化过程
$object = $this->build($concrete, $parameters);
} else {
//递归make函数继续查找容器内的绑定关系中找到接口对应的可实例化类
$object = $this->make($concrete, $parameters);
}
}
从这里我们可以看到make函数是用来找到容器内的接口绑定的将可实例化类传入build函数进行实例化。
build()
build函数函数用来产生类的实例化对象,然后将反射出该类的构造函数依赖类实例化后传入构造函数。
摘录build()函数的部分核心代码
public function build($concrete, array $parameters = [])
//如果是闭包 直接执行闭包后返回
if ($concrete instanceof Closure) {
return $concrete($this, $parameters);
}
//获取类的反射对象
$reflector = new ReflectionClass($concrete);
//解析出该类构造函数所需参数
$dependencies = $constructor->getParameters();
//将构造参数里存在指定数据类型的参数 调用make方法产生实例对象
$instances = $this->getDependencies(
$dependencies, $parameters
);
//将参数内实例化的对象传入构造函数 并实例化对象
return $reflector->newInstanceArgs($instances);
build最关键的部分在于解析构造参数并予以实例化。想深入理解的话看下getDependencies方法。
bind()
bind方法用来将接口与具体类的绑定关系表保存在服务容器内。
摘录bind()函数的部分核心代码
public function bind($abstract, $concrete = null, $shared = false)
{
//如果待绑定的类不是闭包类型 则转换为闭包类型
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
//将待实例化类 传入 容器绑定关系表
//shared指定是否唯一实例
$this->bindings[$abstract] = compact('concrete', 'shared');
}