Laravel IoC container

Lecture



Laravel has a powerful IoC container, but unfortunately, the official Laravel documentation does not describe all of its features. 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.

Introduction to Dependency Injection

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? ” By Fabien Potencier (creator of the Symfony framework ).

Receiving a container (Container)

In Laravel, there are several ways to get the container entity * and the simplest one is calling the helper app():

$container = app();

I will not describe other ways; instead, I will focus on the container itself.

*Laravel has an Application class that inherits from Container (which is why the helper is called app()), but in this article I will only describe methods of the Container class .

Using Illuminate \ Container outside Laravel

To use the Laravel container outside 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();

Usage example

The easiest way to use the 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 new MyClass, we call the container method make():

$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 it AnotherClasshas its dependencies. In this case, the container will automatically create and implement its dependencies, dependencies of its dependencies, etc.)

Real example

The following 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!');

Interface and implementation binding

First, we define the interfaces:

interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }

Then create classes that implement these interfaces. They may depend on other interfaces (or other classes, as it was before):

class MyClass implements MyInterface
{
    private $dependency;

    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

Now we will connect the interfaces to the implementation using the method bind():

$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);

And pass the name of the interface instead of the class name to the method make():

$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 instantiate an interface ( new MyInterface) that is not a class.

Real example

The following is a real-world example of associating an interface with 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();

Linking abstract and concrete classes

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);

Custom bindings

If the object requires additional configuration during creation, you can pass the closure with the second parameter to the method bind()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 queried, a new instance of MySQLDatabase will be created with the specified configuration (if you need to have only one instance of the class, use Singleton, which is described below).

The closure receives an instance of the Container class as the first parameter, 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');
});

Closing can also be used to set up a class after creation:

$container->bind(GitHub\Client::class, function (Container $container) {
    $client = new GitHub\Client;
    $client->setEnterpriseUrl(GITHUB_HOST);

    return $client;
});

Resolving Callbacks

Instead of completely rewriting the binding, we can use the method resolving()to register callbacks that will be called after creating the desired 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 that will be called when creating any class (this can be useful for logging or debugging):

$container->resolving(function ($object, Container $container) {
    // ...
});

Class extension

You can also use the method extend()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 wrapped object, otherwise you will get an error.

Singleton

Each time there is a need for a class (if the name of the class or binding created using the method bind()is specified), a new instance of the required class is created (or a closure is called). In order to have only one instance of a class, you must call the method singleton()instead of the method bind():

$container->singleton(Cache::class, RedisCache::class);

Lock 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 need to pass it by omitting the second parameter:

$container->singleton(MySQLDatabase::class);

A singleton instance will be created only once; in the future, the same object will be used.

If you already have an entity that you want to reuse, then use the method instance(). For example, Laravel uses this so that the Container class has only one instance:

$container->instance(Container::class, $container);

Custom Binding Name

When binding, you can use an arbitrary string instead of the name of the class or interface, however, you will no longer be able to use type hinting and you will have to use the method make():

$container->bind('database', MySQLDatabase::class);

$db = $container->make('database');

In order to have a class name and a short name at the same time, you can use the method alias():

$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, 'cache');

$cache1 = $container->make(Cache::class);
$cache2 = $container->make('cache');

assert($cache1 === $cache2);

Saving an arbitrary value

The container allows you to store arbitrary values ​​(for example, configuration data):

$container->instance('database.name', 'testdb');

$db_name = $container->make('database.name');

Array-access syntax is also supported, which looks more familiar:

$container['database.name'] = 'testdb';

$db_name = $container['database.name'];

This can be useful when used with closure binding:

$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 array-access syntax to create objects instead of a method make():

$db = $container['database'];

Dependency Injection for Functions and Methods

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 called methods:

Short circuits

$closure = function (Cache $cache) { /* ... */ };

$container->call($closure);

Static methods

class SomeClass
{
    public static function staticMethod(Cache $cache) { /* ... */ }
}

$container->call(['SomeClass', 'staticMethod']);
// or:
$container->call('SomeClass::staticMethod');

Object Methods

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]);

Abbreviations for calling object methods

Container allows you to use view abbreviations ClassName@methodNameto 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 the class, i.e.:

  1. Dependencies are passed to the constructor of the class, as well as to the called method
  2. You can declare a class as a singleton if you want to reuse the same object
  3. You can use an interface or an arbitrary name instead of the class name

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);

Substitution of object methods

The method bindMethod()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, this will cause a closure instead of the real method:

$container->call('PostController@index');
$container->call('PostController', [], 'index');
$container->call([new PostController, 'index']);

However, any additional parameters passed to the method call()will not be passed to the closure and they cannot be used:

$container->call('PostController@index', ['Not used :-(']);

Notes: the method is bindMethod()not part of the Container interface , it is only in the Container class . See Pull Request for an explanation of why parameters are not passed when overridden.

Context Based Binding

It may happen that you want to have different implementations of the same interface, depending on where you need it. 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 can depend on the Filesystem interface, but each of the bottom will receive its own implementation of this interface.

You can also pass the closure to the method give(), as you do in the method bind():

$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');

Binding parameters to primitive types

In addition to objects, the container allows binding of primitive types (strings, numbers, etc.). To do this, pass the variable name (instead of the interface name) to the method needs(), and give()pass the value to the method , which 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 method give(), in order to defer the calculation of the value until it is needed:

$container
    ->when(MySQLDatabase::class)
    ->needs('$username')
    ->give(function () {
        return config('database.user');
    });

We cannot pass the give()class name or named dependency (for example give('database.user')) to the method , because it will be returned as is. But we can use the closure:

$container
    ->when(MySQLDatabase::class)
    ->needs('$username')
    ->give(function (Container $container) {
        return $container['database.user'];
    });

Adding tags to binders

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 method tag()also accept an array:

$container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin');
$container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);

Rebinding

Note : this container feature is rarely used, so you can safely skip its description.

A callback registered using the method rebinding()is called when the binding changes. 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 .

refresh ()

There is also a shortcut that may come in handy in some cases - a method 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;
});

It also returns an existing instance of the class or binding (if one 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 the more detailed version that is described above.

Note: these methods are not part of the Container interface , they are only in the Container class .

Overriding Constructor Parameters

The method makeWith()allows you to pass additional parameters to the constructor. At the same time, existing instances or singletones are ignored (i.e. a new object is created). This can be useful when creating objects with different parameters and which 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 called simply make($class, $parameters). It was deleted in Laravel 5.4, but then returned back under the name makeWithin version 5.4.16. It seems that in Laravel 5.5 its name will be changed again to make().

Other methods

I described all the methods that seemed useful to me, but to complete the picture I will describe the remaining available methods.

bound ()

The method bound()verifies there is a class or an alias associated with methods bind(), singleton(), instance()or alias():

if (! $container->bound('database.user')) {
    // ...
}

You can also use the isset method and array-access syntax:

if (! isset($container['database.user'])) {
    // ...
}

The value indicated in the methods binding(), instance(), alias()can be removed by unset():

unset($container['database.user']);

var_dump($container->bound('database.user')); // false

bindIf ()

A method bindIf()does the same as a method bind(), with the exception that it only creates a binding if it does not exist (see the description of the method bound()above). Theoretically, it can be used in the package to register the default binding, allowing the user to override it.

$container->bindIf(Loader::class, FallbackLoader::class);

There is no method singletonIf(), 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);
}

resolved ()

The method resolved()returns true if the class instance 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 method is called unset()(see the description of the method bound()above).

unset($container[Database::class]);

var_dump($container->resolved(Database::class)); // false

factory ()

The method factory()returns a closure that takes no parameters and calls the method when called make().

$dbFactory = $container->factory(Database::class);

$db = $dbFactory();

wrap ()

The method wrap()wraps the closure in another closure, which will inject dependencies into the wrapper when called. The method accepts 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 method is wrap()not part of the Container interface , it is only in the Container class .

afterResolving ()

The method afterResolving()works in exactly the same way as the method resolving(), with the exception that callbacks registered with it are called after callbacks registered by the method resolving().

And finally ...

isShared()- Checks whether 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 container
getBindings()- Returns an array of all registered bindings
getAlias($abstract)- Returns the alias for the specified class / binding
forgetInstance($abstract)- Deletes the specified class instance from the container
forgetInstances()- Deletes all instances of classes
flush()- Deletes all binders and created instances of classes, completely clearing the container
setInstance()- Replaces the object returned by getInstance () (hint: use setInstance(null)to clean, a new container instance will be created later)

Note: none of these methods are part of the Container interface .

created: 2020-07-13
updated: 2021-03-13
132265



Rating 9 of 10. count vote: 2
Are you satisfied?:



Comments


To leave a comment
If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Famworks

Terms: Famworks