contract是php接口,用于解耦实现;必须定义接口、实现类并在服务提供者中绑定,注意bind/singleton生命周期差异,测试应mock contract而非具体类。

直接依赖 Illuminate\Contracts\Cache\Repository 而不是 Illuminate\Cache\RedisStore,是解耦的起点;但光写接口不绑定、不注入,契约就只是个空壳。
Contract 不能 new,也不能靠 Facade 混过去
常见错误现象:Target [Illuminate\Contracts\Cache\Repository] is not instantiable —— 这说明你类型提示了接口,但容器根本不知道该用哪个类来实例化它。
原因在于:Contract 就是 PHP 的 interface,它不能被 new,也不提供默认实现。Laravel 的门面(如 Cache::get())背后走的是静态代理 + 容器解析,它会自动帮你找绑定关系;但你在 Service 或 Controller 里手动写 new CacheContract(),或在构造函数里写了接口却没注册绑定,就会直接崩。
- 别在构造函数里写
new \Illuminate\Cache\RedisStore(...)—— 这等于把 Redis 绑死在代码里 - 别在类型提示用了
Repository却继续用Cache::put()—— 这会让 IDE 和测试都失去控制点 - 如果非要用门面,至少确认
Cache门面底层绑定的仍是契约(它确实是),但门面无法被 mock,单元测试时你会卡住
bind() 和 singleton() 不是选哪个更“高级”,而是看对象有没有状态
在 AppServiceProvider::register() 里绑定自定义 Contract 时,bind() 和 singleton() 的区别直接影响运行结果,不是生命周期“优化”问题,而是正确性问题。
比如你写了个 OrderValidator,它内部读取了当前请求的 $request->ip() 和 Auth::id():
- 用
$this->app->singleton(OrderValidator::class, ...)→ 同一个实例会被所有后续请求复用,IP 和用户 ID 会错乱 - 用
$this->app->bind(OrderValidator::class, ...)→ 每次解析都新建,IP 和用户上下文干净独立 - 但如果你写的是
UuidGenerator,纯函数式无副作用,singleton()完全安全,还能省点开销
判断依据很简单:这个类的属性或方法是否依赖请求周期内的动态数据?有 → 用 bind();没有 → singleton() 更合适。
自定义 Contract 时,最容易漏掉的是“实现类必须满足全部方法”
你新建了 app/Contracts/NotificationSender.php,只声明了 send() 方法,然后写了 app/Services/SmsSender.php 去实现它——但忘了加 queue() 方法,而你的业务代码某处调用了 $sender->queue(...)。
这时 PHP 不会在定义时报错,而是在运行时抛出 Call to undefined method。因为 interface 只约束“声明”,不强制“使用”。IDE 也不会提醒你漏实现了什么,除非你开了严格模式并启用了 PHPStan 或 Psalm。
- 定义 Contract 前先想清楚:哪些方法是调用方真正会用到的?别为了“看起来完整”而堆一堆空方法
- 实现类必须
implements接口,并且方法签名(参数类型、返回类型、是否 static)完全一致 - 如果未来要加新方法,不要直接改原接口——老实现类会立刻 break。应该建新接口(如
AdvancedNotificationSender),或用默认方法(PHP 8.0+)加向后兼容
契约不是加个 interface 文件再 bind 一下就完事了;它真正的复杂点,在于你得时刻意识到:每个类型提示的接口背后,都有一个生命周期、一个上下文边界、一个可能被 mock 的测试入口。漏掉其中任何一个,解耦就变成了假解耦。











