思考并回答以下问题:
服务容器
在说IoC容器之前,我们需要了解什么是IoC容器。
Laravel服务容器是一个用于管理类依赖和执行依赖注入的强大工具。
在理解这句话之前,我们需要先了解一下服务容器的来龙去脉:Laravel神奇的服务容器。这篇博客告诉我们,服务容器就是工厂模式的升级版,对于传统的工厂模式来说,虽然解耦了对象和外部资源之间的关系,但是工厂和外部资源之间却存在了耦和。而服务容器在为对象创建了外部资源的同时,又与外部资源没有任何关系,这个就是IoC容器。
- 所谓的依赖注入和控制反转:依赖注入和控制反转,就是只要不是由内部生产(比如初始化、构造函数__construct中通过工厂方法、自行手动new的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)
也就是说:
依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;
控制反转是从容器的角度在描述,描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
Laravel中的服务容器
Laravel服务容器主要承担两个作用:绑定与解析。
绑定
所谓的绑定就是将接口与实现建立对应关系。几乎所有的服务容器绑定都是在服务提供者中完成,也就是在服务提供者中绑定。
如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用PHP的反射服务自动解析出具体的对象。
也就是说,如果需要依赖注入的外部资源如果没有接口,那么就不需要绑定,直接利用服务容器进行解析就可以了,服务容器会根据类名利用反射对其进行自动构造。
bind绑定
绑定有多种方法,首先最常用的是bind函数的绑定:
绑定自身
1 | $this->app->bind('App\Services\RedisEventPusher', null); |
绑定接口
1 | public function testCanBuildWithoutParameterStackWithConstructors() |
这三种绑定方式中,第一种绑定自身一般用于绑定单例。
bindif绑定
1 | public function testBindIfDoesntRegisterIfServiceAlreadyRegistered() |
singleton绑定
singleton方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:
1 | $this->app->singleton('HelpSpot\API', function ($app) { |
值得注意的是,singleton绑定在解析的时候若存在参数重载,那么就自动取消单例模式。
1 | public function testSingletonBindingsNotRespectedWithMakeParameters() |
instance绑定
我们还可以使用instance方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:
1 | $api = new HelpSpot\API(new HttpClient); |
Context绑定
有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖Illuminate\Contracts\Filesystem\Filesystem契约的不同实现。Laravel为此定义了简单、平滑的接口:
1 | use Illuminate\Support\Facades\Storage; |
原始值绑定
我们可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:
1 | $this->app->when('App\Http\Controllers\UserController') |
数组绑定
数组绑定一般用于绑定闭包和变量,但是不能绑定接口,否则只能返回接口的实现类名字符串,并不能返回实现类的对象。
1 | public function testArrayAccess() |
标签绑定
少数情况下,我们需要解析特定分类下的所有绑定,例如,你正在构建一个接收多个不同Report接口实现的报告聚合器,在注册完Report实现之后,可以通过tag方法给它们分配一个标签:
1 | $this->app->bind('SpeedReport', function () { |
这些服务被打上标签后,可以通过tagged方法来轻松解析它们:
1 | $this->app->bind('ReportAggregator', function ($app) { |
extend扩展
extend是在当原来的类被注册或者实例化出来后,可以对其进行扩展,而且可以支持多重扩展:
1 | public function testExtendInstancesArePreserved() |
Rebounds与Rebinding绑定是针对接口的,是为接口提供实现方式的方法。我们可以对接口在不同的时间段里提供不同的实现方法,一般来说,对同一个接口提供新的实现方法后,不会对已经实例化的对象产生任何影响。但是在一些场景下,在提供新的接口实现后,我们希望对已经实例化的对象重新做一些改变,这个就是rebinding函数的用途。下面就是一个例子:
1 | abstract class Car |
我们在服务容器中是这样对car接口和fuel接口绑定的:
1 | $this->app->bind('fuel', function ($app) { |
如果car被服务容器解析实例化成对象之后,有人修改了fuel接口的实现,从Petrol改为 PremiumPetrol:
1 | $this->app->bind('fuel', function ($app) { |
由于car已经被实例化,那么这个接口实现的改变并不会影响到car的实现,假若我们想要car的成员变量fuel随着fuel接口的变化而变化,我们就需要一个回调函数,每当对fuel接口实现进行改变的时候,都要对car的fuel变量进行更新,这就是rebinding的用途:
1 | $this->app->bindShared('car', function ($app) { |
服务别名
什么是服务别名
在说服务容器的解析之前,需要先说说服务的别名。什么是服务别名呢?不同于上一个博客中提到的Facade门面的别名(在config/app中定义),这里的别名服务绑定名称的别名。通过服务绑定的别名,在解析服务的时候,跟不使用别名的效果一致。别名的作用也是为了同时支持全类型的服务绑定名称以及简短的服务绑定名称考虑的。
通俗的讲,假如我们想要创建auth服务,我们既可以这样写:
1 | $this->app->make('auth') |
又可以写成:
1 | $this->app->make('\Illuminate\Auth\AuthManager::class') |
还可以写成
1 | $this->app->make('\Illuminate\Contracts\Auth\Factory::class') |
后面两个服务的名字都是auth的别名,使用别名和使用auth的效果是相同的。
服务别名的递归
需要注意的是别名是可以递归的:
1 | app()->alias('service', 'alias_a'); |
会得到:
1 | 'alias_a' => 'service' |
服务别名的实现
那么这些别名是如何加载到服务容器里面的呢?实际上,服务容器里面有个aliases数组:
1 | $aliases = [ |
而服务容器的初始化的过程中,会运行一个函数:
1 | public function registerCoreContainerAliases() |
加载后,服务容器的aliases和abstractAliases数组:
1 | $aliases = [ |
服务解析
make解析
有很多方式可以从容器中解析对象,首先,你可以使用make方法,该方法接收你想要解析的类名或接口名作为参数:
1 | public function testAutoConcreteResolution() |
如果你所在的代码位置访问不了$app变量,可以使用辅助函数resolve:
1 | $api = resolve('HelpSpot\API'); |
自动注入
1 | namespace App\Http\Controllers; |
call方法注入
make解析是服务容器进行解析构建类对象时所用的方法,在实际应用中,还有另外一个需求,那就是当前已经获取了一个类对象,我们想要调用它的一个方法函数,这时发现这个方法中参数众多,如果一个个的make会比较繁琐,这个时候就要用到call解析了。我们可以看这个例子:
1 | class TaskRepository |
闭包函数注入
1 | public function testCallWithDependencies() |
普通函数注入
1 | public function testCallWithGlobalMethodName() |
静态方法注入
服务容器的call解析主要依靠call_user_func_array()函数,关于这个函数可以查看Laravel学习笔记之Callback Type - 来生做个漫画家,这个函数对类中的静态函数和非静态函数有一些区别,对于静态函数来说:
1 | class ContainerCallTest |
服务容器调用类的静态方法有三种,注意第三种使用数组的形式,数组中可以直接传类名 TaskRepository::class;
非静态方法注入
对于类的非静态方法:
1 | class ContainerCallTest |
我们可以看到非静态方法只有两种调用方式,而且第二种数组传递的参数是类对象,原因就是 call_user_func_array函数的限制,对于非静态方法只能传递对象。
bindmethod方法绑定
服务容器还有一个bindmethod的方法,可以绑定类的一个方法到自定义的函数:
1 | public function testContainCallMethodBind() |
从结果上看,bindmethod不会对静态的第二种解析方法(::解析方式)起作用,对于其他方式都会调用绑定的函数。
1 | public function testCallWithBoundMethod() |
默认函数注入
1 | public function testContainCallDefultMethod() |
值得注意的是,这种默认函数注入的方法使得非静态的方法也可以利用类名去调用,并不需要对象。默认函数注入也回受到bindmethod函数的影响。
数组解析
app()[‘service’];
app($service)的形式app(‘service’);
服务容器事件
每当服务容器解析一个对象时就会触发一个事件。你可以使用resolving方法监听这个事件:
1 | $this->app->resolving(function ($object, $app) { |
服务容器每次解析对象的时候,都会调用这些通过resolving和afterResolving函数传入的闭包函数,也就是触发这些事件。 注意:如果是单例,则只在解析时会触发一次
1 | public function testResolvingCallbacksAreCalled() |
装饰函数
容器的装饰函数有两种,wrap用于装饰call,factory用于装饰make:
1 | public function testContainerWrap() |
容器重置flush
容器的重置函数flush会清空容器内部的aliases、abstractAliases、resolved、bindings、instances
1 | public function testContainerFlushFlushesAllBindingsAliasesAndResolvedInstances() |
前言
在前面几个博客中,我详细讲了IoC容器各个功能的使用、绑定的源码、解析的源码,今天这篇博客会详细介绍Ioc容器的一些细节,一些特性,以便更好地掌握容器的功能。
注:本文使用的测试类与测试对象都取自Laravel的单元测试文件src/illuminate/tests/Container/ContainerTest.php
rebind绑定特性
rebind在绑定之前instance和普通bind绑定一样,当重新绑定的时候都会调用rebind回调函数,但是有趣的是,对于普通bind绑定来说,rebind回调函数被调用的条件是当前接口
被解析过:
1 | public function testReboundListeners() |
所以遇到下面这样的情况,rebinding的回调函数是不会调用的:
1 | public function testReboundListeners() |
有趣的是对于instance绑定:
1 | public function testReboundListeners() |
rebinding回调函数却是可以被调用的。其实原因就是 instance 源码中 rebinding 回
调函数调用的条件是 rebound 为真,而普通 bind 函数调用 rebinding 回调函数的条
件是 resolved 为真. 目前笔者不是很清楚为什么要对 instance 和 bind 区别对待,
希望有大牛指导。
rebind在绑定之后
为了使得rebind回调函数在下一次的绑定中被激活,在rebind函数的源码中,如果判断当前对象已经绑定过,那么将会立即解析:
1 | public function rebinding($abstract, Closure $callback) |
单元测试代码:
1 | public function testReboundListeners1() |
resolving特性
resolving 回调的类型
resolving不仅可以针对接口执行回调函数,还可以针对接口实现的类型进行回调函数。
1 | public function testResolvingCallbacksAreCalledForType() |
resolving回调与instance
前面讲过,对于singleton绑定来说,resolving回调函数仅仅运行一次,只在singleton 第一次解析的时候才会调用。如果我们利用instance直接绑定类的对象,不需要解析,那么resolving回调函数将不会被调用:
1 | public function testResolvingCallbacksAreCalledForSpecificAbstracts() |
extend扩展特性
extend用于扩展绑定对象的功能,对于普通绑定来说,这个函数的位置很灵活:在绑定前扩展
1 | public function testExtendIsLazyInitialized() |
在绑定后解析前扩展
1 | public function testExtendIsLazyInitialized() |
在解析后扩展
1 | public function testExtendIsLazyInitialized() |
可以看出,无论在哪个位置,extend扩展都有lazy初始化的特点,也就是使用extend函数并不会立即起作用,而是要等到make解析才会激活。
extend与instance绑定
对于instance绑定来说,暂时extend的位置需要位于instance之后才会起作用,并且会立即起作用,没有lazy的特点:
1 | public function testExtendInstancesArePreserved() |
extend绑定与rebind回调
无论扩展对象是instance绑定还是bind绑定,extend都会启动rebind回调函数:
1 | public function testExtendReBindingInstance() |
contextual绑定特性
contextual在绑定前
contextual绑定不仅可以与bind绑定合作,相互不干扰,还可以与instance绑定相互合作。而且instance的位置也很灵活,可以在contextual绑定前,也可以在contextual绑定后:
1 | public function testContextualBindingWorksForExistingInstancedBindings() |
contextual在绑定后
1 | public function testContextualBindingWorksForNewlyInstancedBindings() |
contextual绑定与别名
contextual绑定也可以在别名上进行,无论赋予别名的位置是contextual的前面还是后面:
1 | public function testContextualBindingDoesntOverrideNonContextualResolution() |
争议
目前比较有争议的是下面的情况:
1 | public function testContextualBindingWorksOnExistingAliasedInstances() |
由于instance的特性,当别名被绑定到其他对象上时,别名stub已经失去了与Illuminate\Tests\Container\IContainerContractStub之间的关系,因此不能使用stub代替作上下文绑定。 但是另一方面:
1 | public function testContextualBindingWorksOnBoundAlias() |
代码只是从 instance 绑定改为bind绑定,由于bind绑定只切断了别名中的alias数组的联系,并没有断绝abstractAlias数组的联系,因此这段代码却可以通过,很让人难以理解。本人在给 Taylor Otwell 提出 PR 时,作者原话为“I’m not making any of these changes to the container on a patch release.”。也许,在以后(5.5或以后)版本作者会更新这里的逻辑,我们就可以看看服务容器对别名绑定的态度了,大家也最好不要这样用。
服务容器中的闭包函数参数
服务容器中很多函数都有闭包函数,这些闭包函数可以放入特定的参数,在绑定或者解析过程中,这些参数会被服务容器自动带入各种类对象或者服务容器实例。
bind闭包参数
1 | public function testAliasesWithArrayOfParameters() |
extend闭包参数
1 | public function testExtendedBindings() |
bindmethod闭包参数
1 | public function testCallWithBoundMethod() |
resolve闭包参数
1 | public function testResolvingCallbacksAreCalledForSpecificAbstracts() |
rebinding闭包参数
1 | public function testReboundListeners() |