
本文解释了在 php 依赖注入(di)场景中,因对象创建时机不当导致类属性获取不到容器中已注册服务的常见错误,并提供正确的初始化顺序和实践建议。
在你提供的代码中,Cms 类的 $router 属性始终为 null,而直接调用 $this->di->get('router') 却能正常返回实例——这看似矛盾,实则揭示了一个关键问题:对象构造时依赖尚未就绪。
根本原因:构造时机错误
观察 Cms 的构造函数:
public function __construct($di)
{
$this->di = $di;
$this->router = $this->di->get('router'); // ⚠️ 此时 'router' 尚未注册!
}该赋值发生在 Cms 实例化瞬间,但此时 DI 容器中 'router' 服务还未被任何 Provider 注册($provider->init() 尚未执行)。因此 $this->di->get('router') 返回 null(由 has() 方法中的空合并操作 ?? null 决定),且该值被一次性固化到 $this->router 属性中,后续即使容器中补上了 router 实例,属性也不会自动更新。
而你在 run() 中能成功调用 $this->di->get('router')->add(),正是因为此时 Provider::init() 已执行,DI 容器中 router 键已被正确赋值。
正确做法:延迟解析或后置初始化
✅ 推荐方案:调整对象创建顺序(最简单、最符合 DI 原则)
如答案所示,应确保 DI 容器完成所有服务注册后,再创建依赖这些服务的业务对象(如 Cms):
try {
$di = new DI();
// ✅ 先初始化所有 Provider,向 DI 注入服务
$services = require 'config/services.php';
foreach ($services as $service) {
(new $service($di))->init();
}
// ✅ 此时 DI 已完备,再创建 Cms
$cms = new Cms($di); // $this->router = $di->get('router') → 现在返回真实 Router 实例
$cms->run();
} catch (\ErrorException $e) {
echo $e->getMessage();
}? 替代方案:使用懒加载(Lazy Resolution)
若需保持 Cms 构造时即持有服务引用,可将属性设为私有,并通过 getter 延迟获取:
class Cms
{
private $di;
private $router; // 不在构造中赋值
public function __construct($di)
{
$this->di = $di;
}
private function getRouter()
{
if ($this->router === null) {
$this->router = $this->di->get('router');
if ($this->router === null) {
throw new RuntimeException("Service 'router' is not registered in DI container.");
}
}
return $this->router;
}
public function run()
{
$this->getRouter()->add('home', '/', 'HomeController:index');
// ...
}
}此方式解耦了构造与依赖就绪时间,但增加了运行时检查开销,适用于复杂依赖图或动态服务场景。
注意事项与最佳实践
❌ 避免在构造函数中执行“重”逻辑(如服务解析、I/O、网络请求),它应仅做轻量赋值;
✅ 依赖注入容器应在应用启动早期完成全部注册,再统一构建高层对象;
-
? 检查 DI::get() 行为:当前实现 return $this->has($key) 实际返回的是 has() 的结果(即 container[$key] ?? null),语义上更宜命名为 get() 并抛出异常或返回默认值,而非静默 null;可增强健壮性:
public function get($key) { if (!isset($this->container[$key])) { throw new InvalidArgumentException("Service '$key' is not registered."); } return $this->container[$key]; } ? 若项目规模扩大,建议采用成熟 DI 容器(如 Symfony/DependencyInjection 或 PHP-DI),它们内置作用域管理、自动注入、循环依赖检测等能力。
遵循「先注册,后使用」这一核心原则,即可彻底规避此类 null 属性陷阱,让依赖注入真正发挥其解耦与可测试性的优势。










