ThinkPHP 8 容器机制完全拆解:从源码读懂依赖注入与服务解析

2026-08-08 0 881

前阵子同事发来一段代码找我排查,他说在控制器里用 app(UserService::class) 拿到的对象,和自己 new 出来的 UserService 行为完全对不上。我反问他,你有没有翻过容器源码?他说没有,平时都在调用,但确实不知道底下的逻辑。这个现象不算少见,我刚接触 ThinkPHP 的时候也是这个状态。

今天就花点时间,把 ThinkPHP 8 的容器机制从头到尾讲清楚,特别是“从调用一个类到拿到一个对象,中间到底发生了什么”。如果你能把这篇文章看完,以后再遇到跟容器相关的疑难杂症,排查思路基本就出来了。

先说容器在替我们解决什么问题

很多同学其实已经写过很多年依赖注入了,但你要让他解释一下“这个概念到底解决了什么”,他又支支吾吾。

用一个例子说清楚。你有个下单服务类 OrderService,它依赖订单仓库和支付网关:

class OrderService
{
    protected $repository;
    protected $payment;

    public function __construct(OrderRepository $repository, PaymentGateway $payment)
    {
        $this->repository = $repository;
        $this->payment = $payment;
    }

    public function createOrder(array $data)
    {
        // 业务代码
    }
}

没有容器的时候,你得手动组装这些依赖才能得到一个能用的 OrderService:

$service = new OrderService(new OrderRepository(), new PaymentGateway());

如果 OrderRepository 的构造函数又依赖别的服务,那还要继续一层层往下 new。代码多了以后,你会发现所有组装对象的逻辑散落在项目各个角落,改一个依赖,十来个地方要跟着改。

依赖注入做的事情,是把“对象的创建和组装”从调用方那里拿掉。而容器就是那个帮我们统一完成组装的工厂。你只要跟容器说“给我一个 OrderService”,它会自动根据构造函数需要的参数,一层一层地创建依赖并返回最终对象。

容器在 ThinkPHP 8 里的核心结构

ThinkPHP 8 的容器逻辑主要集中在 thinkContainer 类中,同时 App 类继承它,所以你在框架里见到的 $app 本质就是一个容器。

我观察它主要靠下面三样东西在工作:

一个缓存实例的数组 $instances,记录已经生成过的对象;一个绑定关系数组 $bind,记录服务别名或接口对应的实现;一套创建对象的方法:make()、invokeClass()、bindParams()。

用一句话概括就是:先从绑定表里找找有没有对应的配置,找不到就直接通过反射自动装配。生成出来的对象会顺手扔进缓存,下次再要就直接给同一个。

下面是我根据源码归纳出来的核心流程,不是逐行复制,不过关键步骤都在:

public function make(string $abstract, array $vars = [], bool $newInstance = false)
{
    // 1. 别名解析
    $abstract = $this->getAlias($abstract);

    // 2. 命中缓存就直接返回
    if (isset($this->instances[$abstract]) && !$newInstance) {
        return $this->instances[$abstract];
    }

    // 3. 如果绑定的是闭包,执行闭包
    if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
        $object = $this->invokeFunction($this->bind[$abstract], $vars);
    } else {
        // 否则走反射装配
        $object = $this->invokeClass($this->bind[$abstract] ?? $abstract, $vars);
    }

    // 4. 写缓存
    if (!$newInstance) {
        $this->instances[$abstract] = $object;
    }

    return $object;
}

这段代码虽然看着不长,但已经把容器的核心策略说得差不多了。其中有两个点,很多人在第一次读完源码的时候会忽略:

第一,容器默认是单例模式。第二次 make 同一个东西,不会重新执行构造函数,而是直接返回第一次生成的对象。这也就是我同事那个问题的直接原因——他修改的是自己 new 出来的对象,而框架里 get 到的始终是缓存里的那个旧对象。

第二,$bind 表里可以放闭包,也可以放类名。闭包这种写法非常适合“这个类怎么创建由我这段定义代码说了算”的场景。

invokeClass 的反射装配是怎么实现的

当容器决定走 invokeClass() 时,其实是在利用 PHP 的反射机制动态读取类的构造函数,然后挨个分析参数。

我把这一段的逻辑简化成下面这样:

protected function invokeClass(string $class, array $vars = [])
{
    // 拿到类的反射信息
    $reflect = new ReflectionClass($class);

    // 没有构造函数就直接 new
    $constructor = $reflect->getConstructor();
    if (is_null($constructor)) {
        return new $class();
    }

    // 逐个解析构造函数参数
    $args = $this->bindParams($constructor, $vars);

    return $reflect->newInstanceArgs($args);
}

关键在于 bindParams() 怎么处理参数。看这个伪代码:

protected function bindParams(ReflectionFunctionAbstract $reflect, array $vars = [])
{
    $args = [];

    foreach ($reflect->getParameters() as $param) {
        $type = $param->getType();

        // 参数是类类型,递归去容器里取
        if ($type && !$type->isBuiltin()) {
            $args[] = $this->getObjectParam($param, $vars);
            continue;
        }

        // 有指定的变量就用指定的
        if (isset($vars[$param->getName()])) {
            $args[] = $vars[$param->getName()];
            continue;
        }

        // 有默认值就用默认值
        if ($param->isDefaultValueAvailable()) {
            $args[] = $param->getDefaultValue();
        }
    }

    return $args;
}

如果参数是一个类,容器就递归调用 make() 去解析那个类;如果参数是内置类型比如 string、int,就看看能不能从 $vars 里取一个值;还没有就尝试用默认值。所有依赖的依赖,就这样一层层被容器捋顺了。

这里有一个细节:容器对类的解析顺序依赖构造函数签名,所以不要把构造函数的参数顺序写得乱七八糟,否则调试的时候会比较痛苦。

实战:让容器自动选择支付实现

原理说了不少,来一个能落地的场景。

假设你在做一套订单系统,支持支付宝和微信,OrderService 里的支付逻辑不想用 if…else 来区分端别。你当然可以写一个抽象出来的支付接口,然后通过容器绑定动态决定具体实现。

先定义接口和两个驱动:

<?php
namespace apppaycontracts;

interface Payable
{
    public function pay(int $orderId, float $amount): bool;
}
<?php
namespace apppaydriver;

use apppaycontractsPayable;

class Alipay implements Payable
{
    public function pay(int $orderId, float $amount): bool
    {
        // 这里写支付宝接口对接逻辑
        return true;
    }
}
<?php
namespace apppaydriver;

use apppaycontractsPayable;

class WechatPay implements Payable
{
    public function pay(int $orderId, float $amount): bool
    {
        // 这里写微信支付接口对接逻辑
        return true;
    }
}

在 OrderService 里不直接依赖某个具体类,而是依赖 Payable 接口:

<?php
namespace apporderservice;

use apppaycontractsPayable;

class OrderService
{
    public function __construct(protected Payable $payer)
    {
    }

    public function createAndPay(int $orderId, float $amount)
    {
        // 订单创建逻辑略
        $this->payer->pay($orderId, $amount);
    }
}

接下来在服务提供者中做一个绑定:

<?php
namespace appprovider;

use thinkService;
use apppaycontractsPayable;
use apppaydriverAlipay;
use apppaydriverWechatPay;

class PayServiceProvider extends Service
{
    public function register()
    {
        $this->app->bind(Payable::class, function () {
            if (request()->header('X-Pay-Channel') === 'wechat') {
                return new WechatPay();
            }
            return new Alipay();
        });
    }
}

这个绑定的意思是:当容器里任何地方出现 Payable 类型依赖时,执行这个闭包,根据当前请求的 header 决定给支付宝实现还是微信实现。

因为容器默认会缓存实例,同一个请求里 OrderService 第一次解析 Payable 后,后续再解析同样拿到的是同一个支付对象。如果某个特殊场景你希望每次 make 都是全新的,比如状态会被外部修改,那就传 true:

app(Payable::class, [], true);

或者干脆在绑定时用工厂方法每次返回新对象,但同时记得别让它走缓存。

这个案例本身不算复杂,但很能说明问题:当你把依赖的创建职责交出来以后,原本分散在各处的支付逻辑判断就集中到了一个地方。以后如果要新增一种支付渠道,只需要扩展 Payable 的实现,然后在绑定里加上判断,业务层代码一行都不用动。

几个真实踩过的坑

1. 容器缓存带来的状态污染

容器默认缓存所有 make 出来的对象,所以同一个类在同一个请求生命周期内只有一份。如果你在类上设置了公共属性,某一次请求把它改掉了,后面所有读取这个对象的地方都会受影响。这种问题不好捉,因为它往往需要特定的调用顺序才会出现。如果你明确知道这个对象不应该被共享,应该在 make 时强制 newInstance。

2. 构造函数里不要做重活

容器反射装配时,每个依赖都需要先保证构造函数能顺利执行。如果你在构造函数里写了一些网络请求或者数据库查询,那这个类每被解析一次都会触发这些副作用,一旦依赖关系复杂,性能账单会很难看。构造函数就做赋值,别搞别的。

3. bind 的键名不统一,解析结果对不上

有人习惯在绑定时写短别名,比如 bind(‘pay.driver’, …),但使用的时候又用 app(Payable::class) 来取。两者不是同一个键,容器会当成两个服务来处理。建议要么统一用类名,要么统一用短别名,别混着来。这个习惯养成之后能省掉很多无谓的排查时间。

4. 使用门面(Facade)不等于脱离容器

ThinkPHP 8 里的门面底层依然是容器。比如 thinkfacadeConfig 看起来是个静态类,实际上它内部通过 __callStatic 把调用转发到容器里绑定的 config 实例上。你绕不开容器的,所以理解容器依然是理解框架的前提。

结语

容器在 ThinkPHP 8 里并不是一个高高在上的抽象概念,它的核心就是 bind 表、instances 缓存和反射装配。你可能已经写了很长时间框架代码而不需要手改容器,但当你遇到缓存对象状态怪异、依赖注入拿到非预期实例、或者要给既有系统加一个动态替换实现时,理解容器内部机制能帮你少走很多弯路。

下次再遇到“为什么 app() 拿到的对象和我 new 的不一样”,你就可以直接回答他:因为容器默认缓存了实例,而且你的构造函数依赖已经被自动解析过了。

ThinkPHP 8 容器机制完全拆解:从源码读懂依赖注入与服务解析
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8 容器机制完全拆解:从源码读懂依赖注入与服务解析 https://www.taomawang.com/server/thinkphp/2505.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务