前言
laravel现在已经是一个非常成熟的PHP框架,在学习laravel的这一段时间内,laravel框架中的许多思想让我这个刚刚起步的PHPer受益匪浅。希望在这篇博客中能把自己获得的一点心得与大家分享~
毫无疑问,laravel的核心就是其Ioc服务容器。在刚刚开始使用laravel的时候,说实话,阅读laravel文档有关服务容器和服务提供者的章节的时候是很迷茫的。由于其中涉及了很多新的思想,对于新手来说理解起来有一定难度。所以,这里我仍然推荐大家阅读两篇文章,laravel 学习笔记 —— 神奇的服务容器以及Laravel核心-IoC服务容器。第一篇不用说,作者对服务容器的理解非常之深,讲解深入浅出,对新手来说非常友好。第二篇文章则从源码着手,让我们对laravel底层封装的代码有更深入的理解。今天我想要分享的内容也是从这两篇博客中受的启发。(本篇博客中的laravel源码为5.5版本)
服务容器,绑定,解析到底是什么
其实这个问题是我刚学laravel的时候的遇到最根本的问题,所谓的服务容器、绑定、解析,到底是什么?不回答这三个问题,我们对laravel的核心理念根本无法理解。所幸laravel 学习笔记 —— 神奇的服务容器这篇博客给了我们最好的理解方式,我们来看这篇博客中关于服务容器的一段代码:
class Container
{
protected $binds;
protected $instances;
public function bind($abstract, $concrete)
{
if ($concrete instanceof Closure) {
$this->binds[$abstract] = $concrete;
} else {
$this->instances[$abstract] = $concrete;
}
}
public function make($abstract, $parameters = [])
{
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
array_unshift($parameters, $this);
return call_user_func_array($this->binds[$abstract], $parameters);
}
}
这段代码看上去很简单,但是对于刚刚接触PHP的人来说还是有几个地方要去额外关注,第一个就是关于闭包函数的概念以及理解,推荐阅读:阮一峰:学习Javascript闭包。闭包函数非常重要,是laravel实现延迟加载以及其他一些功能的关键,包括绑定以及解析。第二个关注点是PHP自带的一个函数call_user_func_array。这个函数把第一个参数作为回调函数调用,把第二个参数作为回调函数的参数传入。具体的用法可以参见PHP的手册call_user_func_array。理解了这两点我们具体来看看代码,到底什么是绑定,什么是解析。
public function bind($abstract, $concrete)
{
if ($concrete instanceof Closure) {
$this->binds[$abstract] = $concrete;
} else {
$this->instances[$abstract] = $concrete;
}
}
我们可以看到这个if-else语句中判断concrete是否是一个闭包,如果是一个闭包的话就存入这个容器的binds数组中,如果不是的话,那么就将它存入instances数组中。键值就是bind函数接收的第一个参数abstract,在laravel中我们其实也可以理解为就是我们所需的服务名(当然laravel中情况会更加复杂)。从中我们可以看到,绑定的真正意思就是将我们所需服务的回调函数或者实例存入服务容器中,以备将来我们直接调用。
再让我们看看make函数:
public function make($abstract, $parameters = [])
{
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
array_unshift($parameters, $this);
return call_user_func_array($this->binds[$abstract], $parameters);
}
其实make函数做的事也很简单,首先判断我们所需服务是如何绑定到服务容器的,如果是实例绑定的,则直接从instances数组中取出实例返回,如果不是的话,则使用call_user_func_array函数回调我们存储在binds函数中的闭包函数。array_unshift函数是把服务容器存储在parameter数组中,供回调函数使用。
至此,我们可以粗略的总结一下:
服务容器:进化版的工厂模式,用来存储所需接口或者服务的对应实现方式,并且不依赖于外部服务。
绑定:其实就是将接口或者服务的实现方式存储在服务容器中,绑定内容可以是闭包函数,已存在的实例等等。当然在laravel中,绑定的种类更多,几乎可以满足所有的需求。
解析:本质上就是取出我们存储在服务容器中的 对应接口或服务的实现方式。
了解了大概的内容,其实我们对laravel中的核心也就有了一个比较粗略的了解,具体laravel是如何封装的,我们还是需要到源码中一探究竟。
laravel中的实现方式
服务容器
laravel中的服务容器是怎么样的?我们可以从下面的这张图了解主要几个类与接口的相关关系。
可以看到,服务容器本身使用了两个接口,一个是Container接口,一个是ArrayAccess接口,并且有一个子类Application,当然这个Application类还使用了另外两个接口,这里就不展开说了。在了解服务容器的bind函数之前,我们先来了解一下ArrayAccess接口,作为PHP预定义的六大接口之一,ArrayAccess接口提供了以数组形式访问对象的能力。大家可以先阅读PHP手册了解相关内容:ArrayAccess(数组式访问)接口
具体到laravel的服务容器中,其功能实现主要依赖以下几个函数:
public function offsetExists($key)
{
return $this->bound($key);
}
public function offsetGet($key)
{
return $this->make($key);
}
public function offsetSet($key, $value)
{
$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
return $value;
});
}
public function offsetUnset($key)
{
unset($this->bindings[$key], $this->instances[$key], $this->resolved[$key]);
}
当我们使用数组式访问的方法向对象中添加数值的时候,会自动地调用offsetSet函数,可以看到在laravel的服务容器中,就是将服务和其实现方式绑定至服务容器中。具体的就是存储到服务容器中的bindings或者instances数组中,在bind函数中可以看到其实现方式。而offsetGet函数则是解析相关服务或接口,其实也就是从相关数组中取出相应的实现方式。比如,在关于Route的Facade源码中,便会使用app[‘router’]取出存储在服务容器中关于路由服务的对应实现。
绑定
在看laravel源码之前,我们先来看一下绑定的种类,内容来自于laravel官方文档:laravel服务绑定
绑定单例
这种绑定只能被解析一次,绑定单例被解析后,相同的对象会在随后的调用中返回到容器中,即一个类只能对应一个实例。
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
来看一下singleton源码,实质上就是一个shared为真的bind绑定,对应接口的实现方式仍然存在bindings数组中。但是经过解析之后,单例会被存储到instances数组中,具体的我们会在讲解make解析时提到。
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
绑定实例
这种绑定会将现有的对象实例绑定到容器中,这个实例会始终在随后的调用中返回。
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\API', $api);
通过源码我们可以知道,其实就是给定一个实例存储到instance数组中,如果接口之前已经被解析过,就调用回调函数。
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
// We'll check to determine if this type has been bound before, and if it has
// we will fire the rebound callbacks registered with the container and it
// can be updated with consuming classes that have gotten resolved here.
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
简单绑定
这种是最常见的绑定,其实也就是绑定一个闭包到服务容器中
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
绑定接口到实现
即将接口绑定到给定实现,下面这段代码绑定EventPusher接口的实现方式为RedisEventPusher类,当我们需要实现EventPusher时,就会注入RedisEventPusher。
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);
上下文绑定
有时候我们希望对不同的类可以注入不同的实现,这时候上下文绑定就会发挥作用。下面这段代码,同样是Filesystem的实现,在PhotoController和VideoController中会注入不同的实现。
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function() {
return Storage::disk('s3');
});
上下文绑定与前面的绑定有所不同,它是通过addContextualBinding函数将接口的实现方式存储到服务容器的contextual数组中
public function addContextualBinding($concrete, $abstract, $implementation)
{
$this->contextual[$concrete][$this->getAlias($abstract)] = $implementation;
}
以我们上面的上下文绑定举例,我们会得到
$conctextual['PhotoController']['FileSystem'] = Storage::disk('local');
$conctextual['VideoController']['FileSystem'] = Storage::disk('s3');
总结:实例绑定会存储在instances数组中,上下文绑定会存储在contextual数组中,其余所有绑定都会存储在bindings数组中,单例绑定有一些特殊,bindings以及instances数组中都会有存储,这个会在之后的文章中解释。
bind绑定
了解了这些不同的绑定之后,我们针对使用最频繁的bind绑定函数来看看具体的代码。
public function bind($abstract, $concrete = null, $shared = false)
{
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters
$this->dropStaleInstances($abstract);
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 wrap it
// 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'll fire 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);
}
}
第一部分代码,使用dropStaleInstances方法去除现有接口绑定的实例对象和别名。如果concrete为null的话,服务容器就会判断为绑定自身(一般是单例绑定),直接把abstract赋值给concrete。
dropStaleInstances具体实现方式很简单,就是unset instances数组中存储的服务的对应的实现方式。同时unset这个服务的别名。服务别名我们在这一篇博客中不会展开叙述,现在我们只需要知道对服务容器来说,解析服务绑定的别名和原服务没有任何区别。
protected function dropStaleInstances($abstract)
{
unset($this->instances[$abstract], $this->aliases[$abstract]);
}
接着服务容器会判断我们想要绑定的实现方式concrete是否是闭包,如果不是闭包的话意味着concrete只是一个类名或者接口名,那我们就使用getClosure函数加装闭包。
为什么要使用闭包函数呢?一个是为了延迟加载,另一个原因则是在laravel中,绑定是可以递归的,加装闭包的实际作用就是递归调用make函数进行解析,直到最后一层,即绑定服务的闭包函数或者其他实现方式。否则,很可能A服务绑定的是一个接口,当laravel使用PHP反射特性进行解析构建的时候会直接报错。
来看getClosure具体代码,首先判断是不是绑定自身(一般就是单例绑定),如果是绑定自身的话就外加build解析,此时外加make函数解析会造成无限循环,如果不是绑定自身的话就外加make函数解析。
protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = [] use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->make($concrete, $parameters);
});
}
加装完闭包之后,则是将concrete和shared存储至服务容器的bindings数组中。同时,如果所绑定的接口或服务已经被解析过了,则需要进行重新绑定。
了解了bind绑定,下面我们就可以去了解laravel是如何进行解析的。
make解析
直接查看Illumiante\Container\Container.php中的make函数,可以看到make函数是调用resolve函数进行解析的:
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
我们接着来看resolve函数:
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null($this->getContextualConcrete($abstract));
// 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;
$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;
}
$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);
}
resolve函数到底是如何解析的呢?首先通过getAlias函数得到真正的服务名称。
通过代码我们可以清楚知道,getAlias函数就是利用递归获得真正服务或者接口的名字,然后返回。
public function getAlias($abstract)
{
if (!isset($this->aliases[$abstract])) {
return $abstract;
}
if ($this->aliases[$abstract] === $abstract) {
throw new LogicException("[{$abstract}] is aliased to itself.");
}
return $this->getAlias($this->aliases[$abstract]);
}
然后是判断是否是上下文绑定
$needsContextualBuild = ! empty($parameters) || ! is_null($this->getContextualConcrete($abstract));
接着如果是单例绑定并且不是上下文绑定的话就从instances数组中取出实例返回。
if (isset($this->instances[$abstract]) $$ ! $needsContextualBuild) {
return $this->instances[$abstract];
}
接下来获取接口或者服务对应的实现。
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
通过getConcrete函数,我们判断其绑定的实现方式。如果是上下文绑定的话,我们通过getContextualConcrete函数获取实现方式返回,否则就取出存储在bindings数组中相对应的值。
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
build解析
接下来我们将会使用build解析,首先判断是否需要使用make递归,这里就可以看到之前bind函数中getClosure函数所发挥的作用。由于加装了一层闭包,遇到递归绑定的情况我们可以一直使用make解析,直到可以使用build解析。
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
isBuildable函数判断方式很简单,如果是绑定自身或者绑定了一个闭包,就使用build函数解析,否则就需要递归调用make。
isBuildable解决了一个什么问题呢?我们知道上下文绑定的时候并没有使用getClosure函数给实现方式添加闭包,因此有可能我们在实例化A的时候需要解析其依赖注入B,而B是一个接口。如果不使用make递归解析,PHP反射无法实例化接口,便会报错。
protected function isBuildable($concrete, $abstract)
{
return $concrete == $abstract || $concrete instanceof Closure;
}
下面便是laravel中另一个非常精妙的设计,build函数将使用PHP的反射特性构建函数。一起来看一下build函数的具体代码:
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 fro more fine-tuned resolution of these objects.
if ($concrete instance of Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstraction so we need to bail out
if (! $reflector->isInstantiable()) {
return $this->noInstantiable($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
$instances = $this->resolveDependencies($dependencies);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
第一部分代码我们判断绑定的是不是一个闭包函数,如果是的话,就直接返回这个闭包函数,执行这个闭包函数的代码。如果不是的话,则表明需要我们解析的是一个类,这时我们就可以利用PHP的反射机制获得我们所需要构造的类的结构。建议阅读PHP手册PHP:反射
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
Laravel使用反射机制中自带的函数判断能否实例化,了解手册之后我们应该知道,抽象类以及接口是无法通过反射初始化的。因此如果服务接口没有绑定具体实现的方式的话,laravel解析时会直接报错。
$reflector = new ReflectionClass($concrete);
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
之后服务容器会把当前解析的类存入一个解析栈中。因为在解析当前类的过程中很可能会有依赖注入,这个解析栈便是用来存储我们所需要解析的所有类。接着使用getConstructor函数取得所需解构的类中的构造函数。
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
如果没有构造函数,那我们就可以清空buildStack栈,然后直接构造一个实例出来。
如果有构造函数的话,就使用getParameters函数取得所有参数,为之后递归解析做准备。
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
获得所有构造函数的依赖之后,我们就可以使用反射去构建每一个依赖然后注入到构造函数之中。
$instances = $this->resolveDependencies($dependencies);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
我们来看一下resolveDependencies函数是如何构建所需依赖的实例的。
protected function resolveDependencies(array $dependencies)
{
$results = [];
foreach ($dependencies as $dependency) {
// If this dependency has a override for this particular build we will use
// that instead as the value. Otherwise, we will continue with this run
// of resolutions and let reflection attempt to determine the result.
if ($this->hasParameterOverride($dependency)) {
$results[] = $this->getParameterOverride($dependency);
continue;
}
// If the class is null, it means the dependency is a string or some other
// primitive type which we cannot resolve since it is not a class and
// we will just bomb out with an error since we have no-where to go.
results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency);
}
return $results;
}
对于构造函数的每个依赖我们都需要做判断。首先,如果在这次解析过程中参数有被重写的话,我们需要进行更新。
if ($this->hasParameterOverride($dependecy))
{
$results[] = $this->getParameterOverride($dependency);
continue;
}
接着我们需要对依赖对象的进行区分,如果是原始类型的数据,我们则使用resolvePrimitive方程进行解析,如果是类的话,我们使用resolveClass进行解析:
$results[] = is_null($dependency->getClass()) ? $this->resolvePrimitive($dependency) : $this->resolveClass($dependency);
来分别看一下这两个解析方程:
resolvePrimitive方程直接从上下文中去获取数值,如果没有的话,就去获取默认值。如果还是没有的话,就会报错。
protected function resolvePrimitive(ReflectionParameter $parameter)
{
if (! is_null($concrete = $this->getContextualConcrete('$'.$parameter->name))) {
return $concrete instanceof Closure ? $concrete($this) : $concrete;
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
$this->unresolvablePrimitive($parameter);
}
接下来看一下resolveClass是如何工作的:
可以看到对于依赖对象为类的情况,我们会通过make进行递归解析,仍然利用反射机制初始化实例进行注入。
protected function resolveClass(ReflectionParameter $parameter)
{
try {
return $this->make($parameter->getClass()->name);
}
// If we cannot resolve the class instance, we will check if the value
// is optional, and if it is we will return the optional parameter value as
// the value of the dependency, similarly to how we do this scalars.
catch (BindingResolutionException $e) {
if ($parameter->isOptional()) {
return $parameter->getDefaultValue;
}
throw $e;
}
}
完成build解析之后,我们会执行扩展。由于不是本篇重点,不详细赘述。
接着会判断是否是单例绑定,如果是单例绑定,我们会把实例再存入instances数组中,这样今后解析就只会返回同一个实例。
if ($this->isShared($abstract) && ! needsContextualBuild) {
$this->instances[$abstract] = $object;
}
最后服务容器会进行回调,返回解析出的实例。