contract是illuminate\contracts下的接口集合,只定义方法签名与行为契约,不提供实现;不是抽象基类,不可直接实例化,须经服务容器绑定后通过类型提示自动注入。

Contract 是什么,不是什么
Contract 就是 Illuminate\Contracts 下那一堆接口,比如 Cache\Store、Queue\Queue、Mail\Mailer。它们不提供实现,只约定方法签名和行为契约——换句话说,你写代码时依赖的是“它能做什么”,而不是“它用 Redis 还是 File 实现”。
别把它当成抽象基类或模板;也别指望直接 new 一个 Mail\Mailer 接口。它只在服务容器里被绑定后才生效。
怎么在自己的类里正确使用 Contract
核心就一条:类型提示接口,让 Laravel 自动注入对应实现。不是手动 new,也不是用 app() 硬取。
- ✅ 正确:在构造函数或方法参数里写
Illuminate\Contracts\Cache\Store - ❌ 错误:写
Illuminate\Cache\Repository(这是具体实现类,破坏解耦) - ❌ 错误:在控制器里写
$cache = app('cache.store')(绕过类型提示,难测试、难替换)
示例:
public function __construct(Store $cache)
{
$this->cache = $cache;
}
这样写,换掉缓存驱动(Redis → Memcached),你的类完全不用改。
自己定义 Contract 的三个硬约束
自己写接口不是为了“看起来高级”,而是为了解决真实替换/测试/分层问题。没这三样之一,别急着抽 Contract。
- 必须有至少两个不同实现(比如本地文件日志 + Sentry 上报日志)
- 必须被服务容器绑定,且绑定方式统一(通常在
AppServiceProvider::register()里用$this->app->bind()) - 接口方法不能带 Laravel 特定辅助函数调用(比如不能在接口方法里直接用
config()或request()),否则实现类会被污染
常见翻车点:MyServiceInterface::handle() 返回 Response —— 这会让命令行调用它的场景崩溃,因为 Response 是 HTTP 层概念。
为什么 bind 和 singleton 绑定方式不能乱选
绑定方式决定实例生命周期,错用会导致状态残留或性能浪费。
- 用
bind():每次解析都新建实例(适合有状态、短生命周期的类,比如表单验证器) - 用
singleton():全局唯一实例(适合无状态工具类,比如支付网关客户端) - ⚠️ 别在
singleton()里 hold request 相关数据(比如把Request存进属性),下次请求会读到上一次的脏数据
绑定示例:
$this->app->singleton(PaymentGateway::class, function ($app) {
return new AlipayGateway(config('payment.alipay'));
});
注意:这里传入的是具体类名,不是接口名——接口名只用于类型提示,绑定目标必须是可实例化的类。
Contract 的价值不在“写了多少接口”,而在“删掉某个实现时,有多少地方要改”。越少,说明你抽得越准;越多,说明你可能把不该抽象的也抽象了。











