In depth explanation of Laravel's IoC service container

Posted by maineyak on Fri, 15 Nov 2019 09:20:34 +0100

The article was forwarded from the professional Laravel developer community. Original link: https://learnku.com/laravel/t...

as everyone knows, Laravel The function of inversion of control (IoC) / dependency injection (DI) is very powerful. Unfortunately, Official documents I didn't explain all of its functions in detail, so I decided to practice it on my own and sort it out. The following code is based on Laravel 5.4.26 The other versions may be different.

Understanding dependency injection

I won't go into detail here on the principle of dependency injection / inversion of control - if you don't know that well, it's recommended to read Fabien Potencier( Symfony Founder of the framework) What is Dependency Injection? .

Access container

There are many ways to access the Container instance through Laravel. The simplest way is to call the auxiliary function app():

$container = app();

In order to highlight the Container class, other methods are not covered here.

Note: Official documents Use $this - > app instead of $container in.

(* in Laravel applications, Application It's actually a subclass of Container
(this also explains the origin of auxiliary function app()), but I will focus on it in this article.) Container Class. )

Using IlluminateContainer outside of Laravel

If you want to use the Container without Laravel, install Then:

use Illuminate\Container\Container;

$container = Container::getInstance();

Basic usage

The simplest use is to inject a dependent class through a constructor.

class MyClass
{
    private $dependency;

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

Use the make() method of the Container to instantiate the MyClass class:

$instance = $container->make(MyClass::class);

container will automatically instantiate the dependency class, so the functions implemented in the above code are equivalent to:

$instance = new MyClass(new AnotherClass());

(suppose that another class has classes to depend on - in this case, the Container recursively instantiates all the dependencies.)

actual combat

Here are some based on PHP-DI documentation Example of - decouples the sending email from the code registered by the user:

class Mailer
{
    public function mail($recipient, $content)
    {
        // Send mail
        // ...
    }
}
class UserManager
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register($email, $password)
    {
        // Create user account
        // ...

        // Send greeting email to users
        $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!');

Binding interface and Implementation

With the Container class, we can easily implement the process from interface to concrete class to instance. First define the interface:

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

Declare the concrete class that implements the interface. The concrete class can also depend on other interfaces (or the concrete class in the previous example):

class MyClass implements MyInterface
{
    private $dependency;

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

Then use the bind() method to bind the interface to a specific class:

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

Finally, in the make() method, use the interface as a parameter:

$instance = $container->make(MyInterface::class);

Note: if the interface is not bound to a specific class, an error will be reported:

Fatal error: Uncaught ReflectionException: Class MyInterface does not exist

This is because the container will try to instantiate the new myinterface, which itself is syntactically wrong.

actual combat

Replaceable cache layer:

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()
    {
        // Application cache
        $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 class and concrete class

You can also bind to abstract classes:

$container->bind(MyAbstract::class, MyConcreteClass::class);

Or bind the concrete class to its subclass:

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

Custom binding

When using bind() method for binding operation, if a class needs additional configuration, it can also be implemented through closure function:

$container->bind(Database::class, function (Container $container) {
    return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});

Every time you create an instance of MySQL Database with configuration information (I'll talk about how to create an instance that can be shared through Singletons later), you need to use the Database interface. We see that the closure function receives the Container instance as a parameter, and can be used to instantiate other classes if necessary:

$container->bind(Logger::class, function (Container $container) {
    $filesystem = $container->make(Filesystem::class);

    return new FileLogger($filesystem, 'logs/error.log');
});

You can also customize how to instantiate a class through the closure function:

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

Parse callback function

You can use the resolving() method to register a callback function. When the binding is resolved, the callback function is called:

$container->resolving(GitHub\Client::class, function ($client, Container $container) {
    $client->setEnterpriseUrl(GITHUB_HOST);
});

All registered callback functions will be called. This approach also applies to 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);

You can also register a callback function that will be called when any class is parsed - but I think this might only apply to login and debugging:

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

Extension class

You can also use the extend() method to bind one class to an instance of another class:

$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});

The other class returned here should implement the same interface, otherwise an error will be reported.

Singleton binding

As long as the bind() method is used for binding, a new instance will be created each time it is used (the closure function will be called once). To share an instance, you can use the singleton() method instead of the bind() method:

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

Or closures:

$container->singleton(Database::class, function (Container $container) {
    return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
});

To create a single instance for a specific class, only this class is passed as the only parameter:

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

In each case, the singleton object is created once and used repeatedly. If the instance you want to reuse has already been generated, you can use the instance() method. For example, Laravel uses this method to ensure that there is only one Container instance:

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

Name of custom binding

In fact, you can use any string as the binding name, not necessarily the class name or interface name - but the disadvantage is that you can't use the class name instantiation, but only use the make() method:

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

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

To support both classes and interfaces and simplify the writing of class names, 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);

Storage value

You can also use container to store any value - for example: configuration data:

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

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

Supports storage as an array:

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

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

When binding through closures, this storage method shows its advantages:

$container->singleton('database', function (Container $container) {
    return new MySQLDatabase(
        $container['database.host'],
        $container['database.name'],
        $container['database.user'],
        $container['database.pass']
    );
});

(instead of using container to store configuration files, the Laravel framework uses a separate Config Class - but PHP-DI Used)

Tips: when instantiating an object, you can also replace the make() method with an array:

$db = $container['database'];

Dependency injection by method / function

So far, we have seen many examples of dependency injection through constructors. In fact, Laravel also supports dependency injection for any method:

function do_something(Cache $cache) { /* ... */ }

$result = $container->call('do_something');

In addition to dependency classes, you can also pass other parameters:

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

Available for any callable method:

closure

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

$container->call($closure);

Static method

class SomeClass
{
    public static function staticMethod(Cache $cache) { /* ... */ }
}
$container->call(['SomeClass', 'staticMethod']);
// Or:
$container->call('SomeClass::staticMethod');

Common method

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

Shortcut to call instance method

With this syntax structure ClassName@methodName, you can instantiate a class and call its methods:

$container->call('PostController@index');
$container->call('PostController@show', ['id' => 4]);

Containers are used to instantiate classes, which means:

  1. Dependencies are injected into constructors (and methods).
  2. If you want to reuse this class, you can define it as a singleton class.
  3. You can use an interface or any name, not a specific class.

For example, this 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 use the default method as the third parameter. If the first parameter is a class name that does not specify a method, the default method is called Laravel uses event processing To achieve:

$container->call(MyEventHandler::class, $parameters, 'handle');

// Equivalent to:
$container->call('MyEventHandler@handle', $parameters);

Method call binding

You can override method calls with the bindMethod() method, such as passing other parameters:

$container->bindMethod('PostController@index', function ($controller, $container) {
    $posts = get_posts(...);

    return $controller->index($posts);
});

All of this works by calling the closure instead of the original method:

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

However, any additional parameters to call() are not passed into the closure, so they cannot be used.

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

Note: this method does not belong to Container interface , just specific Container class Reference. Submitted PR Understand why parameters are ignored.

Context binding

Sometimes, you want to use different implementations of interfaces in different places. Here's from Laravel documentation An example of:

$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 rely on file system interfaces, but each will receive a different implementation. You can also use closures for give(), just like bind():

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
        return Storage::disk('s3');
    });

Or name the dependency:

$container->instance('s3', $s3Filesystem);

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give('s3');

Binding parameters to basic types

You can also bind basic types (strings, integers, etc.) by passing variable names to needs() (not interfaces) and values to give():

$container
    ->when(MySQLDatabase::class)
    ->needs('$username')
    ->give(DB_USER);

You can use closures to delay retrieving values until you need it:

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

Here you cannot pass a class or a named dependency (such as give('database.user ') because it will be returned as a literal value - for this you must use a closure:

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

sign

You can use the container tag to bind related tags:

$container->tag(MyPlugin::class, 'plugin');
$container->tag(AnotherPlugin::class, 'plugin');

Then retrieve all tagged instances as arrays:

foreach ($container->tagged('plugin') as $plugin) {
    $plugin->init();
}

The parameters of tag() all accept arrays:

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

Re bind

Note: This is a more advanced one, but rarely needed - please feel free to skip it!

When the binding or instance needs to be changed after it has been used, you can call the rebinding() callback - for example, this Session class is replaced after it is used by the Auth class, so you need to notify the Auth class of 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

(for more information on rebinding, see Here Sum Here.)

refresh()

There is also a shortcut, refresh(), to handle this common pattern:

$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 or binding, if any, so you can do this:

// 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 detailed version above!)

Note: these methods do not belong to Container interface , only specific Container class.

Override constructor parameters

The makeWith() method allows you to pass other parameters to the constructor It ignores any existing instances or singletons, and is still useful when creating multiple instances of classes with different parameters, while still injecting 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 and below, it's very simple to make($class, $parameters). It's in Laravel 5.4 removed , but later Re add as makeWith() In 5.4.16. In Laravel 5.5, it seems that Revert to Laravel 5.3 syntax.

Other methods

This covers all the methods I think are useful - but just to solve the problem, here is a summary of the remaining public methods

bound()

If the class or name is bound to bind(), singleton(), instance(), or alias(), bound() returns true.

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

You can also use array access syntax and isset():

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

It can be reset with unset(), which removes the specified binding / instance / alias.

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

bindIf()

bindIf() does the same thing as bind(), except that it registers only one binding (if not already) (see bond () above) It may be used to register the default binding in the package while allowing the user to override it.

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

There is no singletonIf() method, but you can use bindIf($abstract, $concrete, true) instead:

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

Or write it all as follows:

if (! $container->bound(Loader::class)) {
    $container->singleton(Loader::class, FallbackLoader::class);
}

resolved()

Returns true if the class resolved() has been resolved.

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. If you use unset(), it will be reset (see the above bound()).

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

factory()

The factory() method returns a closure without parameters and calls make().

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

$db = $dbFactory();

I'm not sure what it will do

wrap()

The wrap() method wraps a closure to inject its dependencies at execution time The wrap method takes a set of parameters and the returned closure has no parameters:

$cacheGetter = function (Cache $cache, $key) {
    return $cache->get($key);
};

$usernameGetter = $container->wrap($cacheGetter, ['username']);

$username = $usernameGetter();

I'm not sure it's useful because closures have no parameters

Note: this method does not belong to Container interface , only belongs to Container class.

afterResolving()

The afterResolving() method is exactly the same as that of resolving(), calling the "parse" callback after parsing. I'm not sure when it will work

Last...

  • isShared() - determines whether the given type is a shared instance / instance
  • isAlias() - determines if the given string is a registered alias
  • hasMethodBinding() - determines whether the container has the given method binding
  • getBindings() - retrieve the original array of all registered bindings
  • getAlias($abstract) - resolves the alias of the base class / binding name
  • Forgeinstance ($Abstract) - clears a single instance object
  • forgetInstances() - clear all instance objects
  • flush() - clears all bindings and instances, effectively resets the container
  • setInstance() - replace the instance used by getInstance() (Tip: use setInstance(null) to clear it, so next time it will generate a new instance)

Note: no method in the last section is part of it Container interface.

Topics: PHP Database Laravel Session github