Laravel's Dependency Injection Container in Depth 点击预览
下面是中文翻译
Laravel 拥有强大的控制反转(IoC)/ 依赖注入(DI) 容器不幸的是官方文档并没有涵盖所有可用的功能, 因此, 我决定尝试写文档为自己记录一下以下是基于 Laravel 5.4.26, 其他版本可能有所不同
依赖注入简介
我不会尝试在这里解释 DI/IOC 背后的原理, 如果你不熟悉它们, 你可能需要去阅读由 Fabien Potencier(Symfony 框架作者)创建的什么是依赖注入
访问容器
在 Laravel 中有几种访问 Container 实例的方法, 但最简单的方法是调用 app()helper 方法:
$container = app();
我今天不会描述其他方式, 而是我想专注于 Container 类本身
注意: 如果你读了官方文档, 它使用 $this->app 代替 $container
(在 Laravel 应用程序中, 它实际上是 Container 的一个子类, 称为 Application 这就是为什么称为助手 app(), 但是这篇文章, 我只会描述 Container 方法)
在 Laravel 外使用 IlluminateContainer
要在 Laravel 外使用 Container, 请安装它
然后:
- use Illuminate\Container\Container;
- $container = Container::getInstance();
基本用法
最简单的用法是用你想注入的类键入你的类的构造函数:
- class MyClass
- {
- private $dependency;
- public function __construct(AnotherClass $dependency)
- {
- $this->dependency = $dependency;
- }
- }
然后 new MyClass 使用容器的 make()方法
$instance = $container->make(MyClass::class);
容器会自动实例化依赖关系, 所以这在功能上等同于:
$instance = new MyClass(new AnotherClass());
(除了 AnotherClass 他自己的一些依赖关系, 在这种情况下 Container 将递归实例化它们, 直到没有更多)
实例
以下是一个基于 PHP-DI docs 的更实用的示例, 将邮件功能与用户注册分离:
- class Mailer
- {
- public function mail($recipient, $content)
- {
- // Send an email to the recipient
- // ...
- }
- }
- class UserManager
- {
- private $mailer;
- public function __construct(Mailer $mailer)
- {
- $this->mailer = $mailer;
- }
- public function register($email, $password)
- {
- // Create the user account
- // ...
- // Send the user an email to say hello!
- $this->mailer->mail($email, 'Hello and welcome!');
- }
- }
- use Illuminate\Container\Container;
- $container = Container::getInstance();
- $userManager = $container->make(UserManager::class);
- $userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');
将接口 (Interfaces) 绑定到实现(Implementations)
Container 可以很容易的编写一个接口, 然后在运行时实例化一个具体的实现, 首先定义接口:
- interface MyInterface { /* ... */ }
- interface AnotherInterface { /* ... */ }
并声明实现这些接口的具体类, 他们可能依赖于其他接口(或以前的具体类)
- class MyClass implements MyInterface
- {
- private $dependency;
- public function __construct(AnotherInterface $dependency)
- {
- $this->dependency = $dependency;
- }
- }
然后使用 bind()去将每个接口映射到具体的类
- $container->bind(MyInterface::class, MyClass::class);
- $container->bind(AnotherInterface::class, AnotherClass::class);
最后通过将接口名代替类名去传递给 make()
$instance = $container->make(MyInterface::class);
注意: 如果你忘记去绑定一个接口, 你将会得到一个稍微神秘的致命错误:
Fatal error: Uncaught ReflectionException: Class MyInterface does not exist
这是因为容器会尝试实例化 interface (new MyInterface), 而这不是一个有效的类
实例
下面是一个实用的例子, 一个可交换的缓存层
- interface Cache
- {
- public function get($key);
- public function put($key, $value);
- }
- class RedisCache implements Cache
- {
- public function get($key) { /* ... */ }
- public function put($key, $value) { /* ... */ }
- }
- class Worker
- {
- private $cache;
- public function __construct(Cache $cache)
- {
- $this->cache = $cache;
- }
- public function result()
- {
- // Use the cache for something...
- $result = $this->cache->get('worker');
- if ($result === null) {
- $result = do_something_slow();
- $this->cache->put('worker', $result);
- }
- return $result;
- }
- }
- use Illuminate\Container\Container;
- $container = Container::getInstance();
- $container->bind(Cache::class, RedisCache::class);
- $result = $container->make(Worker::class)->result();
绑定抽象类和具体类(Abstract & Concrete Classes)
Binding 也可以使用到 abstract 类:
$container->bind(MyAbstract::class, MyConcreteClass::class);
或者用一个子类替换一个具体的类:
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
自定义绑定
如果该类需要额外的配置, 你可以传递一个闭包来代替类名作为 bind()的第二个参数:
- $container->bind(Database::class, function (Container $container) {
- return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
- });
每次需要数据库接口时, 都会创建并使用一个新的 MySQLDatabase 实例, 并使用指定的配置值 (要想共享单个实例, 请参考下面的单例) 闭包接收 Container 实例作为第一个参数, 并且可以在需要时用于实例化其他类:
- $container->bind(Logger::class, function (Container $container) {
- $filesystem = $container->make(Filesystem::class);
- return new FileLogger($filesystem, 'logs/error.log');
- });
闭包也可以用来定制具体类如何实例化
- $container->bind(GitHub\Client::class, function (Container $container) {
- $client = new GitHub\Client;
- $client->setEnterpriseUrl(GITHUB_HOST);
- return $client;
- });
解决回调
你可以使用 resolving()去注册一个用于绑定完成后的回调函数:
- $container->resolving(GitHub\Client::class, function ($client, Container $container) {
- $client->setEnterpriseUrl(GITHUB_HOST);
- });
如果有多个回调, 它们将全部被调用, 它们也为接口和抽象类工作
- $container->resolving(Logger::class, function (Logger $logger) {
- $logger->setLevel('debug');
- });
- $container->resolving(FileLogger::class, function (FileLogger $logger) {
- $logger->setFilename('logs/debug.log');
- });
- $container->bind(Logger::class, FileLogger::class);
- $logger = $container->make(Logger::class);
也可以通过添加一个回调来处理无论是哪个类被解析, 总是调用该回调函数但是我认为他可能只能在日志 / 调试中使用:
- $container->resolving(function ($object, Container $container) {
- // ...
- });
扩展一个类
或者你可以使用 extend()包装类并返回一个不同的对象:
- $container->extend(APIClient::class, function ($client, Container $container) {
- return new APIClientDecorator($client);
- });
结果对象仍然应该实现相同的接口, 否则使用类型提示会出错
单例(Singletons)
在使用自动绑定和 bind()时, 每次需要时都会创建一个新的实例 (或者调用闭包) 想要共享一个实例, 使用 singleton() 代替 bind():
$container->singleton(Cache::class, RedisCache::class);
或者使用一个闭包:
- $container->singleton(Database::class, function (Container $container) {
- return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
- });
要让一个具体的类成为实例, 请传递该类且不需要传递第二个参数:
$container->singleton(MySQLDatabase::class);
在不同情况下, 单例对象将在第一次需要时创建, 然后在随后每次需要时重用如果你已经有一个实例, 你想重用使用 instance()方法代替例如, Laravel 使用它来确保无论什么时候将单实例 Container 实例注入到类中都会返回它:
$container->instance(Container::class, $container);
任意绑定名称
你可以使用任意字符串而不是使用一个类 / 接口名称, 尽管你不能使用类型提示检索它, 但必须使用 make()代替:
- $container->bind('database', MySQLDatabase::class);
- $db = $container->make('database');
要同时支持类 / 接口, 请使用 alias()
- $container->singleton(Cache::class, RedisCache::class);
- $container->alias(Cache::class, 'cache');
- $cache1 = $container->make(Cache::class);
- $cache2 = $container->make('cache');
- assert($cache1 === $cache2);
存储任意值
你也可以使用容器来存储任意值, 例如配置数据:
- $container->instance('database.name', 'testdb');
- $db_name = $container->make('database.name');
它支持数组语法访问, 这使得他更自然:
- $container['database.name'] = 'testdb';
- $db_name = $container['database.name'];
当与闭包函数结合使用时, 你可以看到为什么这是有用的:
- $container->singleton('database', function (Container $container) {
- return new MySQLDatabase(
- $container['database.host'],
- $container['database.name'],
- $container['database.user'],
- $container['database.pass']
- );
- });
(Laravel 本是不使用容器进行配置, 它使用一个单独的 Config 类来代替, 但是也是通过 PHP-DI 实现的)
Tip: 在实例化对象的时候, 也可以使用数组语法代替 make():
$db = $container['database'];
函数和方法 (Functions & Methods) 的依赖注入
到现在为止, 我们已经看到了构造函数的依赖注入(DI), 但是 Laravel 还支持任意函数的依赖注入(DI):
- function do_something(Cache $cache) { /* ... */ }
- $result = $container->call('do_something');
其他参数可以作为索引或关联数组传递:
- function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }
- // show_product($cache, 1)
- $container->call('show_product', [1]);
- $container->call('show_product', ['id' => 1]);
- // show_product($cache, 1, 'spec')
- $container->call('show_product', [1, 'spec']);
- $container->call('show_product', ['id' => 1, 'tab' => 'spec']);
这可以用于任意可调用的方法:
闭包
- $closure = function (Cache $cache) { /* ... */ };
- $container->call($closure);
静态方法
- class SomeClass
- {
- public static function staticMethod(Cache $cache) { /* ... */ }
- }
- $container->call(['SomeClass', 'staticMethod']);
- // or:
- $container->call('SomeClass::staticMethod');
实例方法
- class PostController
- {
- public function index(Cache $cache) { /* ... */ }
- public function show(Cache $cache, $id) { /* ... */ }
- }
- $controller = $container->make(PostController::class);
- $container->call([$controller, 'index']);
- $container->call([$controller, 'show'], ['id' => 1]);
调用实例方法的快捷方式
有一个快捷方式来实例化一个类并一次调用一个方法, 使用
- ClassName@methodName
- :
- $container->call('PostController@index');
- $container->call('PostController@show', ['id' => 4]);
该容器用于实例化类, 即:
依赖项注入到构造函数 (以及方法) 中
如果你希望重用它, 你可以将该类定义为单例
你可以使用接口或任意名称而不是具体类
例如:
- class PostController
- {
- public function __construct(Request $request) { /* ... */ }
- public function index(Cache $cache) { /* ... */ }
- }
- $container->singleton('post', PostController::class);
- $container->call('post@index');
最后, 你可以传递一个默认方法作为第三个参数, 如果第一个参数是没有指定方法的类名, 则会调用默认方法, Laravel 使用它来实现事件处理
- $container->call(MyEventHandler::class, $parameters, 'handle');
- // Equivalent to:
- $container->call('MyEventHandler@handle', $parameters);
方法调用绑定
bindMethod()方法可以用于重写方法调用, 例如传递其他参数:
- $container->bindMethod('PostController@index', function ($controller, $container) {
- $posts = get_posts(...);
- return $controller->index($posts);
- });
所有这些都可以通过使用闭包代替原始方法进行工作:
- $container->call('PostController@index');
- $container->call('PostController', [], 'index');
- $container->call([new PostController, 'index']);
但是, 任何多余传递给 call()的参数都不会传递到闭包中, 因此无法使用他们
$container->call('PostController@index', ['Not used :-(']);
_Notes: 该方法不是 Container interface 的一部分, 只适用于具体的 Container 类为什么忽略参数, 请参阅 PR
上下文绑定
有时候你想在不同的地方使用不同的接口实现, 下面是 Laravel 文档中的一个例子:
- $container
- ->when(PhotoController::class)
- ->needs(Filesystem::class)
- ->give(LocalFilesystem::class);
- $container
- ->when(VideoController::class)
- ->needs(Filesystem::class)
- ->give(S3Filesystem::class);
现在, PhotoController 和 VideoController 都可以依赖文件系统接口, 但是每个接口都会接受到不同的实现, 你也可以像使用 bind()一样使用闭包 give()
- $container
- ->when(VideoController::class)
- ->needs(Filesystem::class)
- ->give(function () {
- return Storage::disk('s3');
- });
或者一个命名的依赖关系:
- $container->instance('s3', $s3Filesystem);
- $container
- ->when(VideoController::class)
- ->needs(Filesystem::class)
- ->give('s3');
将参数绑定到原函数
你也可以通过传递变量名称给 needs()(而不是接口)和传递变量给 give()来绑定原函数
- $container
- ->when(MySQLDatabase::class)
- ->needs('$username')
- ->give(DB_USER);
你可以使用闭包来延迟检索值直到需要用到它:
- $container
- ->when(MySQLDatabase::class)
- ->needs('$username')
- ->give(function () {
- return config('database.user');
- });
在这里, 你不能传递一个类或者一个命名依赖(例如
give('database.user')
), 因为它会作为一个字面值返回, 要做到这一点, 你将不得不使用闭包:
- $container
- ->when(MySQLDatabase::class)
- ->needs('$username')
- ->give(function (Container $container) {
- return $container['database.user'];
- });
做标记
你可以使用容器去标记相关的绑定:
- $container->tag(MyPlugin::class, 'plugin');
- $container->tag(AnotherPlugin::class, 'plugin');
然后以数组方式检索所有标记的实例:
- foreach ($container->tagged('plugin') as $plugin) {
- $plugin->init();
- }
tag()的两个参数也可以传递数组:
- $container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin');
- $container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);
重新绑定
_Note: 这个更高级一点, 但是很少用到, 可以跳过它
打工绑定或者实例已经被使用后, rebinding()调用一个回调函数例如, 这里的 session 类在被 Auth 类使用后被替换, 所以 Auth 需要被告知更改:
- $container->singleton(Auth::class, function (Container $container) {
- $auth = new Auth;
- $auth->setSession($container->make(Session::class));
- $container->rebinding(Session::class, function ($container, $session) use ($auth) {
- $auth->setSession($session);
- });
- return $auth;
- });
- $container->instance(Session::class, new Session(['username' => 'dave']));
- $auth = $container->make(Auth::class);
- echo $auth->username(); // dave
- $container->instance(Session::class, new Session(['username' => 'danny']));
- echo $auth->username(); // danny
(有关重新绑定的更多信息, 请查看 这里 和 这里.)
刷新
还有一种更便捷的方法来处理这种模式, 通过 refresh()
- $container->singleton(Auth::class, function (Container $container) {
- $auth = new Auth;
- $auth->setSession($container->make(Session::class));
- $container->refresh(Session::class, $auth, 'setSession');
- return $auth;
- });
它也返回现有的实例或绑定(如果有的话), 所以你可以这样做:
- // This only works if you call singleton() or bind() on the class
- $container->singleton(Session::class);
- $container->singleton(Auth::class, function (Container $container) {
- $auth = new Auth;
- $auth->setSession($container->refresh(Session::class, $auth, 'setSession'));
- return $auth;
- });
(我个人觉得这个语法更令人困惑, 并且更喜欢上面的更详细的版本)
Note: 这些方法不是 Container interface 的一部分, 只是具体的 Container class.
重写构造函数参数
该 makeWith()方法允许您将其他参数传递给构造函数, 她忽略了任何现有的实例或单例, 并且可以用于创建具有不同参数的类的多个实例, 同时依然注入依赖关系:
- class Post
- {
- public function __construct(Database $db, int $id) { /* ... */ }
- }
- $post1 = $container->makeWith(Post::class, ['id' => 1]);
- $post2 = $container->makeWith(Post::class, ['id' => 2]);
Note: 在 Laravel 5.3 以及以下版本中, 它很简单
make($class, $parameters)
, 但在 Laravel 5.4 中被删除, 但在 5.4.16 被重新添加为 makeWith() 在 Laravel 5.5 可能会 恢复到 Laravel 5.3 语法.
其他方法
这里涵盖了我认为有用的所有方法, 但只是为了整理一些内容下面这些是对其余共用方法的总结:
bound()
如果类或名称使用 bind(), singleton(), instance() 或 alias()绑定, bound()将会返回 true
- if (! $container->bound('database.user')) {
- // ...
- }
你还可以使用数组语法和 isset()访问:
- if (! isset($container['database.user'])) {
- // ...
- }
它可以使用 unset()重置删除指定的绑定 / 实例 / 别名
- unset($container['database.user']);
- var_dump($container->bound('database.user')); // false
- bindIf()
bindIf()和 bind()相同, 除了他只在不存在绑定的情况下才回注册绑定(请参见上面的 bound()), 它可以用于在包注册中默认绑定, 同事允许用户覆盖它:
$container->bindIf(Loader::class, FallbackLoader::class);
没有 singletonIf()方法, 但是你可以使用
bindIf($abstract, $concrete, true)
实现它:
$container->bindIf(Loader::class, FallbackLoader::class, true);
或者全部写出来:
- if (! $container->bound(Loader::class)) {
- $container->singleton(Loader::class, FallbackLoader::class);
- }
- resolved()
如果一个类已经被解析, resolved()方法返回 true
- var_dump($container->resolved(Database::class)); // false
- $container->make(Database::class);
- var_dump($container->resolved(Database::class)); // true
我不确定他有什么用处, 如果使用 unset()它会被重置(请看上面的 bound())
- unset($container[Database::class]);
- var_dump($container->resolved(Database::class)); // false
- factory()
该 factory()方法返回一个不带参数和调用的闭包 make()
- $dbFactory = $container->factory(Database::class);
- $db = $dbFactory();
我不确定他有什么用处
wrap()
该 wrap()方法封装了一个闭包, 以便在其执行时注册他的依赖关系, wrap 方法接收一个数组参数, 返回的闭包不带参数:
- $cacheGetter = function (Cache $cache, $key) {
- return $cache->get($key);
- };
- $usernameGetter = $container->wrap($cacheGetter, ['username']);
- $username = $usernameGetter();
我不确定他有什么用处, 因为闭包不需要参数
Note: 此方法不是 Container interface 的一部分, 只是具体的 Container class.
afterResolving()
afterResolving()方法的作用和 resolving()类似, 不同的点是在 resolving()回调后调用 afterResolving 我不确定何时会用到
最后
isShared() - 确定给定类型是否是共享单例 / 实例
isAlias() - 确定给定的字符串是否是已注册的别名
hasMethodBinding() - 确定容器是否具有给定的方法绑定
getBindings() - 检索所有注册绑定的原始数组
getAlias($abstract)
- 解析底层类 / 绑定名称的别名
forgetInstance($abstract)
- 清除单个实例对象
forgetInstances() - 清除所有实例对象
flush() - 清除所有绑定和实例, 有效的重置容器
setInstance() - 使用 getInstance()替换使用的实例
_Note: 最后一节的方法都不是 Container interface. 的一部分
来源: https://juejin.im/entry/5a9664e86fb9a063535074b6