单例在Laravel中应在服务提供者register()方法内用$this->app->singleton()注册,避免跨请求状态污染;FPM下每次请求独立,Swoole下常驻内存需手动清理;务必通过依赖注入获取,禁用app()调用和静态属性缓存。

单例在 Laravel 里不是“用不用”的问题,而是“什么时候不该用”——它确实能省资源、保状态,但一旦滥用,就会让请求间的状态互相污染,尤其在 FPM 或 Swoole 长生命周期环境下,singleton 实例可能跨请求存活,导致数据错乱。
注册单例必须在服务提供者里调用 $this->app->singleton()
不能在控制器、模型或任意类的构造函数里直接 new,也不能在路由闭包里注册。Laravel 的服务容器只在启动阶段(register 方法执行时)完成绑定,之后再调用 singleton() 不会生效。
- 必须在
AppServiceProvider或自定义服务提供者的register()方法中注册 - 传入类名(
SomeService::class)比字符串更安全,避免命名空间拼写错误 - 闭包工厂函数里不要做耗时操作(如数据库查询、HTTP 请求),否则首次解析会阻塞整个请求
- 如果类有依赖,推荐用容器自动解析,而不是手动 new:
function ($app) { return new SomeService($app->make(Config::class)); }
控制器里用依赖注入拿单例最稳妥
这是 Laravel 推荐方式,也是唯一能保证类型安全和自动解析的路径。别试图在控制器方法里用 app(SomeService::class),虽然能拿到实例,但绕过了依赖注入系统,会导致单元测试难 mock、IDE 无法跳转、PHPStan 报告类型缺失。
- 构造函数参数声明类型即可:
public function __construct(SomeService $service) - Laravel 自动从容器取已注册的
SomeService单例,无需额外配置 - 若控制器本身被缓存(如通过
php artisan route:cache),依赖注入仍是唯一可靠方式;app()调用在缓存路由下可能因类未加载而失败
非控制器类(比如普通工具类)怎么获取单例?
不能靠构造函数自动注入,因为这些类不是由容器创建的。你有两个选择:显式依赖注入(推荐),或运行时从容器取(谨慎)。
- 把单例当参数传进去,让调用方负责提供:
class DataProcessor { public function __construct(private SomeService $service) {} }—— 这样仍可测试、可替换 - 如果实在要运行时取,用
app(SomeService::class),但必须确保该类在服务提供者注册之后才被使用,否则抛出BindingResolutionException - 绝对不要在类属性里用
static $instance = app(...),PHP 静态属性在请求间不重置,Swoole 下会永久持有旧实例 - 注意:在队列任务、命令行 Artisan 命令中,单例生命周期与 Web 请求不同,每次命令执行都是新容器实例,除非你手动复用
singleton() 和 bind() 别混用,状态共享是隐式行为
很多人以为 “注册了单例,我就控制了状态”,其实真正危险的是没意识到这个“单例”在哪儿被改了。比如一个 CacheManager 单例里存了数组,A 请求往里塞了 ['user_id' => 123],B 请求进来时读到的就是这个,哪怕 B 完全没调用写方法。
-
singleton()绑定后,所有app(X::class)、依赖注入、resolve(X::class)都返回同一对象引用 - 没有“线程安全”保障:PHP-FPM 是进程隔离,但每个 worker 进程内多个请求共用一个单例实例;Swoole 更明显,协程间直接共享内存
- 如果类内部有可变状态(如
$this->cache = []),务必确认业务逻辑是否真的需要跨请求共享——大多数时候你真正需要的是缓存驱动(Redis)、数据库或 request-scoped service - 调试时可用
var_dump(spl_object_id($instance))对比两个地方拿到的实例 ID 是否一致,快速验证是否真为单例
单例真正的复杂点不在注册或获取,而在状态边界。一个 HttpClient 单例可以复用连接池,没问题;但一个 UserContext 单例如果存了当前登录用户 ID,就注定会崩——因为它的语义是 request-scoped,却用了 application-scoped 的生命周期。


















