
本文解释了在依赖注入(di)场景中,因对象创建时机不当导致类属性获取不到容器中已注册服务的问题,并提供正确的初始化顺序与实践建议。
在使用自定义 DI 容器时,一个常见但容易被忽视的陷阱是:对象的构造函数中过早访问尚未初始化的服务。你遇到的 $this->router 为 null,而 $this->di->get('router') 却能正常返回值,其根本原因并非 DI 实现有误,而是 Cms 实例的创建发生在服务注册之前。
回顾你的 Bootstrap 文件原始逻辑:
$di = new DI();
$cms = new Cms($di); // ❌ 此时 $di 中尚未注册 'router'
$services = require 'config/services.php';
foreach ($services as $service) {
$provider = new $service($di);
$provider->init(); // ✅ 此处才真正执行 $di->set('router', $router)
}问题就在这里:Cms 构造函数中调用了 $this->di->get('router'),但此时 Provider::init() 还未执行,$di->container['router'] 根本不存在,has() 方法中的空合并操作 ?? null 直接返回 null —— 因此 $this->router 被赋值为 null 并永久固化在实例属性中,后续即使 DI 容器补上了该服务,也不会自动更新已存在的属性。
✅ 正确做法是:确保所有服务注册完成之后,再创建依赖这些服务的对象。修正后的启动流程如下:
try {
$di = new DI();
// ✅ 先完成全部服务注册
$services = require 'config/services.php';
foreach ($services as $service) {
(new $service($di))->init();
}
// ✅ 再创建 Cms 实例(此时 'router' 已存在于 $di 中)
$cms = new Cms($di);
$cms->run();
} catch (\ErrorException $e) {
echo $e->getMessage();
}此外,还建议对 DI::get() 方法做健壮性增强,避免静默返回 null 导致难以调试:
public function get($key)
{
if (!array_key_exists($key, $this->container)) {
throw new \InvalidArgumentException("Service '{$key}' is not registered in DI container.");
}
return $this->container[$key];
}⚠️ 注意:当前 DI::get() 实际调用了 has(),而 has() 返回的是值或 null,这掩盖了“键不存在”的语义。应区分「键存在但值为 null」和「键不存在」两种情况——生产环境推荐显式校验并抛出异常,而非默认兜底。
最后,若希望 Cms 更具可测试性与松耦合性,还可考虑将 $router 改为延迟加载(Lazy Load)或通过方法注入替代构造注入:
public function getRouter(): Router
{
if ($this->router === null) {
$this->router = $this->di->get('router');
}
return $this->router;
}这样既保持了构造轻量,又规避了初始化顺序风险。总结一句话:DI 的威力不在于“能存能取”,而在于“何时存、何时取”的严格时序控制。










