Contract 是 PHP 接口,用于解耦实现;需定义接口、实现类并在服务提供者中绑定,注意 bind/singleton 生命周期差异,测试应 mock Contract 而非具体类,避免注释膨胀和类型不兼容。

Contract 是接口,不是抽象类或配置文件
很多人看到 Contract 就以为是 Laravel 特有语法糖,其实它就是 PHP 的 interface。Laravel 把核心服务(比如日志、缓存、队列)的契约抽成接口,放在 Illuminate\Contracts\* 命名空间下,目的只有一个:解耦实现。你写业务时依赖的是 Illuminate\Contracts\Cache\Repository,而不是 Illuminate\Cache\FileStore 或 RedisStore。
常见错误现象:
• 直接 new 一个 Contract(new CacheContract())——报错,因为 interface 不能实例化
• 在控制器里 type-hint 了 Contract 却没绑定实现——抛出 Target [Illuminate\Contracts\Cache\Repository] is not instantiable
- 定义自己的 Contract:新建
app/Contracts/OrderProcessor.php,用interface关键字,方法只声明不实现 - 必须提供对应实现类(比如
app/Services/StripeOrderProcessor.php),且该类要实现 Contract 中所有方法 - 在
AppServiceProvider::register()里用$this->app->bind()绑定,例如:$this->app->bind(\App\Contracts\OrderProcessor::class, \App\Services\StripeOrderProcessor::class);
bind 和 singleton 绑定方式影响对象生命周期
Contract 绑定不是“注册一下就行”,选错绑定方式会导致状态错乱或性能问题。默认 bind() 每次解析都新建实例;singleton() 全局复用同一个实例。对无状态工具类(如格式化器)用 singleton 没问题;但对带请求上下文的类(比如依赖 Request 或用户 session 的处理器),用 singleton 可能导致数据串扰。
- 用
bind():适合每次请求都需要干净实例的场景,比如订单校验器、临时 DTO 构造器 - 用
singleton():适合纯函数式、无副作用的服务,比如UuidGenerator、RateLimiter(注意:Laravel 自带的限流器已处理好上下文) - 别在
singleton()实现里偷偷 hold 住$request或Auth::user()—— 这些对象在后续请求中不会自动更新
测试时 mock Contract 比 mock 具体类更安全
写 PHPUnit 测试时,如果你直接 mock FileStore,换到 Redis 后测试可能意外通过(因为逻辑没覆盖真实路径),但 mock CacheContract 就强制你只关心接口行为。Laravel 的测试容器支持无缝替换绑定,所以测试里 bind 一个假实现比 patch 方法更可靠。
- 测试中临时替换:
$this->app->instance(\App\Contracts\OrderProcessor::class, new class implements \App\Contracts\OrderProcessor { public function process($order) { return true; } }); - 避免在测试里用
Mockery::mock()去 mock 实现类——它绕过了容器,容易漏掉构造函数依赖或中间件逻辑 - 如果 Contract 方法返回值类型是具体类(比如
public function get(): Collection),mock 时得确保返回兼容对象,否则类型检查失败
不要把 Contract 当作代码规范文档来用
Contract 是运行时契约,不是设计文档。有人把所有业务规则、字段注释、SQL 提示全塞进 interface 的 PHPDoc 里,结果接口膨胀、难以维护,还误导新人以为“写了 doc 就算约束了”。真正的约束靠类型系统 + 测试 + Code Review,不是靠注释。
- Interface 方法签名必须精简:只暴露调用方真正需要的方法,别为了“看起来完整”加一堆空方法
- 参数和返回值尽量用基础类型或领域内窄接口(比如
UserContract而非stdClass) - 别在 Contract 里定义常量或静态方法——interface 不支持,那是 trait 或 abstract class 的事
最常被忽略的一点:Contract 修改后,所有实现类和测试必须同步更新。IDE 不会像检查 class 那样高亮 interface 变更的影响范围,很容易漏改,上线后爆 Method not found。










