2.3 容器与服务绑定


2.3 容器与服务绑定

生命周期一节反复出现「容器装配」四个字,本节把它拆开:容器怎么知道要造什么对象、依赖怎么自动填进去、你自己的服务怎么登记进去,以及服务提供者在其中扮演的角色。读完本节,2.1 里那句「类型提示的参数由容器装配」对你就不再是咒语,而是能亲手复现的机制。

一、为什么需要容器:new 的困境

不用容器时,对象的依赖要自己一层层 new:

// 手动装配:每一层都要认识下一层 $config = new MysqlConfig(env('DB_HOST'), env('DB_USER')); $pdo = new PdoAdapter($config); $repo = new BookRepository($pdo); $service = new OrderService($repo); // 改构造函数签名?所有调用点全要动

三个问题随着项目变大同步恶化:装配逻辑散落各处、同一对象被反复创建、想换成测试替身时无从下手。容器把「创建什么、怎么创建、创建几份」集中登记成一张注册表,用到时按需取。框架本身就是容器的头号用户——控制器、模型、中间件、验证器全从容器里出来,这也是框架能实现「方法参数写个类型提示就自动注入」的原因。

二、注入的两种姿势与原理

容器识别依赖靠反射:拿到类名后检查构造函数(或方法)参数的类型提示,缺什么造什么,递归到底。控制器里的两种常用姿势:

use app\service\BookService; use think\facade\Request; class Book extends BaseController { protected BookService $service; // 姿势一:构造器注入——整个类都依赖它时用 public function __construct(BookService $service) { $this->service = $service; } // 姿势二:方法注入——只有当前动作需要时用,按需装配 public function detail(int $id, BookService $service) { return json($service->detail($id)); } }

可选依赖、接口到实现的绑定则要在注册时说清。绑定发生在服务提供者里——它是框架预留的「装配车间」,在应用初始化阶段被统一调用(回忆 2.2 时序图的第二段):

namespace app; use think\Service; class PayService extends Service { public function boot() { // 接口绑定实现:此后容器里要 PayGateway,一律给 AlipayGateway $this->app->bind(\app\contract\PayGateway::class, \app\service\AlipayGateway::class); // 单例绑定:整个请求周期共享同一实例,省去重复构建 $this->app->instance('pay.notify.lock', new \app\service\NotifyLock()); } }
// 绑定完成后,任何容器装配的位置都直接面向接口 public function pay(\app\contract\PayGateway $gateway, int $orderId) { return $gateway->charge($orderId); }

接口绑定的价值在换实现时显形:支付渠道从支付宝切微信,改动只在服务提供者的一行绑定,业务代码毫发无伤;测试时把接口绑定成假实现,控制器测试就不再碰真网关。

三、门面与助手函数:容器上的快捷方式

日常代码里你更多见的写法是 Db::name('book')Request::param() 这类静态调用。它们不是传统意义的静态方法——门面类把调用转发给容器里的真实服务实例,所以 Db 背后是同一个数据库管理器对象。门面让代码短,但依赖关系也被藏了起来:测试替身、多实例场景都会别扭。本书的取向:业务服务用注入,框架设施(日志、缓存、Db)用门面无可厚非,助手函数(input()session())少用——它们是最隐形的全局状态。三种写法要能在团队规范里说清楚边界。

四、动手练习:把「短信发送」装进容器

背景:项目里发短信的代码散落各处,运营商要切换。操作:先定义 SmsGateway 接口约定 send(string $mobile, string $tpl, array $vars): bool;实现 AliyunSms 与备用的 LogSms(后者只写日志,开发环境用);在服务提供者里按配置环境绑定不同实现;把旧代码改成注入接口调用。结果:业务代码零改动完成运营商切换,开发环境再也不真发短信。解读:这一套正是 2.1「换入口仍成立的逻辑下沉」的延伸——下沉之后还要再进一步,把「怎么造」也交出去,容器接管的是对象的完整生命周期。变式:给绑定加一层判断,按用户归属地选择不同短信通道,观察绑定闭包里能拿到什么上下文、又拿不到什么(拿不到请求参数——绑定发生在装配时,不是调用时)。

⚠️ 常见坑:容器里的单例在请求结束即销毁,不要把它当跨请求的存储用——「全局唯一」只在当前请求内成立,跨请求数据请走缓存(第 6 章)或数据库。

💡 关键直觉:凡是 new 出现在业务代码里且依赖超过一层,就该考虑交给容器——不是教条,是让依赖可见、可替换、可测试的工程手段。

本节要点回顾

  • 容器的价值:集中装配、按需创建、可替换可测试,框架自身的对象也全部出自容器。
  • 注入姿势:整类依赖用构造器注入,单动作依赖用方法注入,接口到实现的对应关系在服务提供者里绑定。
  • 服务提供者:应用初始化阶段的装配车间,bind 定映射、instance 放现成实例。
  • 门面与助手:门面是容器的静态糖衣,可用但别滥用;助手函数依赖最隐蔽,团队规范里要划界。
  • 单例边界:容器单例活在当前请求内,跨请求状态不属于它管。

对象装配的机关拆完了,还差最后一道闸:请求进控制器之前,谁来安检?下一节看中间件管道。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U