bind()每次make()都新建实例,因其本质是“工厂注册”,仅存储闭包或类名,每次调用均重新执行构造逻辑且不缓存;singleton()则首次创建并缓存,后续复用同一实例,是唯一可靠的单例绑定方式。

为什么 bind() 注册的服务每次 make() 都是新实例
因为 bind() 本质是“工厂注册”:它只存一个闭包或类名,每次调用 app()->make() 都会重新执行构造逻辑,不缓存结果。这不是 bug,是设计使然。
常见错误现象:
- 数据库连接类用
bind()→ 每次请求新建 PDO 实例,连接池迅速打满 - 日志器用
bind()→ 同一请求内多次make(Logger::class)得到不同实例,上下文(如 request_id)无法贯穿 - Redis 客户端用
bind()→ 连接未复用,TIME_WAIT 暴增,超时频发
关键点:只要没显式声明 $shared = true(Laravel 6 的 bind() 不支持该参数),就永远不是单例。
singleton() 是唯一可靠的单例绑定方式
它在首次 make() 时创建并缓存实例,后续全部返回同一对象。这是 Laravel 6 中实现“跨方法/跨类共享状态”的唯一安全路径。
必须遵守的实操规则:
- 只能在服务提供者的
register()方法里调用,比如AppServiceProvider::register() - 不能在
boot()、控制器、模型或路由闭包中调用——此时容器已开始解析依赖,绑定无效 - 接口绑定更健壮:
$this->app->singleton(CacheContract::class, RedisCache::class),比字符串'cache'更易被 IDE 和 PHPStan 识别 - 闭包工厂中禁止耗时操作:不要在闭包里查数据库或发 HTTP 请求,否则首请求必然卡顿
验证是否生效?跑这行代码:app()->make('cache') === app()->make('cache') 必须返回 true。
别踩这些 singleton() 的坑
Laravel 6 的容器机制对生命周期很敏感,几个高频翻车点:
- FPM 下看似“安全”,但单元测试中若修改了 singleton 实例的属性(比如 mock 后设了
$cache->data = []),不手动清理会污染后续测试 —— 记得加app()->forgetInstance(CacheContract::class) - Swoole 或 Octane 环境下,singleton 实例常驻内存,跨请求存活;若内部持有用户相关数据(如 token、session_id),会导致严重状态错乱
- 用
app(SomeService::class)手动取实例,绕过了依赖注入系统:IDE 无法跳转、PHPStan 报类型缺失、测试时难 mock - 绝对不要在类里写
static $instance = app(SomeService::class)—— FPM 下可能没事,Swoole 下这个静态属性会锁死旧实例,再也刷不出来
最稳妥的获取方式,永远是构造函数类型提示:public function __construct(private SomeService $service) {}。
什么时候该用 instance() 而不是 singleton()
instance() 是硬塞一个现成对象进容器,完全跳过构造和依赖解析。它不是单例的替代品,而是特殊场景的逃生通道。
适用场景非常有限:
- 单元测试中注入 Mock 对象:
$this->app->instance(LoggerInterface::class, $mockLogger) - 外部 SDK 客户端已由别处初始化完成(如 Twilio、Stripe),且你确保所有依赖(
config()、env 变量)早已加载完毕 - 需要精确控制实例创建时机,比如延迟到某配置加载完之后才初始化
致命风险:传进去的对象若依赖未就绪(比如 new SmsService() 但 env('TWILIO_SID') 还没读取),后续任何 make() 都会在第一次调用方法时崩溃,报 Undefined index: twilio_sid。
记住:instance() 不校验依赖,不触发事件,不参与自动解析 —— 你塞什么,它就返什么,出事不背锅。











