简介
服务容器就是一个普通的容器,用来封装类的实例,然后在需要的时候再取出来。用更专业的术语来说是服务容器实现了控制反转(Inversion of Control,缩写为IoC),
意思是正常情况下类A需要一个类B的时候,我们需要自己去new类B,意味着我们必须知道类B的更多细节,比如构造函数,随着项目的复杂性增大,这种依赖是毁灭性的。控制反转的意思就是,将类A主动获取类B的过程颠倒过来变成被动,类A只需要声明它需要什么,然后由容器提供。
样做的好处是,类A不依赖于类B的实现,这样在一定程度上解决了耦合问题。
在Laravel的服务容器中,为了实现控制反转,可以有以下两种:
- 依赖注入(Dependency Injection)。
- 绑定。
依赖注入
依赖注入是一种类型提示,举官网的例子
users = $users;
}
/**
* Show the profile for the given user.
*
* @param int $id
* @return Response
*/
public function show($id)
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
在本例中,UserController 需要从数据源获取用户,所以,我们注入了一个可以获取用户的服务 UserRepository,其扮演的角色类似使用 Eloquent 从数据库获取用户信息。
这里UserController
需要一个UserRepository
实例,我们只需在构造方法中声明我们需要的类型,容器在实例化UserController
时会自动生成UserRepository
的实例(或者实现类,因为UserRepository
可以为接口),而不用主动去获取UserRepository
的实例,这样也就避免了了解UserRepository
的更多细节,也不用解决UserRepository
所产生的依赖,我们所做的仅仅是声明我们所需要的类型,所有的依赖问题都交给容器去解决。
绑定
几乎所有的服务容器绑定都是在服务提供者中完成。
注:如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用 PHP 的反射服务自动解析出具体的对象。
绑定操作一般在 服务提供者 ServiceProviders
中的register
方法中,最基本的绑定是容器的bind
方法,它接受一个类的别名或者全名和一个闭包来获取实例:
简单的绑定
在一个服务提供者中,可以通过 $this->app 变量访问容器,然后使用 bind 方法注册一个绑定,该方法需要两个参数,第一个参数是我们想要注册的类名或接口名称,第二个参数是返回类的实例的闭包:
$this->app->bind('blogConfig', function ($app) {
return new MapRepository();
});
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
绑定一个单例
singleton
方法和bind
写法没什么区别,singleton
方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
绑定实例
你还可以使用 instance 方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:
$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);
上文中提到的request实例就是通过这种方法绑定到容器的:
$this->app->instance('request', $request);
绑定原始值
你可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
绑定接口到实现
服务容器的一个非常强大的功能是其绑定接口到实现。我们假设有一个 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;
}
上下文绑定
有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖 Illuminate\Contracts\Filesystem\Filesystem
契约 的不同实现。Laravel 为此定义了简单、平滑的接口:
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\VideoController;
use App\Http\Controllers\PhotoControllers;
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'));
});
解析
也就是获取绑定的实例:
获取方法
1. app('HelpSpot\API');
2. app()->make('HelpSpot\API');
3. app()['HelpSpot\API'];
4. resolve('HelpSpot\API');
以上四种方法均会返回获得HelpSpot\API
的实例。
区别是在一次请求的生命周期中:
bind方法的闭包会在每一次调用以上四种方法时执行,
singleton方法的闭包只会执行一次。
在使用中,如果每一个类要获的不同的实例,或者需要“个性化”的实例时,这时我们需要用bind方法以免这次的使用对下次的使用造成影响;
如果实例化一个类比较耗时或者类的方法不依赖该生成的上下文,那么我们可以使用singleton方法绑定。singleton方法绑定的好处就是,如果在一次请求中我们多次使用某个类,那么只生成该类的一个实例将节省时间和空间。
自动注入
最后,也是最常用的,你可以简单的通过在类的构造函数中对依赖进行类型提示来从容器中解析对象,控制器、事件监听器、队列任务、中间件等都是通过这种方式。在实践中,这是大多数对象从容器中解析的方式。
容器会自动为其解析类注入依赖,例如,你可以在控制器的构造函数中为应用定义的仓库进行类型提示,该仓库会自动解析并注入该类:
users = $users;
}
/**
* 通过指定ID显示用户
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}
容器事件
服务容器在每一次解析对象时都会触发一个事件,可以使用 resolving
方法监听该事件:
$this->app->resolving(function ($object, $app) {
// Called when container resolves object of any type...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
// Called when container resolves objects of type "HelpSpot\API"...
});
正如你所看到的,被解析的对象将会传递给回调函数,从而允许你在对象被传递给消费者之前为其设置额外属性。