Laravel's Dependency Injection Container in Depthphp
下面是中文翻译html
Laravel拥有强大的控制反转(IoC)/依赖注入(DI) 容器。不幸的是官方文档并无涵盖全部可用的功能,所以,我决定尝试写文档为本身记录一下。如下是基于Laravel 5.4.26,其余版本可能有所不一样。laravel
我不会尝试在这里解释DI/IOC背后的原理,若是你不熟悉它们,你可能须要去阅读由Fabien Potencier(Symfony框架做者)建立的什么是依赖注入git
在Laravel中有几种访问Container实例的方法,但最简单的方法是调用app()
helper方法:github
$container = app();
我今天不会描述其余方式,而是我想专一于Container类自己。数据库
注意: 若是你读了官方文档,它使用$this->app
代替$container
数组
(在Laravel应用程序中,它其实是Container的一个子类,称为Application这就是为何称为助手app()
,可是这篇文章,我只会描述Container方法)缓存
要在Laravel外使用Container,请安装它 session
而后:闭包
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!');
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();
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); });
结果对象仍然应该实现相同的接口,不然使用类型提示会出错。
在使用自动绑定和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'];
到如今为止,咱们已经看到了构造函数的依赖注入(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 语法.
这里涵盖了我认为有用的全部方法,但只是为了整理一些内容。下面这些是对其他共用方法的总结:
若是类或名称使用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()
和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()
方法返回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()
方法返回一个不带参数和调用的闭包make()
$dbFactory = $container->factory(Database::class); $db = $dbFactory();
我不肯定他有什么用处
该wrap()
方法封装了一个闭包,以便在其执行时注册他的依赖关系,wrap方法接收一个数组参数,返回的闭包不带参数:
$cacheGetter = function (Cache $cache, $key) { return $cache->get($key); }; $usernameGetter = $container->wrap($cacheGetter, ['username']); $username = $usernameGetter();
我不肯定他有什么用处,由于闭包不须要参数
Note: 此方法不是Container interface的一部分, 只是具体的 Container class.
afterResolving()
方法的做用和resolving()
相似,不一样的点是在resolving()
回调后调用afterResolving。我不肯定什么时候会用到。。。
isShared()
- 肯定给定类型是不是共享单例/实例isAlias()
- 肯定给定的字符串是不是已注册的别名hasMethodBinding()
- 肯定容器是否具备给定的方法绑定getBindings()
- 检索全部注册绑定的原始数组getAlias($abstract)
- 解析底层类/绑定名称的别名forgetInstance($abstract)
- 清除单个实例对象forgetInstances()
- 清除全部实例对象flush()
- 清除全部绑定和实例,有效的重置容器setInstance()
- 使用getInstance()
替换使用的实例_Note: 最后一节的方法都不是 Container interface.的一部分
本文最初发布于2017年6月15日的DaveJamesMiller.com