容器,字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。当然,有这样一种容器,它存放的不是文本、数值,而是对象、对象的描述(类、接口)或者是提供对象的回调,通过这种容器,我们得以实现许多高级的功能,其中最常提到的,就是 “解耦” 、“依赖注入(DI
)”。
Laravel
的核心就是一个 IoC
容器,根据文档,称其为“服务容器”,顾名思义,该容器提供了整个框架中需要的一系列服务。作为初学者,很多人会在这一个概念上犯难,因此,我打算从一些基础的内容开始讲解,通过理解面向对象开发中依赖的产生和解决方法,来逐渐揭开“依赖注入”的面纱,逐渐理解这一神奇的设计理念。
Laravel 服务容器是一个用于管理类依赖和执行依赖注入的强大工具。
依赖注入:只要不是由内部生产(比如初始化、构造函数 __construct 中通过工厂方法、自行手动 new 的),而是由外部以参数或其他形式注入的,都属于依赖注入(DI)(个人觉得最清晰的解释)
app
这个辅助函数定义在vendor/laravel/lumen-framework/src/helpers.php
里面,,这个文件定义了很多help
函数,并且会通过composer
自动加载到项目中(在autoload_static.php
文件中可以看到它,composer
的具体实现可以自己百度去看看)。所以,在参与http
请求处理的任何代码位置都能够访问其中的函数,比如app()
。
$app = app();
这个其实是用到
Facade
(门面),在laravel
中会存在一个名称为aliases
的数组,专门用来配置一些类型的别名,在lumen
中App
的别名是在vendor/barryvdh/laravel-ide-helper/src/Generator.php:244
中定义了的,还有一些其他的门面定义,可以自行查看
use App;
App::basePath();
比如:在服务提供者里面直接使用
$this->app
。laravel
在实例化服务提供器的时候,会把laravel
容器实例注入到这个$app
上面。所以我们在服务提供器里面,始终能通过$this->$app
访问到laravel
容器实例,而不需要再使用app()
函数或者App Facade
了。
abstract class ServiceProvider
{
protected $app;
...
}
$tis->app->singleton(...);
如果类没有依赖任何接口,就没有必要将类绑定到容器中。容器不需要指定如何构建这些对象,因为它可以使用反射自动解析这些对象。
在接口\Illuminate\Contracts\Container\Container
中定义了容器绑定和解析的各种方法,接口Container
存在一个实现类\Illuminate\Container\Container
,\Laravel\Lumen\Application
继承了它的实现类,容器$app
就是类Application
的实现
下面先看看各个方法做了些什么事,最后看Lumen
从入口进来是如何调用服务提供器的
前面说了我们在服务提供器里面可以通过
$this->app
属性访问容器。我们可以通过bind
方法注册绑定,传递我们想要注册的类或接口名称再返回类的实例的Closure
注意,我们接受容器本身作为解析器的参数。然后,我们可以使用容器来解析正在构建的对象的子依赖。
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
我们看看bind
方法具体的做了些什么事
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
if (is_null($concrete)) {
$concrete = $abstract;
}
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
1、根据一个名称,删除已有的所有实例和别名,就是删除instance
数组和aliases
数组中已存在的值
protected function dropStaleInstances($abstract)
{
unset($this->instances[$abstract], $this->aliases[$abstract]);
}
2、判断是否给了具体类型$concrete
,没有的话把抽象类型$abstract
赋值给具体类型$concrete
3、如果具体类型$concrete
不是一个闭包,不是的话,则根据抽象类型$abstract
和具体类型$concrete
转成一个闭包
build
的实例,实例化给定的具体实例makeWith
的实例,从容器中解析出一个实例protected function getClosure($abstract, $concrete)
{
return function ($container, $parameters = []) use ($abstract, $concrete) {
if ($abstract == $concrete) {
return $container->build($concrete);
}
return $container->makeWith($concrete, $parameters);
};
}
4、以$abstract
为key
将具体类型$concrete
以及$shared
的值绑定到bindings
的成员数组中
5、判断$abstract
是否被解析,已经被解析,存在实例,rebound一次?(不太懂这个操作,不知道对不对)
服务容器有一个强大的功能,就是将接口绑定到给定实现。例如,如果我们有一个 EventPusher 接口和一个 RedisEventPusher 实现。编写完接口的 RedisEventPusher 实现后,我们就可以在服务容器中注册它,像这样:
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);
这么做相当于告诉容器:当一个类需要实现 EventPusher 时,应该注入 RedisEventPusher。现在我们就可以在构造函数或者任何其他通过服务容器注入依赖项的地方使用类型提示注入 EventPusher 接口:
use App\Contracts\EventPusher;
/**
* 创建一个新的类实例
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}
这个用法感觉还可以,但是一个接口也只是能绑定一个实现,其他要同一个接口,不同实现的,这个就没啥用了(laravel
给出了上下文绑定的方式),但是对修改方便,实现一个新类,修改一下绑定就可以了(个人想法)
singleton 方法将类或接口绑定到只能解析一次的容器中。绑定的单例被解析后,相同的对象实例会在随后的调用中返回到容器中:
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
看看singleton
方法中做了些什么事情
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
把shared
设置为true
,然后调用build
方法,参见上面
你也可以使用 instance 方法将现有对象实例绑定到容器中。给定的实例会始终在随后的调用中返回到容器中:
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\API', $api);
直接创建一个类对象,然后调用instance
,看看instance()
方法做了些什么
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
}
1、从上下文绑定的别名aliases
中删除已存在的abstractAliases
缓存
2、在bindings
、instances
、aliases
中查找是否存在抽象类型$abstract
的绑定
3、删除aliases
中的绑定
4、在instances
上绑定该实例
5、根据第二步的判断,如果存在实例,rebound一次(又来了,不知道干嘛?)
当你有一个类不仅需要接受一个注入类,还需要注入一个基本值(比如整数)。你可以使用上下文绑定来轻松注入你的类需要的任何值
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
这种场景,我没有使用过,我们也大概看看他们是做些什么处理
public function when($concrete)
{
return new ContextualBindingBuilder($this, $this->getAlias($concrete));
}
public function needs($abstract)
{
$this->needs = $abstract;
return $this;
}
public function give($implementation)
{
$this->container->addContextualBinding(
$this->concrete, $this->needs, $implementation
);
}
1、when()
函数里面实例化了一个ContextualBindingBuilder
对象(上下文绑定对象),传入container
对象以及在aliases
数组最终的信息
2、needs()
函数,对当前对象的成员变量needs
赋值抽象类型$abstract
3、give()
函数,调用container
对象的addContextualBinding
方法根据抽象类型和具体类型添加一个上下文绑定到contextual
这个变量中,值为给定的变量(give
里面的参数)
有时候,你可能有两个类使用了相同的接口,但你希望每个类都能注入不同的实现。例如,两个控制器可能需要依赖不同的 Illuminate\Contracts\Filesystem\Filesystem 契约 实现。 Laravel 提供了一个简单、优雅的接口来定义这个行为:
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');
});
函数调用和绑定初始数据一样
有时候,你可能需要解析某个「分类」下的所有绑定。例如,你正在构建一个报表的聚合器,它接收一个包含不同 Report 接口实现的数组。注册了 Report 实现后,你可以使用 tag 方法为其分配标签:
$this->app->bind('SpeedReport', function () {
//
});
$this->app->bind('MemoryReport', function () {
//
});
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
服务被标记后,你可以通过 tagged 方法轻松地将它们全部解析:
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});
tag
函数将第一个参数的信息,绑定container
的成员变量tags
中;
tagged
函数将这个标签中的所有抽象类型都实例化,返回。相当于给构造函数传了一个数组,数组内有很多不同的对象
绑定大概就是这么多方式了
我现阶段的代码都是在
Controller
中use xxFacaed
,然后使用调用`````
public function make($abstract)
{
$abstract = $this->getAlias($abstract);
if (array_key_exists($abstract, $this->availableBindings) &&
! array_key_exists($this->availableBindings[$abstract], $this->ranServiceBinders)) {
$this->{$method = $this->availableBindings[$abstract]}();
$this->ranServiceBinders[$method] = true;
}
return parent::make($abstract);
}
1、获取抽象类型$abstract
的最终别名
2、判断$abstract
是否默认可用的绑定availableBindings
,而且未执行,则先执行availableBindings
中对应的方法
3、调用父类的make
方法,该方法只有一句return $this->resolve($abstract);
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
从容器中解析给定的类型:
1、获取最终的别名信息
2、判断是否需要绑定上下文,存在参数或者
路由中的request
就是在解析完所有的请求信息时,使用instance
的方式绑定到容器的,后面不论在哪里调用request
对象,都是同一个
本文一些定义抄的地址
https://learnku.com/articles/4698/laravel-core-ioc-service-container
https://laravelacademy.org/post/769.html