CI4中Services::database()默认单例,Services::email()默认瞬时,Services::session()为请求作用域;自定义服务需在Services.php中用addSingleton()或addTransient()显式注册。

CI4里Services::database()默认是单例,但不是所有服务都这样
CodeIgniter 4 的服务容器(Config\Services)对不同服务采用不同生命周期策略。比如 Services::database() 返回的是单例实例——同一请求内多次调用返回同一个对象;而 Services::email() 默认是瞬时(每次调用都新建),Services::session() 是作用域(per-request)级别。这不是配置错误,而是框架预设的合理默认。
手动注册服务时,addSingleton()和addTransient()才是关键
如果你自己定义的服务类需要控制生命周期,必须显式注册,不能只靠自动加载。CI4 的服务注册入口在 app/Config/Services.php 中,用以下方式切换:
-
public static function myService($getShared = true):这个$getShared参数决定是否复用实例。传true(默认)→ 单例;传false→ 每次调用都新建 - 更明确的方式是直接在
app/Config/Services.php的init()方法里调用容器方法:$services->addSingleton(MyService::class)或$services->addTransient(MyService::class) - 注意:一旦用了
addSingleton(),该类在整次请求中只会被构造一次;若依赖其他服务(如Database),它们也会按各自生命周期参与解析,不会被“降级”
控制器里用 $this->myService 时,别忘了父类构造函数已触发服务解析
在控制器构造函数中直接赋值 $this->myService = service('myService') 是常见写法,但要注意:如果该服务是单例,且你在多个控制器中都这么写,它们拿到的是同一个实例——这通常没问题;但如果服务内部维护了状态(比如缓存数组、计数器),就可能跨控制器污染。
- 典型陷阱:
MyLogger类带一个private $logs = [],注册为单例后,A控制器写日志,B控制器读到的已是合并后的数组 - 解决办法:要么改用
addTransient(),要么把状态移到请求上下文(如service('request')或session())中管理 - 另一个坑:在构造函数里调用
service('myService')前没调用parent::__construct(),会导致服务容器未初始化,抛出Call to a member function get() on null
调试时看 getShared() 返回值比猜更可靠
不确定某个服务当前是不是单例?别翻文档,直接在代码里打个断点或 var_dump():
service('database')->getShared() // true 表示单例<br>service('email')->getShared() // false 表示瞬时
这个方法返回布尔值,是 CI4 内部判定依据。它不反映你“想让它是什么”,而是反映当前注册方式——也就是说,它告诉你“现在确实是单例”,而不是“应该用单例”。很多问题其实出在注册逻辑被覆盖或重复执行,而不是理解偏差。
最常被忽略的一点:服务注册代码如果写在 Services.php 的静态方法里,又在多个地方被调用(比如测试环境和生产环境共用同一文件但条件加载),addSingleton() 可能被执行两次,导致容器内部状态混乱。建议所有自定义服务注册集中放在 init() 方法,并加注释说明生命周期意图。

















