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
41
namespace App\Http\Controllers;
use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;
class UserController extends Controller
{
/**
* User Repository 的实现。
*
* @var UserRepository
*/
protected $users;
/**
* 创建新的控制器实例。
*
* @param UserRepository $users
* @return void
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* 显示指定用户的详细信息。
*
* @param int $id
* @return Response
*/
public function show($id)
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}讲解:
在这个例子中,控制器 UserController 需要从数据源中获取 users 。
因此,我们要 注入 可以获取 users 的服务。
在这种情况下, UserRepository 可能是通过使用 Eloquent 来从数据库中获取 user 信息。
因为 UserRepository 是通过注入获取,所以我们可以容易地切换为其他实现。当测试应用程序时,我们还可以轻松地 「mock」 ,或创建假的 UserRepository 实例。服务容器: 管理类的以来和运行依赖注入的有效管理工具,
Laravel服务容器主要承担两个作用:绑定与解析,服务容器的结构如下:
绑定
所谓的绑定就是将接口与实现建立对应关系。几乎所有的服务容器绑定都是在服务提供者中完成,也就是在服务提供者中绑定。
如果一个类没有基于任何接口那么就没有必要将其绑定到容器。容器并不需要被告知如何构建对象,因为它会使用 PHP 的反射服务自动解析出具体的对象。
也就是说,如果需要依赖注入的外部资源如果没有接口,那么就不需要绑定,直接利用服务容器进行解析就可以了,服务容器会根据类名利用反射对其进行自动构造。
bind绑定
绑定有多种方法,首先最常用的是bind函数的绑定
绑定自身
1
$this->app->bind('App\Services\RedisEventPusher', null);
绑定闭包
1
2
3
4
5
6
7$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API();
});//闭包直接提供实现方式
$this->app->bind('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});//需要依赖注入绑定接口
1
2
3
4$this->app->bind(
'App\Contracts\EventPusher',
'App\Services\RedisEventPusher'
);singleton绑定
singleton 方法绑定一个只需要解析一次的类或接口到容器,然后接下来对容器的调用将会返回同一个实例:
1
2
3$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});instance绑定
我们还可以使用 instance 方法绑定一个已存在的对象实例到容器,随后调用容器将总是返回给定的实例:
1
2$api = new HelpSpot\API(new HttpClient);
$this->app->instance('HelpSpot\Api', $api);Context绑定
有时侯我们可能有两个类使用同一个接口,但我们希望在每个类中注入不同实现,例如,两个控制器依赖 Illuminate\Contracts\Filesystem\Filesystem 契约的不同实现。Laravel 为此定义了简单、平滑的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\VideoController;
use App\Http\Controllers\PhotoControllers;
use Illuminate\Contracts\Filesystem\Filesystem;
$this->app->when(StorageController::class)
->needs(Filesystem::class)
->give(function () {
Storage::class
});//提供类名
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return new Storage();
});//提供实现方式
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return new Storage($app->make(Disk::class));
});//需要依赖注入原始值绑定
我们可能有一个接收注入类的类,同时需要注入一个原生的数值比如整型,可以结合上下文轻松注入这个类需要的任何值:1
2
3$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);数组绑定
1
2
3app()['service'] = function(){
return new Service();
};标签绑定
少数情况下,我们需要解析特定分类下的所有绑定,例如,你正在构建一个接收多个不同 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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public function testExtendInstancesArePreserved()
{
$container = new Container;
$container->bind('foo', function () {
$obj = new StdClass;
$obj->foo = 'bar';
return $obj;
});
$obj = new StdClass;
$obj->foo = 'foo';
$container->instance('foo', $obj);
$container->extend('foo', function ($obj, $container) {
$obj->bar = 'baz';
return $obj;
});
$container->extend('foo', function ($obj, $container) {
$obj->baz = 'foo';
return $obj;
});
$this->assertEquals('foo', $container->make('foo')->foo);
$this->assertEquals('baz', $container->make('foo')->bar);
$this->assertEquals('foo', $container->make('foo')->baz);
}Rebounds与Rebinding
绑定是针对接口的,是为接口提供实现方式的方法。我们可以对接口在不同的时间段里提供不同的实现方法,一般来说,对同一个接口提供新的实现方法后,不会对已经实例化的对象产生任何影响。但是在一些场景下,在提供新的接口实现后,我们希望对已经实例化的对象重新做一些改变,这个就是 rebinding 函数的用途。
下面就是一个例子: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
36abstract class Car
{
public function __construct(Fuel $fuel)
{
$this->fuel = $fuel;
}
public function refuel($litres)
{
return $litres * $this->fuel->getPrice();
}
public function setFuel(Fuel $fuel)
{
$this->fuel = $fuel;
}
}
class JeepWrangler extends Car
{
//
}
interface Fuel
{
public function getPrice();
}
class Petrol implements Fuel
{
public function getPrice()
{
return 130.7;
}
}我们在服务容器中是这样对car接口和fuel接口绑定的:
1
2
3
4
5
6
7
8
9$this->app->bind('fuel', function ($app) {
return new Petrol;
});
$this->app->bind('car', function ($app) {
return new JeepWrangler($app['fuel']);
});
$this->app->make('car');如果car被服务容器解析实例化成对象之后,有人修改了 fuel 接口的实现,从 Petrol 改为 PremiumPetrol:
1
2
3$this->app->bind('fuel', function ($app) {
return new PremiumPetrol;
});由于 car 已经被实例化,那么这个接口实现的改变并不会影响到 car 的实现,假若我们想要 car 的成员变量 fuel 随着 fuel 接口的变化而变化,我们就需要一个回调函数,每当对 fuel 接口实现进行改变的时候,都要对 car 的 fuel 变量进行更新,这就是 rebinding 的用途:
1
2
3
4
5$this->app->bindShared('car', function ($app) {
return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
$app['car']->setFuel($fuel);
}));
});
今天先到这里,未来会继续学习和总结。