【翻訳】LaravelのDI(Dependency Injection )コンテナについて
Laravel has a powerful Inversion of Control (IoC) / Dependency Injection (DI) Container. Unfortunately the official documentation doesn't cover all of the available functionality, so I decided to experiment with it and document it for myself. The following is based on Laravel 5.4.26 - other versions may vary. Laravelには強力な制御反転(IoC)/依存性注入(DI)コンテナがあります。
残念ながら、公式ドキュメントに は利用可能な機能のすべてが網羅されているわけではないので、私はそれを試して自分でドキュメント化することにしました。以下はLaravel 5.4.26に基づいてい ます が、他のバージョンは異なるかもしれません。
DI入門(Introduction to Dependency Injection)
I won't attempt to explain the principles behind DI / IoC here - if you're not familiar with them you might want to read What is Dependency Injection? by Fabien Potencier (creator of the Symfony framework). 私はここでDI / IoCの背後にある原則を説明しようとはしません。
Accessing the Container
There are several ways to access the Container instance* within Laravel, but the simplest is to call the app() helper method:
Laravel内でContainerインスタンス*にアクセスする方法はいくつかありますが、最も簡単なのはapp() ヘルパーメソッドを呼び出すこと です。
code: (php)
$container = app();
I won't describe the other ways today - instead I want to focus on the Container class itself.
他の方法については説明しません。代わりにContainerクラス自体に注目したいと思います。
Note: If you read the official docs, it uses $this->app instead of $container. 公式のドキュメントでは、$this->appの代わりに、$containerが使われています。
(* In Laravel applications it's actually a subclass of Container called Application (which is why the helper is called app()), but for this post I'll only describe Container methods.) (* Laravelアプリケーションでは、実際にはApplicationというContainerのサブクラスです (これがヘルパーが呼び出される理由ですapp())が、この記事ではContainer メソッドについてのみ説明します。)
Using Illuminate\Container Outside Laravel
To use Container outside of Laravel, install it and then: code: (php)
use Illuminate\Container\Container;
$container = Container::getInstance();
Basic Usage
The simplest usage is to type hint your class's constructor with the classes you want injected:
最も簡単な使い方は、クラスのコンストラクタに注入したいクラスをタイプヒントとして入力することです
これはコンストラクタインジェクションとも呼ばれるものいです。
code: (php)
class MyClass
{
private $dependency;
public function __construct(AnotherClass $dependency)
{
$this->dependency = $dependency;
}
}
Then instead of using new MyClass, use the Container's make() method:
それからnew MyClass、使用する代わりに、コンテナのmake() メソッドを使用します 。
code: (php)
$instance = $container->make(MyClass::class);
The container will automatically instantiate the dependencies, so this is functionally equivalent to:
コンテナーは自動的に依存関係を保ったままインスタンス化するのでこれは機能的には以下と同等です。
code: (php)
$instance = new MyClass(new AnotherClass());
まさしく、コンストラクタインジェクションですね
(Except AnotherClass could have some dependencies of its own - in which case Container would recursively instantiate them until there were no more.)
例外 として、 AnotherClassはそれ自身の依存関係を持っている可能性があります - その場合Containerはそれ以上存在しなくなるまでそれらを再帰的にインスタンス化します)
Practical Example
練習してみましょう。
Here's a more practical example based on the PHP-DI docs - separating the mailer functionality from the user registration: これはPHP-DIドキュメントに基づいたより実用的な例です - メーラ機能をユーザ登録から切り離します:
code: (php)
class Mailer
{
public function mail($recipient, $content)
{
// Send an email to the recipient
// ...
}
}
code: (php)
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!');
}
}
code: (php)
use Illuminate\Container\Container;
$container = Container::getInstance();
$userManager = $container->make(UserManager::class);
$userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');
Binding Interfaces to Implementations
The Container makes it easy to code to an interface and then instantiate a concrete instance at runtime. First define the interfaces:
code: (php)
interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }
And declare the concrete classes implementing those interfaces. They may depend on other interfaces (or concrete classes as before):
code: (php)
class MyClass implements MyInterface
{
private $dependency;
public function __construct(AnotherInterface $dependency)
{
$this->dependency = $dependency;
}
}
Then use bind() to map each interface to a concrete class:
code: (php)
$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);
Finally pass the interface name instead of the class name to make():
code: (php)
$instance = $container->make(MyInterface::class);
Note: If you forget to bind an interface you will get a slightly cryptic fatal error instead:
code:_
Fatal error: Uncaught ReflectionException: Class MyInterface does not exist
This is because the container will try to instantiate the interface (new MyInterface), which isn't a valid class.
Practical Example
Here's a practical example of this - a swappable cache layer:
code: (php)
interface Cache
{
public function get($key);
public function put($key, $value);
}
code: (php)
class RedisCache implements Cache
{
public function get($key) { /* ... */ }
public function put($key, $value) { /* ... */ }
}
code: (php)
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;
}
}
code: (php)
use Illuminate\Container\Container;
$container = Container::getInstance();
$container->bind(Cache::class, RedisCache::class);
$result = $container->make(Worker::class)->result();
Binding Abstract & Concrete Classes
Binding can be also used with abstract classes:
code: (php)
$container->bind(MyAbstract::class, MyConcreteClass::class);
Or to replace a concrete class with a subclass:
code: (php)
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
Custom Bindings
If the class requires additional configuration you can pass a closure instead of a class name as the second parameter to bind():
code: (php)
$container->bind(Database::class, function (Container $container) {
return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});
Each time the Database interface is required, a new MySQLDatabase instance will be created and used, with the specified configuration values. (To share a single instance, see Singletons below.) The closure receives the Container instance as the first parameter, and it can be used to instantiate other classes if needed:
code: (php)
$container->bind(Logger::class, function (Container $container) {
$filesystem = $container->make(Filesystem::class);
return new FileLogger($filesystem, 'logs/error.log');
});
A closure can also be used to customise how a concrete class is instantiated:
code: (php)
$container->bind(GitHub\Client::class, function (Container $container) {
$client = new GitHub\Client;
$client->setEnterpriseUrl(GITHUB_HOST);
return $client;
});
Resolving Callbacks
Instead of overriding the binding completely, you can use resolving() to register a callback that's called after the binding is revolved:
code: (php)
$container->resolving(GitHub\Client::class, function ($client, Container $container) {
$client->setEnterpriseUrl(GITHUB_HOST);
});
If there are multiple callbacks, they will all be called. They also work for interfaces and abstract classes:
code: (php)
$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);
It is also possible to add a callback that's always called no matter what class is resolved - but I think it's probably only useful for logging / debugging:
code: (php)
$container->resolving(function ($object, Container $container) {
// ...
});
Extending a Class
Alternatively you can also use extend() to wrap a class and return a different object:
code: (php)
$container->extend(APIClient::class, function ($client, Container $container) {
return new APIClientDecorator($client);
});
The resulting object should still implement the same interface though, otherwise you'll get an error when using type hinting.
Singletons
With both automatic binding and bind(), a new instance will be created (or the closure will be called) every time it's needed. To share a single instance, use singleton() instead of bind():
code: (php)
$container->singleton(Cache::class, RedisCache::class);
Or with a closure:
code: (php)
$container->singleton(Database::class, function (Container $container) {
return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
});
To make a concrete class a singleton, pass that class with no second parameter:
code: (php)
$container->singleton(MySQLDatabase::class);
In each case, the singleton object will be created the first time it is needed, and then reused each subsequent time. If you already have an instance that you want to reuse, use the instance() method instead. For example, Laravel uses this to make sure the singleton Container instance is returned whenever it is injected into a class:
code: (php)
$container->instance(Container::class, $container);
Arbitrary Binding Names
You can use any arbitrary string instead of a class/interface name - although you won't be able to use type hinting to retrieve it and will have to use make() instead:
code: (php)
$container->bind('database', MySQLDatabase::class);
$db = $container->make('database');
To support both a class/interface and a short name simultaneously, use alias():
code: (php)
$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, 'cache');
$cache1 = $container->make(Cache::class);
$cache2 = $container->make('cache');
assert($cache1 === $cache2);
Storing Arbitrary Values
You can also use the container to store arbitrary values - e.g. configuration data:
code: (php)
$container->instance('database.name', 'testdb');
$db_name = $container->make('database.name');
It supports array access syntax, which makes this feel more natural:
code: (php)
When combined with closure bindings you can see why this could be useful:
code: (php)
$container->singleton('database', function (Container $container) {
return new MySQLDatabase(
);
});
(Laravel itself doesn't use the container for configuration - it uses a separate Config class instead - but PHP-DI does.) Tip: Array syntax can also be used instead of make() when instantiating objects:
code: (php)
Dependency Injection for Functions & Methods
So far we've seen DI for constructors, but Laravel also supports DI for arbitrary functions:
code: (php)
function do_something(Cache $cache) { /* ... */ }
$result = $container->call('do_something');
Additional parameters can be passed as an ordered or associative array:
code: (php)
function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }
// show_product($cache, 1)
$container->call('show_product', 1); // show_product($cache, 1, 'spec')
This can be used for any callable method:
Closures
code: (php)
$closure = function (Cache $cache) { /* ... */ };
$container->call($closure);
Static methods
code: (php)
class SomeClass
{
public static function staticMethod(Cache $cache) { /* ... */ }
}
code: (php)
// or:
$container->call('SomeClass::staticMethod');
Instance methods
code: (php)
class PostController
{
public function index(Cache $cache) { /* ... */ }
public function show(Cache $cache, $id) { /* ... */ }
}
code: (php)
$controller = $container->make(PostController::class);
Shortcut for Calling Instance Methods
There is a shortcut to instantiate a class and call a method in one go - use the syntax ClassName@methodName:
code: (php)
$container->call('PostController@index');
$container->call('PostController@show', 'id' => 4); The container is used to instantiate the class. This means:
1. Dependencies are injected into the constructor (as well as the method).
2. You can define the class as a singleton if you want it to be reused.
3. You can use an interface or arbitrary name instead of a concrete class.
For example, this will work:
code: (php)
class PostController
{
public function __construct(Request $request) { /* ... */ }
public function index(Cache $cache) { /* ... */ }
}
code: (php)
$container->singleton('post', PostController::class);
$container->call('post@index');
Finally, you can pass a "default method" as the third parameter. If the first parameter is a class name with no method specified, the default method will be called instead. Laravel uses this to implement event handlers: code: (php)
$container->call(MyEventHandler::class, $parameters, 'handle');
// Equivalent to:
$container->call('MyEventHandler@handle', $parameters);
Method Call Bindings
The bindMethod() method can be used to override a method call, e.g. to pass additional parameters:
code: (php)
$container->bindMethod('PostController@index', function ($controller, $container) {
$posts = get_posts(...);
return $controller->index($posts);
});
All of these will work, calling the closure instead of the original method:
code: (php)
$container->call('PostController@index');
$container->call('PostController', [], 'index');
However, any additional parameters to call() are not passed into the closure so they can't be used.
code: (php)
Contextual Bindings
Sometimes you want to use different implementations of an interface in different places. Here is an example adapted from the Laravel docs: code: (php)
$container
->when(PhotoController::class)
->needs(Filesystem::class)
->give(LocalFilesystem::class);
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(S3Filesystem::class);
Now both PhotoController and VideoController can depend on the Filesystem interface, yet each will receive a different implementation. You can also use a closure for give(), just as you can with bind():
code: (php)
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
Or a named dependency:
code: (php)
$container->instance('s3', $s3Filesystem);
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give('s3');
Binding Parameters to Primitives
You can also bind primitives (strings, integers, etc.) by passing a variable name to needs() (instead of an interface) and passing the value to give():
code: (php)
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(DB_USER);
You can use a closure to delay retrieving the value until it is needed:
code: (php)
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(function () {
return config('database.user');
});
Here you can't pass a class or a named dependency (e.g. give('database.user')) because it would be returned as a literal value - to do that you would have to use a closure instead:
code: (php)
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(function (Container $container) {
});
Tagging
You can use the container to "tag" related bindings:
code: (php)
$container->tag(MyPlugin::class, 'plugin');
$container->tag(AnotherPlugin::class, 'plugin');
And then retrieve all tagged instances as an array:
code: (php)
foreach ($container->tagged('plugin') as $plugin) {
$plugin->init();
}
Both tag() parameters also accept arrays:
code: (php)
Rebinding
Note: This is a little more advanced, and only rarely needed - feel free to skip over it!
A rebinding() callback is called when a binding or instance is changed after it has already been used - for example, here the session class is replaced after it has been used by the Auth class, so the Auth class needs to be informed of the change:
code: (php)
$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;
});
$auth = $container->make(Auth::class);
echo $auth->username(); // dave
echo $auth->username(); // danny
(For more information about rebinding, see here and here.) refresh()
There is also a shortcut method, refresh(), to handle this common pattern:
code: (php)
$container->singleton(Auth::class, function (Container $container) {
$auth = new Auth;
$auth->setSession($container->make(Session::class));
$container->refresh(Session::class, $auth, 'setSession');
return $auth;
});
It also returns the existing instance or binding (if there is one), so you can do this:
code: (php)
// 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;
});
(Personally I find this syntax more confusing and prefer the more verbose version above!)
Overriding Constructor Parameters
The makeWith() method allows you to pass additional parameters to the constructor. It ignores any existing instances or singletons, and can be useful for creating multiple instances of a class with different parameters while still injecting dependencies:
code: (php)
class Post
{
public function __construct(Database $db, int $id) { /* ... */ }
}
code: (php)
$post1 = $container->makeWith(Post::class, 'id' => 1); $post2 = $container->makeWith(Post::class, 'id' => 2); Other Methods
That covers all of the methods I think are useful - but just to round things off, here's a summary of the remaining public methods...
bound()
The bound() method returns true if the class or name has been bound with bind(), singleton(), instance() or alias().
code: (php)
if (! $container->bound('database.user')) {
// ...
}
You can also use the array access syntax and isset():
code: (php)
// ...
}
It can be reset with unset(), which removes the specified binding/instance/alias.
code: (php)
var_dump($container->bound('database.user')); // false
bindIf()
bindIf() does the same thing as bind(), except it only registers a binding if one doesn't already exist (see bound() above). It could potentially be used to register a default binding in a package while allowing the user to override it.
code: (php)
$container->bindIf(Loader::class, FallbackLoader::class);
There is no singletonIf() method, but you can use bindIf($abstract, $concrete, true) instead:
code: (php)
$container->bindIf(Loader::class, FallbackLoader::class, true);
Or write it out in full:
code: (php)
if (! $container->bound(Loader::class)) {
$container->singleton(Loader::class, FallbackLoader::class);
}
resolved()
The resolved() method returns true if a class has previously been resolved.
code: (php)
var_dump($container->resolved(Database::class)); // false
$container->make(Database::class);
var_dump($container->resolved(Database::class)); // true
I'm not sure what it's useful for... It is reset if unset() is used (see bound() above).
code: (php)
var_dump($container->resolved(Database::class)); // false
factory()
The factory() method returns a closure that takes no parameters and calls make().
code: (php)
$dbFactory = $container->factory(Database::class);
$db = $dbFactory();
I'm not sure what it's useful for...
wrap()
The wrap() method wraps a closure so that its dependencies will be injected when it is executed. The wrap method accepts an array of parameters; the returned closure takes no parameters:
code: (php)
$cacheGetter = function (Cache $cache, $key) {
return $cache->get($key);
};
$usernameGetter = $container->wrap($cacheGetter, 'username'); $username = $usernameGetter();
I'm not sure what it's useful for, since the closure takes no parameters...
afterResolving()
The afterResolving() method works exactly the same as resolving(), except the "afterResolving" callbacks are called after the "resolving" callbacks. I'm not sure when that would be useful...
And Finally...
isShared() - Determines if a given type is a shared singleton/instance
isAlias() - Determines if a given string is a registered alias
hasMethodBinding() - Determines if the container has a given method binding
getBindings() - Retrieves the raw array of all registered bindings
getAlias($abstract) - Resolves an alias to the underlying class/binding name
forgetInstance($abstract) - Clears a single instance object
forgetInstances() - Clears all instance objects
flush() - Clear all bindings and instances, effectively resetting the container
setInstance() - Replaces the instance used by getInstance() (Tip: Use setInstance(null) to clear it, so next time it will generate a new instance)
/icons/hr.icon
This article was originally posted on DaveJamesMiller.com on 15 June 2017.