[ Laravel 从学徒到工匠系列 ] 服务容器篇:使用入门

声明:原书中本章叫做 IoC 容器,在 Laravel 5 中,IoC 容器改名为服务容器,所以,在后续章节,IoC 容器和服务容器指代同一个东西。

我们已经了解了依赖注入及其使用,接下来咱们一起来探索控制反转容器(IoC)。我们前面已经说过,通过 IoC 容器可以帮助我们更方便地管理类依赖,而且 Laravel 提供了一个功能强大的 IoC 容器。这个 IoC 容器在 Laravel 中被称作服务容器,是整个 Laravel 框架最核心的部分,在它的调度下,框架各个组件可以很好的组合在一起工作。实际上,Laravel 的Application 类就是一个继承自 Container 的容器类,它就是整个 Laravel 应用的服务容器。

IoC 容器:控制反转容器让依赖注入更方便,它负责在整个应用生命周期内解析和注入那些定义在容器中的类和接口。

学院君注:在 Laravel 中经常提及的服务容器就是这里的 IoC 容器,你可以把服务容器看作 IoC 容器在 Laravel 框架中的方言别名,两者等价。

在 Laravel 应用中,可以通过 App 门面来访问服务容器,还可以通过辅助函数 app() 来访问,如果是在服务提供者(可以理解为一个专门用于绑定接口与实现到服务容器的地方)中,则一般通过 $this->app 来访问容器。服务容器提供了很多方法,不过我们会从最基础的开始。下面我们继续使用上一章创建的 BillerInterfaceBillingNotifierInterface 为例,并且假设在应用中使用 Stripe 进行支付操作。我们可以将支付接口的 Stripe 实现类绑定到容器里,这项工作可以在服务提供者的 register() 方法中完成(在本系列文档中,不特别说明,我们使用的都是 AppServiceProvider),就像这样:

public function register()
{
    $this->app->bind(BillerInterface::class, function ($app) {
        return new StripeBiller($app->make(BillingNotifierInterface::class));
    });
}

注意在我们在初始化 BillingInterface 实现类时,额外需要一个BillingNotifierInterface 的实现,为此,我们需要编写一个针对该接口的实现类 EmailBillingNotifier,具体实现先留空:

namespace App\Services;
use App\Contracts\BillingNotifierInterface;

class EmailBillingNotifier implements BillingNotifierInterface
{
    public function notify(array $user, $amount)
    {
        // TODO: Implement notify() method.
    }
}

然后在服务提供者中将其绑定到所实现的接口:

$this->app->bind(BillingNotifierInterface::class, function ($app) {
   return new EmailBillingNotifier();
});

注:注意到我们在定义绑定关系的时候使用的是匿名函数,这样做的好处是用到该依赖时才会实例化,从而提升了应用的性能。

如你所见,这个服务容器就是个用来注册各种接口与实现绑定的地方。一旦一个类在容器里注册了以后,就可以很容易地在应用的任何位置解析并调用它。我们甚至还可以在一个绑定函数内解析其它的绑定关系,就像我们上面做的那样。

一旦我们使用了服务容器,切换接口的实现就是一行代码的事儿。举个例子,考虑以下代码:

class UserController extends BaseController{
    public function __construct(BillerInterface $biller)
    {
        $this->biller = $biller;
    }
}

当这个控制器被服务容器实例化的时候,引用 EmailBillingNotifierStripeBiller 会被注入到这个控制器中。现在,如果我们想要换一种通知的实现方式,比如通过短信发送通知(仿照 EmailBillingNotifier 新建一个 SmsBillingNotifier 类),只需在服务提供者中修改绑定到通知接口的实现类即可,其它任何地方都不用修改:

$this->app->bind(BillingNotifierInterface::class, function ($app) {
    return new SmsBillingNotifier();
});

这样,不管在应用的哪个地方注入/解析账单通知接口,都会得到 SmsBillingNotifier 类的实例。利用这种架构设计,我们的应用可以在各种服务的不同实现方式之间快速切换。

只改一行代码就能切换接口实现,真的是很强大。例如,如果我们想把短信服务的提供商从原来的联通替换为移动,可以开发一个新的基于移动接口实现的短信服务类,然后切换绑定语句。如果移动服务挂了,只需修改一行代码就可以快速切换回原来的短信提供商,这正是服务容器的强大之处。

有时候,你可能想在整个应用生命周期中只实例化某类一次,类似单例模式,可以通过 singleton 方法来注册接口与实现类:

$this->app->singleton(BillingNotifierInterface::class, function ($app) {
    return new SmsBillingNotifier();
});

现在,只要服务容器解析过这个账单通知对象实例一次,在剩余的请求生命周期中都会使用同一个实例。

服务容器还提供了和 singleton 方法很类似的 instance 方法,区别是 instance 方法可以绑定一个已经存在的对象实例。然后容器每次解析的时候都会返回这个对象实例。

$notifier = new SmsBillingNotifier;
$this->app->instance(BillingNotifierInterface::class, $notifier);

现在我们已经熟悉了服务容器的基本使用,接下来,让我们深入挖掘它更加强大的功能:依靠反射来动态解析类。

单独使用容器:即使你的项目不是基于 Laravel 框架的,依然可以使用Laravel 的服务容器,只要通过 Composer 安装 illuminate/container 就好了。

学院君 has written 1162 articles

Laravel学院院长,终身学习者

积分:146507 等级:P12 职业:手艺人 城市:杭州

7 条回复

  1. 学院君 学院君 says:
    @ 五花肉

    这个问题很好

    对于通知场景,Laravel 是支持通知通过多种渠道发送通知的,你可以看下 Illuminate\Notifications\ChannelManager 底层的实现,不同通知驱动是可以水平横向扩展的。

    你这个疑问看到后面扩展框架篇就明白了,Laravel 底层并不是简单将实现绑定到借口而已,而是每个核心组件都有一个管理器管理不同的驱动,理论上来说,很多组件都是支持多个实现并行的,只不过区分默认实现和非默认实现而已,但是服务容器是后面的基础,暂时没有说那么深而已,多数服务容器上绑定的其实是管理器,而非具体实现

  2. 五花肉 五花肉 says:

    有个问题不太明白: 在一个应用中,如果有的地方需要邮件通知,有的地方需要短信通知,是不是就不能用同一个通知接口的实现? (或者说注册的接口虽然可以非常方便的切换不同的实现,但是不能同时有多种实现?)

登录后才能进行评论,立即登录?