应使用singleton()绑定的服务包括:数据库连接管理器(保障事务与连接池)、日志写入器(避免句柄冲突)、redis缓存驱动(复用底层连接)、配置管理器(避免重复解析);而request、response等带请求上下文的类严禁单例,否则导致数据污染。

哪些服务该用 singleton() 绑定
ThinkPHP 容器的 singleton() 本质是“首次创建、全局复用”,适合有状态、开销大、需一致性保障的服务。它不是“所有工具类都该单例”,而是看是否满足三个硬条件:是否需跨请求/调用共享状态、初始化成本是否高、是否允许多实例导致资源冲突。
- 数据库连接管理器(如
think\db\Connection):连接池、事务上下文、查询日志统计都依赖单例状态;多次 new 会触发重复握手,还可能破坏事务隔离 - 日志写入器(如
think\log\driver\File或think\log\driver\Socket):文件句柄、缓冲区、异步队列等资源必须唯一持有,否则日志错乱或丢失 - 缓存驱动实例(如
think\cache\driver\Redis):底层 Redis 连接对象本身应复用,避免频繁建连断连;注意:缓存「操作」是无状态的,但「连接」是有状态的 - 配置管理器(如
think\Config):加载后内容不变,且被大量服务依赖,重复解析 config/*.php 是纯浪费
singleton() 和 bind() 混用时的典型错误
常见误用是把本该多例的类也 singleton(),比如 Request、Response、DTO 类——它们天然携带请求上下文,强制单例会导致后续请求覆盖前一个的状态,出现“A 用户看到 B 用户的数据”这类严重 bug。
另一个坑是:在 singleton() 闭包里引用了非单例依赖,例如:
$container->singleton('sms_service', function ($app) {
return new SmsService($app->make('request')); // ❌ request 是每次请求新建的,但这里只取第一次
});
这会导致短信服务永远绑定到第一个请求的 Request 实例,后续请求无法感知新参数。
TP8 中 singleton() 的实际生效前提
它不是写完就自动起效,必须确保容器已接管对象创建流程。控制器构造函数注入、中间件 handle 方法参数注入、命令行 handle() 方法里的类型提示,这些场景才走容器 make() 流程;而直接 new SmsService() 或静态方法调用,完全绕过容器,singleton() 配置毫无意义。
另外,TP8 的 singleton() 默认不支持运行时替换(除非手动 $container->bind() 覆盖),所以别指望在测试中 swap 掉它——要 mock,得用 instance() 提前注入模拟对象。
和工厂方法(如 Services::database())的关系
别混淆:工厂方法只是封装 new 逻辑,它本身不决定复用性;singleton() 才是让这个“造出来的对象”变成全局唯一的关键开关。Services 类内部若没调用容器 get(),而只是每次都 new,那就算你给它套十层 singleton() 也没用——对象根本没进容器实例池。
真正起作用的链路是:Services::db() → 内部调用 $container->get('database') → 容器查 $instances 缓存 → 命中则返回,未命中则执行绑定的闭包并存入缓存。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











