laravel 源码 - 服务容器
- IoC 容器理解
- 1 问题的产生
- 2 依赖
- 3 容器的出现
- Laravel 中的容器
- 1 bind 绑定
- 1.1 加装闭包
- 1.2 注册
- 1.3 回调
- 2 make 解析
- 2.1 获取注册的实现
- 2.2 build 解析
Laravel 服务容器 是一个用于管理类依赖和执行依赖注入的强大工具,该容器提供了整个框架中需要的一系列服务 。
容器:字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。
IoC 容器理解
有这样一种容器,它存放的不是文本、数值,而是对象、对象的描述(类、接口)或者是提供对象的回调,通过这种容器(IOC 容器),我们得以实现许多高级的功能,其中最常提到的,就是 “解耦” 、“依赖注入(DI)”。
面向对象编程 有以下几样东西无时不刻的接触: 接口 、 类 还有 对象 。这其中,接口是类的原型,一个类必须要遵守其实现的接口;对象则是一个类实例化后的产物,我们称其为一个实例。他们紧密相连,如若处理不好则会牵一发而动全身,下面就结合例子展现问题并引入容器。
1 问题的产生
“小白,一个家境一般的应届毕业生。目前他最大的问题是找工作,由于小白非常爱学习,上学期间很少接触电子产品也不懂人情世故,所以找工作也没有接触招聘网站以及人才市场等,最后也只有拿着简历举着牌子在天桥上跟别人抢地盘了 ”:
小白开始找工作
<?php
/**
* Class Student
* 学生类
*/
class Student
{
protected $name;// 学生姓名
public function __construct($name)
{
$this->name = $name;
}
/**
* 找工作
*/
public function searchJob()
{
$job = new Job('睡觉', '10k');// 小白期望的工作,总的来说做的少赚的多
return $job;
}
}
/**
* Class Job
* 工作类
*/
class Job
{
protected $content;// 工作内容
protected $salary;// 工作薪资
public function __construct($content, $salary)
{
$this->content = $content;
$this->salary = $salary;
}
/**
* @return mixed
* 获取工作内容
*/
public function getContent()
{
return $this->content;
}
/**
* @return mixed
* 获取薪资
*/
public function getSalary()
{
return $this->salary;
}
}
$xiaoBai = new Student('小白');
$sleep = $xiaoBai->searchJob();// 试睡员
结果可想而知,工作找的并不理想。符合小白的需求的工作自己去找很难找到,所以小白想要改变方式
找工作。
2 依赖
“虽然小白不太会社交但总会由那么几个朋友,于是小白去向他的朋友请教了。小白的朋友建议他可以去人才市场看看,于是小白去人才市场找了一下工作:
为了提供更多的就业机会,让不同的公司能够找到更适合的人,找工作的人能找到更适合的公司,人才市场就产生了。人才市场是一个主要用来存放招聘者 公司名称 和 公司信息 以及 应聘者名称 和 应聘者信息 的容器,降低了应聘者和招聘者之间的耦合: ”
以上代码还产生了一个问题,“Student” 和 “Job” 产生了 “依赖”。所谓“依赖”,就是 “我若依赖你,我就不能离开你”。在一个贯彻面向对象编程的项目中,这样的依赖随处可见。少量的依赖并不会有太过直观的影响,但当依赖达到一个量级时,将会是一个噩梦般的体验。
为了减少依赖,我们不应该手动在类中固化类的初始化的行为,而转由外部负责。我们可以提供一个接口,只要所提供的部分满足这个接口的需求都可以被使用。这种由外部负责其依赖需求的行为,我们可以称其为 “ 控制反转(IoC) ”。
创建工厂类来管理工作
/**
* Class Student
* 学生类
*/
class Student
{
protected $name;// 学生姓名
protected $job;// 工作
public function __construct($name, $job, ...$mess)
{
$this->name = $name;
$jobFactory = new JobFactory();
$this->job = $jobFactory->provideWork($job, $mess);// 通过工厂提供工作
}
/**
* 找工作
*/
public function searchJob()
{
return $this->job;
}
}
/**
* Class Job
* 工作类
*/
class Job
{
protected $content;// 工作内容
protected $salary;// 工作薪资
public function __construct($content, $salary)
{
$this->content = $content;
$this->salary = $salary;
}
/**
* @return mixed
* 获取工作内容
*/
public function getContent()
{
return $this->content;
}
/**
* @return mixed
* 获取薪资
*/
public function getSalary()
{
return $this->salary;
}
}
/**
* Class JobFactory
* 提供工作的工厂
*/
class JobFactory
{
/**
* 提供工作 可以添加
*/
public function provideWork($name, $job)
{
var_dump($job);
switch ($name) {
case 'Job':
return new Job(...$job);
break;
// case 'Test1': return new Test1($options[0]);
// case 'Test2': return new Test2($options[0], $options[1],$options[2]);
}
}
}
$xiaoBai = new Student('小白','Job','睡觉','10k');
var_dump($xiaoBai->searchJob());
以上代码依赖并未解除,只是由原来对不同工作的依赖变成了对一个工厂的依赖,假如工厂出了点麻烦,问题变得就很麻烦。
大多数情况下,工厂模式已经足够了。工厂模式的缺点就是:接口未知(即没有一个很好的契约模型)、产生对象类型单一。总之就是,还是不够灵活。虽然如此,工厂模式依旧十分优秀,并且适用于绝大多数情况。
依赖注入:只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动new 的),而是由外部以参数或其他形式注入的,都属于 依赖注入(DI) 。
依赖注入:是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;
控制反转:是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
3 容器的出现
“小白虽然是一个应届毕业生,但是对工作还是有要求的,首先想要找本专业的工作,这样我们要求有统一的接口,这样才能和小白需求的工作对接(可以通过接口实现)。另外上面的人才市场还是会有相对来说比较大的依赖,我们需要继续降低小白和其他的耦合,管理工作的方式还需继续改进。”
容器:
/**
* Class Recruitment
* 招聘网站:IOC 容器
*/
class Recruitment
{
protected $bindings = [];# 存放客户信息
/**
* 注册用户信息
*/
public function bind($name, $concrete)
{
$this->bindings[$name] = $concrete;
}
/**
* 提供所需要客户信息
*/
public function make($abstract)
{
return ($this->bindings[$abstract])();
}
}
结合容器完成小白的找工作:
# IOC
/**
* Class Student
* 学生类
*/
class Student
{
protected $job;// 容器实例
protected $name;// 学生姓名
public function __construct(Job $job, $name)
{
$this->job = $job;
$this->name = $name;
}
/**
* 找工作
*/
public function searchJob()
{
return $this->job;
}
}
/**
* Interface Jobs
* 工作接口
*/
interface Jobs
{
public function getContent();// 获取工作内容
public function getSalary();// 获取薪资
}
/**
* Class Job
* 工作类
*/
class Job implements Jobs
{
protected $content;// 工作内容
protected $salary;// 工作薪资
public function __construct($content, $salary)
{
$this->content = $content;
$this->salary = $salary;
}
/**
* @return mixed
* 获取工作内容
*/
public function getContent()
{
return $this->content;
}
/**
* @return mixed
* 获取薪资
*/
public function getSalary()
{
return $this->salary;
}
}
/**
* Class Recruitment
* 招聘网站
*/
class Recruitment
{
protected $bindings = [];# 存放客户信息
/**
* 注册用户信息
*/
public function bind($name, $concrete)
{
$this->bindings[$name] = $concrete;
}
/**
* 提供工作
*/
public function make($abstract)
{
return ($this->bindings[$abstract])();
}
}
$ioc = new Recruitment();
// 公司注册
$ioc->bind('sleep',function(){
return new Job('睡觉', '10k');
});
// 小白注册并且招聘信息提供工作
$ioc->bind('xiaoBai',function() use($ioc){
return new Student($ioc->make('sleep'), '小白');// 这里通过容器向 小白 提供了所需的工作
});
$ioc->make('xiaoBai')->searchJob();
以上,“人才市场” 就是一个容器,用来存放客户信息。主要功能是 “ 存放(注册) ” 客户信息 和 “ 获取(解析) ” 客户信息,这也是容器的主要功能,而存放的本质就是一个数组。现在 “小白” 和 “公司” 之间就由 “人才市场” 来管理,不需要他们自己亲自去找需要的东西,并且 “ 小白 ” 在感觉这一份 “ 工作 ” 不合适之后,“ 人才市场 ” 还可以快速的提供另外一份工作,大大降低了耦合。
Laravel 中的容器
Laravel 中容器存放于 vendor\laravel\framework\src\Illuminate\Container\Container.php;没错,Laravel 框架的核心,这么重要的部分只有这一个。
1 bind 绑定
容器主要的两个作用就是注册绑定和解析,这里我们先看看绑定(以 bind方法 为例):
以下没有特别提到都存在于 Container 类中。
以下存放信息的数组可能不止一个,除了存储正常的用户信息外还有可能因为招到合适的人而改变,所以容器中还应该有其他的数组来存放这些已经改变状态的用户,在 Laravel 中主要是 $bindings,$aliases,$instances 等几个数组。
/**
* The container's bindings.
*
* @var array[]
*/
protected $bindings = [];
/**
* The registered type aliases.
*
* @var string[]
*/
protected $aliases = [];
/**
* The container's shared instances.
*
* @var object[]
*/
protected $instances = [];
/**
* Register a binding with the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);// 去除原有注册
// If no concrete type was given, we will simply set the concrete typeto the
// abstract type. After that, the concrete type to be registered asshared
// without being forced to state their classes in both of theparameters.
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrapit
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) { // 加装闭包
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');// 注册
// If the abstract type was already resolved in this container we'llfire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {// 回调
$this->rebound($abstract);
}
}
从源码中我们可以看出,服务器的绑定有如下几个步骤:
- 去除原有注册:去除当前绑定接口的原有实现单例对象,和原有的别名,为实现绑定新的实
现做准备。- 加装闭包:如果实现类不是闭包(绑定自身或者绑定接口),那么就创建闭包,以实现 lazy
加载。- 注册:将闭包函数和单例变量存入 bindings 数组中,以备解析时使用。
- 回调:如果绑定的接口已经被解析过了,将会调用回调函数,对已经解析过的对象进行调
整。
主要查看其中的 2,3,4:
1.1 加装闭包
getClosure 的作用是为注册的非闭包实现外加闭包,这样做有两个作用:
延时加载
服务容器在 getClosure 中为每个绑定的类都包一层闭包,这样服务容器就只有进行解析的时候闭包才会真正进行运行,实现了 lazy 加载的功能。
递归绑定
对于服务容器来说,绑定是可以递归的,例如:
$app->bind(A::class,B::class);
$app->bind(B::class,C::class);
$app->bind(C::class,function(){
return new C;
})
对于 A 类,我们直接解析 A 可以得到 B 类,但是如果仅仅到此为止,服务容器直接去用反射去创建 B类的话,那么就很有可能创建失败,因为 B 类很有可能也是接口,B 接口绑定了其他实现类,要知道接口是无法实例化的。
因此服务容器需要递归地对 A 进行解析,这个就是 getClosure 的作用,它把所有可能会递归的绑定在闭包中都用 make 函数,这样解析 make (A::class) 的时候得到闭包 make (B::class),make (B::class)的时候会得到闭包 make (C::class),make (C::class) 终于可以得到真正的实现了。
对于自我绑定的情况,因为不存在递归情况,所以在闭包中会使用 build 函数直接创建对象。(如果仍
然使用 make,那就无限循环了)
1.2 注册
注册就是向 binding 数组中添加注册的接口与它的实现,其中 compact () 函数创建包含变量名和它们
的值的数组:
$this->bindings[$abstract] = compact('concrete', 'shared');
等价于
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
]
1.3 回调
注册之后,还要查看当前注册的接口是否已经被实例化,如果已经被服务容器实例化过,那么就要调用回调函数。(若存在回调函数)
resolved () 函数用于判断当前接口是否曾被解析过,在判断之前,先获取了接口的最终服务名:
/**
* Determine if the given abstract type has been resolved.
*
* @param string $abstract
* @return bool
*/
public function resolved($abstract)
{
if ($this->isAlias($abstract)) {
$abstract = $this->getAlias($abstract);
}
return isset($this->resolved[$abstract]) || isset($this->instances[$abstract]);
}
/**
* Determine if a given string is an alias.
*
* @param string $name
* @return bool
*/
public function isAlias($name)
{
return isset($this->aliases[$name]);
}
/**
* Get the alias for an abstract if available.
*
* @param string $abstract
* @return string
*/
public function getAlias($abstract)
{
if (! isset($this->aliases[$abstract])) {
return $abstract;
}
return $this->getAlias($this->aliases[$abstract]);
}
getAlias () 函数利用递归的方法获取别名的最终服务名称。
如果当前接口已经被解析过了,那么就要运行回调函数:
/**
* Fire the "rebound" callbacks for the given abstract type.
*
* @param string $abstract
* @return void
*/
protected function rebound($abstract)
{
$instance = $this->make($abstract);
foreach ($this->getReboundCallbacks($abstract) as $callback) {
call_user_func($callback, $this, $instance);
}
}
/**
* Get the rebound callbacks for a given type.
*
* @param string $abstract
* @return array
*/
protected function getReboundCallbacks($abstract)
{
return $this->reboundCallbacks[$abstract] ?? [];
}
2 make 解析
接下来就看看解析(以make为例):
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
该方法被子类
Application(vendor\laravel\framework\src\Illuminate\Foundation\Application.php)重写,那么接下来进入到 Application.php:
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
public function make($abstract, array $parameters = [])
{
$this->loadDeferredProviderIfNeeded($abstract = $this->getAlias($abstract));
return parent::make($abstract, $parameters);
}
该方法主要做了以下事情:
- 获取服务名称。
- 加载延迟服务。判断当前的接口是否是延迟服务提供者,若是延迟服务提供者,那么还要对
服务提供者进行注册与启动操作。- 调用父类 make 方法。
父类 make 没做太多事情,只是调用了 resolve 方法:
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$concrete = $this->getContextualConcrete($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null($concrete);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
if (is_null($concrete)) {
$concrete = $this->getConcrete($abstract);
}
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;# 改变解析状态,解析成功
array_pop($this->with);
return $object;
}
由于绑定的方式比较多,例如绑定闭包,绑定类名等,所以需要先判断需要被解析的是被绑定为什么类型,这里由 resolv 方法的作用就是 从容器中解析给定类型 :
主要步骤:
- 获取注册的实现:实现方式可能是上下文绑定的,也可能是 binding 数组中的闭包,也有可能就是接口本身。
- build 解析:首先判断是否需要递归。是,则递归 make;否,则调用 build 函数;直到调用 build 为止
- 执行扩展:若当前解析对象存在扩展,运行扩展函数。
- 创造单例对象:若 shared 为真,且不存在上下文绑定,则放入单例数组中
- 回调
- 标志解析
主要讲解步骤 1,2;
2.1 获取注册的实现
/**
* Get the contextual concrete binding for the given abstract.
*
* @param string $abstract
* @return \Closure|string|array|null
*/
protected function getContextualConcrete($abstract)
{
if (! is_null($binding = $this->findInContextualBindings($abstract))) {
return $binding;
}
// Next we need to see if a contextual binding might be bound under an alias of the
// given abstract type. So, we will need to check if any aliases exist with this
// type and then spin through them and check for contextual bindings on these.
if (empty($this->abstractAliases[$abstract])) {
return;
}
foreach ($this->abstractAliases[$abstract] as $alias) {
if (! is_null($binding = $this->findInContextualBindings($alias))) {
return $binding;
}
}
}
获取解析类的真正实现,函数优先去获取上下文绑定的实现,否则获取 binding 数组中的实现,获取不到就是直接返回自己作为实现。
2.2 build 解析
绑定是可以递归的,例如:
$app->bind('a','b');
$app->bind('b','c');
$app->bind('c',function(){
return new C;
})
遇到这样的情况,bind 绑定中 getClosure 函数开始发挥作用,该函数会给类包一层闭包,闭包内调用make 函数直到最后一层。
而有一些绑定方式并没有调用 bind 函数,例如上下文绑定 context:
$this->app->when(E::class)
->needs(F::class)
- >give(A::class);
当 make (E::class) 的时候,getConcrete 返回 A 类,而不是调用 make 函数的闭包,所以并不会启动递归流程得到 C 的匿名函数,所以造成 A 类完全无法解析,isBuildable 函数就是解决这种问题的,当发现需要解析构造的对象很有可能是递归的,那么就递归调用 make 函数,否则才会调用 build。
/**
* Instantiate a concrete instance of the given type.
*
* @param \Closure|string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) { # 判断是否为闭包,是则调用闭包
return $concrete($this, $this->getLastParameterOverride());
}
try {
$reflector = new ReflectionClass($concrete); # 不是闭包 则创建反射对象来 返回实例
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface or Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}