[ Laravel 从学徒到工匠系列 ] 服务容器篇:反射解决方案

Laravel 服务容器中最强大的功能之一就是通过反射来自动解析类的依赖。反射是一种在运行时检查类和方法的能力,比如,PHP 的 ReflectionClass 类可以动态检查给定类的所有方法,PHP 函数 method_exists 从某种意义上说也是一种反射(关于反射的更多细节可以查看 PHP 反射文档)。在开始进入正题之前,我们先来看看 PHP 中反射类的使用:

$reflection = new ReflectionClass(\App\Services\StripeBiller::class);
dump($reflection->getMethods());  # 获取 StripeBiller 类中的所有方法
dump($reflection->getNamespaceName());  # 获取 StripeBiller 的命名空间
dump($reflection->getProperties());  # 获取 StripeBiller 上的所有属性

打印结果如下:

PHP反射

注:还可以获取类的很多其它信息,你可以自己查看相应 API,然后去玩玩。

依靠这个强大的 PHP 特性,Laravel 的服务容器可以做一些很奇妙的事情!例如,考虑下面这个类:

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

注意这个控制器的构造函数在形参里类型约束了一个 StripBiller 类,通过反射我们可以获取这个类型约束指定的类。当 Laravel 的服务容器中没有某个显式绑定类的解析器时(即没有注册接口与对应实现的绑定),将会尝试使用反射来解析。程序流程类似于下面这样:

  1. 已经有一个 StripBiller 的解析器了吗?
  2. 没有?那用反射来检查一下 StripBiller 吧,看看它有没有依赖。
  3. 解析 StripBiller 需要的所有依赖(递归处理)。
  4. 使用 ReflectionClass->newInstanceArgs() 来创建一个新的 StripBiller 实例。

注:底层逻辑详见 Illuminate\Container\Containerresolve 方法。

如你所见,容器替我们干了好多重活,这能帮你省去为每个类编写解析器的麻烦。这就是 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);

注:这种方式会在绑定时就会实例化 StripeBiller,性能不及匿名函数。

这里,我们只传了一个字符串进去,而不是一个匿名函数。这个字符串告诉容器总是使用 StripBiller 类作为 BillerInterface 接口的默认实现类。此外,我们也获得了在容器绑定中只修改一行代码即可轻松切换服务实现的能力。例如,如果我们需要切换到余额支付作为我们的支付提供者,只需要新写一个 BalancedBiller 来实现 BillerInterface 接口,然后修改容器绑定如下:

$this->app->bind(BillerInterface::class, BalancedBiller::class);

这样,新的实现就可以在整个应用中生效了!

在绑定实现到接口时,你也可以使用 singleton 方法,这样容器在整个请求生命周期中只会实例化这个实现类一次,从而实现单例模式:

$this->app->singleton(BillerInterface::class, StripeBiller::class);

掌握容器:想了解更多关于容器的知识?去读源码吧!容器在底层只有一个类Illuminate\Container\Container,读完了你就会对容器如何工作有更深的理解。

学院君 has written 1037 articles

Laravel学院院长,终身学习者

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

2 条回复

  1. 嗨丶PHP 嗨丶PHP says:

    StripeBiller实现了BillerInterface 接口,而里面这儿绑定的是 $this->app->bind(BillingNotifierInterface::class, StripeBiller::class); 没看懂

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