Laravel-Container

思考并回答以下问题:

  • 怎么进行自动注入?

简介

Laravel服务容器是一个用于管理类依赖和执行依赖注入的强大工具。依赖注入其实质是通过构造函数或者某些情况下通过「setter」方法将类依赖注入到类中。

让我们看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

namespace App\Http\Controllers;

use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
/**
* The user repository implementation.
*
* @var UserRepository
*/
protected $users;

/**
* Create a new controller instance.
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->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从数据库获取用户信息。注入UserRepository后,我们可以在其基础上封装其他实现,也可以模拟或者创建一个假的UserRepository实现用于测试。

深入理解Laravel服务容器对于构建功能强大的大型Laravel应用而言至关重要,对于贡献代码到Laravel核心也很有帮助。

绑定

绑定基础

几乎所有的服务容器绑定都是在服务提供者中完成。因此本文档的演示例子用到的容器都是在服务提供者中绑定。

注:如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用PHP的反射服务自动解析出具体的对象。

简单的绑定

在一个服务提供者中,可以通过$this->app变量访问容器,然后使用bind方法注册一个绑定,该方法需要两个参数,第一个参数是我们想要注册的类名或接口名称,第二个参数是返回类的实例的闭包:

1
2
3
4
// API类依赖HttpClient
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});

注意到我们将容器本身作为解析器的一个参数,然后我们可以使用该容器来解析我们正在构建的对象的子依赖。

绑定一个单例

singleton方法绑定一个只会解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个对象实例:

1
2
3
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});

绑定实例

你还可以使用instance方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:

1
2
3
$api = new HelpSpot\API(new HttpClient);

$this->app->instance('HelpSpot\API', $api);

绑定原始值

你可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:

1
2
3
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);

绑定接口到实现

服务容器的一个非常强大的功能是其绑定接口到实现。我们假设有一个EventPusher接口及其实现类RedisEventPusher,编写完该接口的RedisEventPusher实现后,就可以将其注册到服务容器:

1
2
3
4
$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);

这段代码告诉容器当一个类需要EventPusher的实现时将会注入RedisEventPusher,现在我们可以在构造器或者任何其它通过服务容器注入依赖的地方进行EventPusher接口的依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
use App\Contracts\EventPusher;

/**
* 创建一个新的类实例
*
* @param EventPusher $pusher
* @return void
*/
public function __construct(EventPusher $pusher)
{
$this->pusher = $pusher;
}

上下文绑定

有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖Illuminate\Contracts\Filesystem\Filesystem契约的不同实现。Laravel为此定义了简单、平滑的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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方法给它们分配一个标签:

1
2
3
4
5
6
7
8
9
$this->app->bind('SpeedReport', function () {
//
});

$this->app->bind('MemoryReport', function () {
//
});

$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');

这些服务被打上标签后,可以通过tagged方法来轻松解析它们:

1
2
3
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});

扩展绑定

extend方法允许对解析服务进行修改。例如,当服务被解析后,可以运行额外代码装饰或配置该服务。extend方法接收一个闭包来返回修改后的服务:

1
2
3
$this->app->extend(Service::class, function($service) {
return new DecoratedService($service);
});

解析

make方法

有很多方式可以从容器中解析对象,首先,你可以使用make方法,该方法接收你想要解析的类名或接口名作为参数:

1
$fooBar = $this->app->make('HelpSpot\API');

如果你所在的代码位置访问不了$app变量,可以使用辅助函数resolve:

1
$api = resolve('HelpSpot\API');

某些类的依赖不能通过容器来解析,你可以通过关联数组方式将其传递传递到makeWith方法来注入:

1
$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);

自动注入

最后,也是最常用的,你可以简单的通过在类的构造函数中对依赖进行类型提示来从容器中解析对象,控制器、事件监听器、中间件等都是通过这种方式。此外,你还可以在队列任务的handle方法中进行类型提示。在具体实践中,这是大多数对象从容器中解析的方式。

容器会自动为其解析类注入依赖,例如,你可以在控制器的构造函数中为应用定义的仓库进行类型提示,该仓库会自动解析并注入该类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace App\Http\Controllers;

use App\Users\Repository as UserRepository;

class UserController extends Controller{
/**
* 用户仓库实例
*/
protected $users;

/**
* 创建一个控制器实例
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}

/**
* 通过指定ID显示用户
*
* @param int $id
* @return Response
*/
public function show($id)
{
//
}
}

容器事件

服务容器在每一次解析对象时都会触发一个事件,可以使用resolving方法监听该事件:

1
2
3
4
5
6
7
$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"...
});

正如你所看到的,被解析的对象将会传递给回调函数,从而允许你在对象被传递给消费者之前为其设置额外属性。

PSR-11

Laravel的服务容器实现了PSR-11接口。所以,你可以通过类型提示PSR-11容器接口来获取Laravel容器的实例:

1
2
3
4
5
6
7
use Psr\Container\ContainerInterface;

Route::get('/', function (ContainerInterface $container) {
$service = $container->get('Service');

//
});

如果传入的标识不能被解析则会抛出异常。如果该标识未被绑定的话抛出的异常会是Psr\Container\NotFoundExceptionInterface接口实例;如果标识已绑定但未能成功解析则抛出的异常是Psr\Container\ContainerExceptionInterface 接口实例。

0%