Laravel has a powerful IoC container, but, unfortunately, the official Laravel documentation does not describe all its capabilities. I decided to study it and document it for my own use.
The examples in this article are based on Laravel 5.4.26, other versions may vary.
I will not explain what DI and IoC are in this article - if you are not familiar with these principles, you can read the article " What is Dependency Injection? " From Fabien Potencier (the creator of the symfony framework).
In Laravel, there are several ways to get the container entity * and the simplest of them is to call the app()
helper:
$container = app();
I will not describe other ways, instead I will focus my attention on the container itself.
*
In Laravel, there is an Application class that inherits from Container (which is why the helper is called app()
), but in this article I will describe only the methods of the Container class.
To use the Laravel container outside of the framework, you need to install it using Composer, after which we can get the container like this:
use Illuminate\Container\Container; $container = Container::getInstance();
The easiest way to use a container is to specify in the constructor the classes that your class needs using type hinting:
class MyClass { private $dependency; public function __construct(AnotherClass $dependency) { $this->dependency = $dependency; } }
Then, instead of creating an object with the help of new MyClass
, let's call the container's make()
method:
$instance = $container->make(MyClass::class);
The container will automatically create and inject dependencies, which will be equivalent to the following code:
$instance = new MyClass(new AnotherClass());
(Except when AnotherClass
has its own dependencies. In this case, the container will automatically create and inject its dependencies, dependencies of its dependencies, etc.)
Below is a more realistic example, which is taken from the PHP-DI documentation. In it, the message sending logic is separated from the user registration logic:
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!');
First, let's define the interfaces:
interface MyInterface { /* ... */ } interface AnotherInterface { /* ... */ }
Then create the classes that implement these interfaces. They may depend on other interfaces (or other classes, as was previously the case):
class MyClass implements MyInterface { private $dependency; public function __construct(AnotherInterface $dependency) { $this->dependency = $dependency; } }
Now we will bind()
interfaces with the implementation using the bind()
method:
$container->bind(MyInterface::class, MyClass::class); $container->bind(AnotherInterface::class, AnotherClass::class);
And pass the interface name instead of the class name to the make()
method:
$instance = $container->make(MyInterface::class);
Note: If you forget to bind the interface to the implementation, you will get a slightly strange error:
Fatal error: Uncaught ReflectionException: Class MyInterface does not exist
This is because the container is trying to create an instance of the interface ( new MyInterface
), which is not a class.
Below is a real example of binding an interface to a specific implementation — a modifiable cache driver:
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 can also be used with an abstract class:
$container->bind(MyAbstract::class, MyConcreteClass::class);
Or to replace a class with its descendant (a class that inherits from it):
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
If the object requires additional configuration during creation, you can pass the closure with the second parameter to the bind()
method instead of the class name:
$container->bind(Database::class, function (Container $container) { return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS); });
Each time the Database class is requested, a new MySQLDatabase instance will be created with the specified configuration (if you need to have only one instance of the class, use the Singleton, described below).
The closure receives as the first parameter an instance of the Container class, which can be used to create other classes, if necessary:
$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 configure a class after creation:
$container->bind(GitHub\Client::class, function (Container $container) { $client = new GitHub\Client; $client->setEnterpriseUrl(GITHUB_HOST); return $client; });
Instead of completely rewriting the binding, we can use the resolving()
method to register callbacks that will be called after creating the required object:
$container->resolving(GitHub\Client::class, function ($client, Container $container) { $client->setEnterpriseUrl(GITHUB_HOST); });
If several callbacks have been registered, they will all be called. This also works for interfaces and abstract classes:
$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 register a callback, which will be called when creating any class (this can be useful for logging or debugging):
$container->resolving(function ($object, Container $container) { // ... });
You can also use the extend()
method to wrap the original class and return another object:
$container->extend(APIClient::class, function ($client, Container $container) { return new APIClientDecorator($client); });
The class of the returned object must implement the same interface as the class of the object being wrapped, otherwise you will get an error.
Every time a need arises in any class (if the name of a class or a binding created with the bind()
method is specified), a new instance of the required class is created (or a closure is called). In order to have only one instance of the class, you must call the singleton()
method instead of the bind()
method:
$container->singleton(Cache::class, RedisCache::class);
Closing example:
$container->singleton(Database::class, function (Container $container) { return new MySQLDatabase('localhost', 'testdb', 'user', 'pass'); });
In order to get a singleton from a class, you must pass it by omitting the second parameter:
$container->singleton(MySQLDatabase::class);
A singleton instance will be created only once, the same object will be used later.
If you already have an entity that you want to reuse, then use the instance()
method. For example, Laravel uses this to ensure that the Container class has only one instance:
$container->instance(Container::class, $container);
When binding, you can use an arbitrary string instead of the name of the class or interface, but you can no longer use type hinting and you will need to use the make()
method:
$container->bind('database', MySQLDatabase::class); $db = $container->make('database');
To have a class name and a short name at the same time, you can use the alias()
method:
$container->singleton(Cache::class, RedisCache::class); $container->alias(Cache::class, 'cache'); $cache1 = $container->make(Cache::class); $cache2 = $container->make('cache'); assert($cache1 === $cache2);
The container allows storing arbitrary values (for example, configuration data):
$container->instance('database.name', 'testdb'); $db_name = $container->make('database.name');
Also supported is the array-access syntax, which looks more familiar:
$container['database.name'] = 'testdb'; $db_name = $container['database.name'];
This can be useful when using it with binding-closure:
$container->singleton('database', function (Container $container) { return new MySQLDatabase( $container['database.host'], $container['database.name'], $container['database.user'], $container['database.pass'] ); });
(Laravel itself does not use a container for storing the configuration, for this there is a separate class - Config, but PHP-DI does this).
Tip: You can use the array-access syntax to create objects instead of the make()
method:
$db = $container['database'];
So far, we have used DI only for constructors, but Laravel also supports DI for arbitrary functions:
function do_something(Cache $cache) { /* ... */ } $result = $container->call('do_something');
Additional parameters can be passed as a simple or associative array:
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']);
DI can be used for any methods called:
$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]);
Container allows you to use an abbreviation of the form ClassName@methodName
to create an object and call its method. Example:
$container->call('PostController@index'); $container->call('PostController@show', ['id' => 4]);
The container is used to create an instance of a class, i.e .:
The example below will work:
class PostController { public function __construct(Request $request) { /* ... */ } public function index(Cache $cache) { /* ... */ } }
$container->singleton('post', PostController::class); $container->call('post@index');
Finally, you can pass the name of the "default method" as the third parameter. If the first parameter is the class name and the method name is not specified, the default method will be called. Laravel uses this in event handlers:
$container->call(MyEventHandler::class, $parameters, 'handle'); // Equivalent to: $container->call('MyEventHandler@handle', $parameters);
The bindMethod()
method allows you to override the method call, for example, to pass parameters:
$container->bindMethod('PostController@index', function ($controller, $container) { $posts = get_posts(...); return $controller->index($posts); });
All the examples below will work, and a closure instead of the present method will be called:
$container->call('PostController@index'); $container->call('PostController', [], 'index'); $container->call([new PostController, 'index']);
However, any additional parameters passed to the call()
method will not be passed to the closure and they cannot be used:
$container->call('PostController@index', ['Not used :-(']);
Notes: The bindMethod()
method is not part of the Container interface, it is only in the Container class. See Pull Request for a description of why parameters are not passed during the override.
It may happen that you want to have different implementations of the same interface depending on where it is needed. The following is a slightly modified example from the Laravel documentation :
$container ->when(PhotoController::class) ->needs(Filesystem::class) ->give(LocalFilesystem::class); $container ->when(VideoController::class) ->needs(Filesystem::class) ->give(S3Filesystem::class);
Now the PhotoController and VideoController controllers may depend on the Filesystem interface, but each of them will get its own implementation of this interface.
You can also pass a closure to the give()
method, as you do in the bind()
method:
$container ->when(VideoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });
Or you can use a named dependency:
$container->instance('s3', $s3Filesystem); $container ->when(VideoController::class) ->needs(Filesystem::class) ->give('s3');
In addition to objects, the container allows for the binding of primitive types (strings, numbers, etc.). To do this, pass the variable name (instead of the interface name) to the needs()
method, and the give()
method to pass the value that will be substituted by the container when the method is called:
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(DB_USER);
We can also pass the closure to the give()
method, in order to postpone the calculation of the value until it is needed:
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(function () { return config('database.user'); });
We cannot pass a class name or a named dependency to a give()
method (for example, give('database.user')
), because it will be returned as it is. But we can use the closure:
$container ->when(MySQLDatabase::class) ->needs('$username') ->give(function (Container $container) { return $container['database.user']; });
You can use the container to add tags to related (as intended) binders:
$container->tag(MyPlugin::class, 'plugin'); $container->tag(AnotherPlugin::class, 'plugin');
And then get an array of entities with the specified tag:
foreach ($container->tagged('plugin') as $plugin) { $plugin->init(); }
Both parameters of the tag()
method also accept an array:
$container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin'); $container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);
Note : this feature of the container is used quite rarely, so you can safely skip its description.
A callback registered using the rebinding()
method is called when the binding is changed. In the example below, the session was replaced after it was used by the Auth class, so the Auth class should be informed about the change:
$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
More information on this topic can be found here and here .
There is also an abbreviation that can be useful in some cases - the refresh()
method:
$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 an existing instance of a class or binding (if it exists), so you can do this:
// , `singleton()` `bind()` $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, this syntax seems a bit confusing to me, so I prefer a more detailed version, which is described above.
Note: these methods are not part of the Container interface, they are only in the Container class.
The makeWith()
method allows you to pass additional parameters to the constructor. At the same time, existing instances or singletons are ignored (that is, a new object is created). This can be useful when creating objects with different parameters and that have any dependencies:
class Post { public function __construct(Database $db, int $id) { /* ... */ } }
$post1 = $container->makeWith(Post::class, ['id' => 1]); $post2 = $container->makeWith(Post::class, ['id' => 2]);
Note: In Laravel> = 5.3, this method is simply called make($class, $parameters)
. It was removed in Laravel 5.4, but then returned back under the name makeWith
in version 5.4.16. It seems that in Laravel 5.5 its name will be changed to make()
again .
I described all the methods that seemed useful to me, but for the sake of completeness, I will describe the remaining available methods.
The bound()
method checks if there is a class or alias associated with the bind()
, singleton()
, instance()
or alias()
methods:
if (! $container->bound('database.user')) { // ... }
You can also use the isset method and the array-access syntax:
if (! isset($container['database.user'])) { // ... }
The value specified in the methods binding()
, instance()
, alias()
can be removed using unset()
:
unset($container['database.user']); var_dump($container->bound('database.user')); // false
The bindIf()
method does the same thing as the bind()
method, except that it creates a binding only if it does not exist (see the description of the bound()
method above). Theoretically, it can be used in a package to register a binding by default, allowing the user to override it.
$container->bindIf(Loader::class, FallbackLoader::class);
There is no singletonIf()
method; instead, you can use bindIf ($ abstract, $ concrete, true):
$container->bindIf(Loader::class, FallbackLoader::class, true);
Or write the verification code yourself:
if (! $container->bound(Loader::class)) { $container->singleton(Loader::class, FallbackLoader::class); }
The resolved()
method returns true if an instance of the class was previously created.
var_dump($container->resolved(Database::class)); // false $container->make(Database::class); var_dump($container->resolved(Database::class)); // true
It is reset when the unset()
method is called (see the description of the bound()
method above).
unset($container[Database::class]); var_dump($container->resolved(Database::class)); // false
The factory()
method returns a closure that takes no parameters and calls the make()
method when invoked.
$dbFactory = $container->factory(Database::class); $db = $dbFactory();
The wrap()
method wraps the closure in yet another closure that will inject the dependencies into the wrapper that is being called. The method takes an array of parameters that will be passed to the wrapped closure; the return closure does not accept any parameters:
$cacheGetter = function (Cache $cache, $key) { return $cache->get($key); }; $usernameGetter = $container->wrap($cacheGetter, ['username']); $username = $usernameGetter();
Note: the wrap()
method is not part of the Container interface, it is only in the Container class.
The afterResolving()
method works in the same way as the resolving()
method, with the exception that callbacks registered with it are called after callbacks registered using the resolving()
method.
isShared()
- Checks if a singleton / instance exists for the specified type.isAlias()
- Checks if an alias exists with the specified name.hasMethodBinding()
- Checks if there is a binding for the specified method in the containergetBindings()
- Returns an array of all registered bindings.getAlias($abstract)
- Returns the alias for the specified class / bindingforgetInstance($abstract)
- Removes the specified class instance from the container.forgetInstances()
- Removes all instances of classes.flush()
- Removes all bindings and created instances of classes, completely clearing the containersetInstance()
- Replaces the object returned by getInstance () (hint: use setInstance(null)
to clean up, a new container instance will be created later)
Note: None of these methods are part of the Container interface.
Source: https://habr.com/ru/post/331982/
All Articles