使用入门
声明:原书中本章叫做 IoC 容器,在 Laravel 5 中,IoC 容器改名为服务容器,所以,在后续章节,IoC 容器和服务容器指代同一个东西。
我们已经了解了依赖注入及其使用,接下来咱们一起来探索控制反转容器(IoC)。我们前面已经说过,通过 IoC 容器可以帮助我们更方便地管理类依赖,而且 Laravel 提供了一个功能强大的 IoC 容器。这个 IoC 容器在 Laravel 中被称作服务容器,是整个 Laravel 框架最核心的部分,在它的调度下,框架各个组件可以很好的组合在一起工作。实际上,Laravel 的Application 类就是一个继承自 Container 的容器类,它就是整个 Laravel 应用的服务容器。
IoC 容器:控制反转容器让依赖注入更方便,它负责在整个应用生命周期内解析和注入那些定义在容器中的类和接口。
在 Laravel 应用中,可以通过 App 门面来访问服务容器,还可以通过辅助函数 app() 来访问,如果是在服务提供者(可以理解为一个专门用于绑定接口与实现到服务容器的地方)中,则一般通过 $this->app
来访问容器。服务容器提供了很多方法,不过我们会从最基础的开始。下面我们继续使用上一章创建的 BillerInterface
和BillingNotifierInterface
为例,并且假设在应用中使用 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 AppServices;
use AppContractsBillingNotifierInterface;
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;
}
}
当这个控制器被服务容器实例化的时候,引用 EmailBillingNotifie
r 的 StripeBiller
会被注入到这个控制器中。现在,如果我们想要换一种通知的实现方式,比如通过短信发送通知(仿照 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 就好了。
反射解决方案
Laravel 服务容器中最强大的功能之一就是通过反射来自动解析类的依赖。反射是一种在运行时检查类和方法的能力,比如,PHP 的 ReflectionClass
类可以动态检查给定类的所有方法,PHP 函数 method_exists
从某种意义上说也是一种反射(关于反射的更多细节可以查看 PHP 反射文档)。在开始进入正题之前,我们先来看看 PHP 中反射类的使用:
$reflection = new ReflectionClass(AppServicesStripeBiller::class);
dump($reflection->getMethods()); # 获取 StripeBiller 类中的所有方法
dump($reflection->getNamespaceName()); # 获取 StripeBiller 的命名空间
dump($reflection->getProperties()); # 获取 StripeBiller 上的所有属性
打印结果如下:
注:还可以获取类的很多其它信息,你可以自己查看相应 API,然后去玩玩。
依靠这个强大的 PHP 特性,Laravel 的服务容器可以做一些很奇妙的事情!例如,考虑下面这个类:
class UserController extends BaseController
{
public function __construct(StripBiller $biller)
{
$this->biller = $biller;
}
}
注意这个控制器的构造函数在形参里类型约束了一个 StripBiller
类,通过反射我们可以获取这个类型约束指定的类。当 Laravel 的服务容器中没有某个显式绑定类的解析器时(即没有注册接口与对应实现的绑定),将会尝试使用反射来解析。程序流程类似于下面这样:
- 已经有一个
StripBiller
的解析器了吗? - 没有?那用反射来检查一下
StripBiller
吧,看看它有没有依赖。 - 解析
StripBille
r 需要的所有依赖(递归处理)。 - 使用
ReflectionClass->newInstanceArgs()
来创建一个新的StripBiller
实例。
注:底层逻辑详见
IlluminateContainerContainer
的resolve
方法。
如你所见,容器替我们干了好多重活,这能帮你省去为每个类编写解析器的麻烦。这就是 Laravel 服务容器最强大也是最独特的特性,熟练掌握这个功能对构建大型 Laravel 应用是十分有用的。
下面我们修改一下控制器,看看改成这样会发生什么事:
class UserController extends BaseController
{
public function __construct(BillerInterface $biller)
{
$this->biller = $biller;
}
}
假设我们没有为 BillerInterface
显式绑定过任何解析器,即没有在服务提供者中定义过下面这样的绑定代码:
$this->app->bind(BillerInterface::class, function ($app) {
return new StripeBiller($app->make(BillingNotifierInterface::class));
});
容器该怎么知道要注入什么类呢?要知道,接口(或抽象类)本身是不能被实例化的。如果我们没有告知容器更多信息的话,容器是无法实例化这个依赖的。我们需要明确指出哪个类是这个接口的默认实现(正如我们之前在服务提供者 AppServiceProvider
中所做的那样),这就需要用到 bind 方法:
$this->app->bind(BillerInterface::class, StripeBiller::class);
这里,我们只传了一个字符串进去,而不是一个匿名函数。这个字符串告诉容器总是使用 StripBiller 类作为 BillerInterface 接口的默认实现类。此外,我们也获得了在容器绑定中只修改一行代码即可轻松切换服务实现的能力。例如,如果我们需要切换到余额支付作为我们的支付提供者,只需要新写一个 BalancedBiller
来实现 BillerInterface
接口,然后修改容器绑定如下:
$this->app->bind(BillerInterface::class, BalancedBiller::class);
这样,新的实现就可以在整个应用中生效了!
在绑定实现到接口时,你也可以使用 singleton 方法,这样容器在整个请求生命周期中只会实例化这个实现类一次,从而实现单例模式:
$this->app->singleton(BillerInterface::class, StripeBiller::class);
掌握容器:想了解更多关于容器的知识?去读源码吧!容器在底层只有一个类IlluminateContainerContainer,读完了你就会对容器如何工作有更深的理解。