Home  >  Article  >  Backend Development  >  A deep dive into Laravel’s IoC container

A deep dive into Laravel’s IoC container

WBOY
WBOYOriginal
2023-09-01 18:49:101316browse

深入探索 Laravel 的 IoC 容器

Inversion of Control (IoC) is a technique that allows the inversion of control compared to classic procedural code. Of course, the most prominent form of IoC is dependency injection (DI). Laravel's IoC container is one of the most commonly used Laravel features, but probably the least understood.

This is a very simple example of using dependency injection to implement inversion of control.

<?php

class JeepWrangler
{
    public function __construct(Petrol $fuel)
    {
        $this->fuel = $fuel;
    }
	
    public function refuel($litres)
    {
	    return $litres * $this->fuel->getPrice();
    }
}

class Petrol
{
    public function getPrice()
    {
    	return 130.7;
    }
}

$petrol = new Petrol;
$car = new JeepWrangler($petrol);

$cost = $car->refuel(60);

By using constructor injection, we now delegate the creation of the Petrol instance to the caller itself, thus achieving inversion of control. Our JeepWrangler doesn't need to know where the Petrol comes from, just get it.

So what does all this have to do with Laravel? Quite a lot actually. If you didn't know, Laravel is actually an IoC container. As you might expect, a container is an object that contains things. Laravel's IoC container is used to contain many different bindings. Everything you do in Laravel will interact with the IoC container at some point. This interaction usually takes the form of a binding being resolved.

If you open any existing Laravel service provider, you will most likely see something similar in the register method (the example is much simplified).

$this->app['router'] = $this->app->share(function($app) {
    return new Router;
});

This is a very, very basic binding. It consists of the name of the binding (router) and the parser (the closure). When the binding is resolved from the container, we will return an instance of Router.

Laravel often groups similar binding names, such as session and session.store.

To resolve bindings, we can call the method directly, or use the make method on the container.

$router = $this->app->make('router');

This is what a container does in its most basic form. But, like most things in Laravel, there's more to it than just binding and resolving classes.

Shared and non-shared binding

If you browse through several of the Laravel service providers, you'll notice that most bindings are defined similarly to the previous examples. here we go again:

$this->app['router'] = $this->app->share(function($app) {
    return new Router;
});

This binding uses the share method on the container. Laravel uses static variables to store previously resolved values ​​and simply reuse the value when the binding is resolved again. This is basically what the share method does.

$this->app['router'] = function($app) {
    static $router;
     
    if (is_null($router)) {
        $router = new Router;
    }
     
    return $router;
     
};

Another way to write it is to use the bindShared method.

$this->app->bindShared('router', function($app) {
    return new Router;
});

You can also use the singleton and instance methods to implement shared binding. So, if they both achieve the same goal, what's the difference? Not a lot actually. I personally prefer to use the bindShared method.

Conditional Binding

Sometimes you may want to bind something to a container, but only if it hasn't been bound before. There are several ways to solve this problem, but the easiest is to use the bindIf method.

$this->app->bindIf('router', function($app) {
    return new ImprovedRouter;
});

Will be bound to the container only if the router binding does not already exist. The only thing to note here is how the conditional bindings are shared. To do this, you need to provide a third parameter to the bindIf method with the value true.

Automatic dependency resolution

One of the most commonly used features of the IoC container is its ability to automatically resolve dependencies of unbound classes. What does this mean? First, we don't actually need to bind something to the container to resolve the instance. We can simply make an instance of almost any class.

class Petrol
{
    public function getPrice()
    {
        return 130.7;
    }
}

// In our service provider...
$petrol = $this->app->make('Petrol');

The container will instantiate the Petrol class for us. The best part is that it will also resolve constructor dependencies for us.

class JeepWrangler
{
    public function __construct(Petrol $fuel)
    {
        $this->fuel = $fuel;
    }
	
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
    
}

// In our service provider...
$car = $this->app->make('JeepWrangler');

The first thing the container does is check the dependencies of the JeepWrangler class. It will then try to resolve these dependencies. So, because our JeepWrangler type hints the Petrol class, the container will automatically resolve and inject it as a dependency.

Containers cannot automatically inject non-type-hinted dependencies. So if one of your dependencies is an array, then you need to instantiate it manually or specify default values ​​for the parameters.

Binding implementation

Having Laravel resolve dependencies automatically is great and simplifies the process of manually instantiating classes. However, sometimes you want to inject a specific implementation, especially when using interfaces. This is easily achieved by using the fully qualified name of the class as the binding. To demonstrate this, we will use a new interface called Fuel.

interface Fuel
{
    public function getPrice();
}

现在我们的 JeepWrangler 类可以对接口进行类型提示,并且我们将确保我们的 Petrol 类实现该接口。

class JeepWrangler
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
	
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
}

class Petrol implements Fuel
{
    public function getPrice()
    {
        return 130.7;
    }
}

现在,我们可以将 Fuel 接口绑定到容器,并让它解析 Petrol 的新实例。

$this->app->bind('Fuel', 'Petrol');

// Or, we could instantiate it ourselves.
$this->app->bind('Fuel', function ($app) {
    return new Petrol;
});

现在,当我们创建 JeepWrangler 的新实例时,容器会看到它请求 Fuel ,并且它会知道自动注入 Petrol

这也使得更换实现变得非常容易,因为我们可以简单地更改容器中的绑定。为了进行演示,我们可能会开始使用优质汽油为汽车加油,这种汽油价格稍贵一些。

class PremiumPetrol implements Fuel
{
    public function getPrice()
    {
        return 144.3;
    }
}

// In our service provider...
$this->app->bind('Fuel', 'PremiumPetrol');

上下文绑定

请注意,上下文绑定仅在 Laravel 5 中可用。

上下文绑定允许您将实现(就像我们上面所做的那样)绑定到特定的类。

abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
}

然后,我们将创建一个新的 NissanPatrol 类来扩展抽象类,并且我们将更新 JeepWrangler 来扩展它。

class JeepWrangler extends Car
{
    //
}

class NissanPatrol extends Car
{
    //
}

最后,我们将创建一个新的 Diesel 类,该类实现 Fuel 接口。

class Diesel implements Fuel
{
    public function getPrice()
    {
        return 135.3;
    }
}

现在,我们的吉普牧马人将使用汽油加油,我们的日产途乐将使用柴油加油。如果我们尝试使用与之前相同的方法,将实现绑定到接口,那么这两辆车都会获得相同类型的燃料,这不是我们想要的。

因此,为了确保每辆车都使用正确的燃料加油,我们可以通知容器在每种情况下使用哪种实现。

$this->app->when('JeepWrangler')->needs('Fuel')->give('Petrol');
$this->app->when('NissanPatrol')->needs('Fuel')->give('Diesel');

标记

请注意,标记仅在 Laravel 5 中可用。

能够解析容器中的绑定非常重要。通常,只有知道某些内容如何绑定到容器时,我们才能解决该问题。在 Laravel 5 中,我们现在可以为绑定添加标签,以便开发人员可以轻松解析具有相同标签的所有绑定。

如果您正在开发一个允许其他开发人员构建插件的应用程序,并且您希望能够轻松解析所有这些插件,那么标签将非常有用。

$this->app->tag('awesome.plugin', 'plugin');

// Or an array of tags.

$tags = ['plugin', 'theme'];

$this->app->tag('awesome.plugin', $tags);

现在,要解析给定标记的所有绑定,我们可以使用 tagged 方法。

$plugins = $this->app->tagged('plugin');

foreach ($plugins as $plugin) {
    $plugin->doSomethingFunky();
}

篮板和重新绑定

当您将某些内容多次绑定到同名容器时,称为重新绑定。 Laravel 会注意到你再次绑定了一些东西并会触发反弹。

这里最大的好处是当您开发一个包时,允许其他开发人员通过重新绑定容器中的组件来扩展它。要使用它,我们需要在 Car 摘要上实现 setter 注入。

abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }

    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
    
    public function setFuel(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
    
}

假设我们将 JeepWrangler 像这样绑定到容器。

$this->app->bindShared('fuel', function ($app) {
    return new Petrol;
});

$this->app->bindShared('car', function ($app) {
	return new JeepWrangler($app['fuel']);
});

这完全没问题,但假设另一位开发人员出现并希望扩展此功能并在汽车中使用优质汽油。因此,他们使用 setFuel 方法将新燃料注入汽车。

$this->app['car']->setFuel(new PremiumPetrol);

在大多数情况下,这可能就是所需要的;但是,如果我们的包变得更加复杂并且 fuel 绑定被注入到其他几个类中怎么办?这将导致其他开发人员必须多次设置他们的新实例。因此,为了解决这个问题,我们可以利用重新绑定:

$this->app->bindShared('car', function ($app) {
    return new JeepWrangler($app->rebinding('fuel', function ($app, $fuel) {
		$app['car']->setFuel($fuel);
	}));
});

重新绑定 方法将立即返回给我们已经绑定的实例,以便我们能够在 JeepWrangler 的构造函数中使用它。提供给 rebinding 方法的闭包接收两个参数,第一个是 IoC 容器,第二个是新绑定。然后,我们可以自己使用 setFuel 方法将新绑定注入到我们的 JeepWrangler 实例中。

剩下的就是其他开发人员只需在容器中重新绑定 fuel 即可。他们的服务提供商可能如下所示:

$this->app->bindShared('fuel', function () {
    return new PremiumPetrol;
});

一旦绑定在容器中反弹,Laravel 将自动触发关联的闭包。在我们的示例中,新的 PremiumPetrol 实例将在我们的 JeepWrangler 实例上设置。

扩展

如果您想将依赖项注入核心绑定之一或由包创建的绑定,那么容器上的 extend 方法是最简单的方法之一。

此方法将解析来自容器的绑定,并以容器和解析的实例作为参数执行闭包。这使您可以轻松解析和注入您自己的绑定,或者简单地实例化一个新类并注入它。

$this->app->extend('car', function ($app, $car) {
    $car->setFuel(new PremiumPetrol);
});

与重新绑定不同,这只会设置对单个绑定的依赖关系。

Laravel 之外的使用

与构成 Laravel 框架的许多 Illuminate 组件一样,Container 可以在 Laravel 之外的独立应用程序中使用。为此,您必须首先将其作为 composer.json 文件中的依赖项。

{
    "require": {
        "illuminate/container": "4.2.*"
   }
}

这将安装容器的最新 4.2 版本。现在,剩下要做的就是实例化一个新容器。

require 'vendor/autoload.php';

$app = new Illuminate\Container\Container;

$app->bindShared('car', function () {
    return new JeepWrangler;
});

在所有组件中,当您需要灵活且功能齐全的 IoC 容器时,这是最容易使用的组件之一。

The above is the detailed content of A deep dive into Laravel’s IoC container. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn