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);
		}
	}

从源码中我们可以看出,服务器的绑定有如下几个步骤

  1. 去除原有注册:去除当前绑定接口的原有实现单例对象,和原有的别名,为实现绑定新的实
    现做准备。
  2. 加装闭包:如果实现类不是闭包(绑定自身或者绑定接口),那么就创建闭包,以实现 lazy
    加载。
  3. 注册:将闭包函数和单例变量存入 bindings 数组中,以备解析时使用。
  4. 回调:如果绑定的接口已经被解析过了,将会调用回调函数,对已经解析过的对象进行调
    整。
    主要查看其中的 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);
}

该方法主要做了以下事情:

  1. 获取服务名称。
  2. 加载延迟服务。判断当前的接口是否是延迟服务提供者,若是延迟服务提供者,那么还要对
    服务提供者进行注册与启动操作。
  3. 调用父类 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 方法的作用就是 从容器中解析给定类型 :
主要步骤:

  1. 获取注册的实现:实现方式可能是上下文绑定的,也可能是 binding 数组中的闭包,也有可能就是接口本身。
  2. build 解析:首先判断是否需要递归。是,则递归 make;否,则调用 build 函数;直到调用 build 为止
  3. 执行扩展:若当前解析对象存在扩展,运行扩展函数。
  4. 创造单例对象:若 shared 为真,且不存在上下文绑定,则放入单例数组中
  5. 回调
  6. 标志解析

主要讲解步骤 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);
}