依赖注入是将对象创建权交由外部容器管理,而非类内自行new;ioc容器本质是对象创建配置表,核心在于bind/make/resolve三步,需合理设计接口边界与生命周期。

依赖注入不是“传参数”,而是把对象的创建权交给外部
很多人第一次看到 __construct(UserRepository $repo) 就以为“哦,就是个参数类型声明”。其实关键不在类型,而在谁 new 了这个 $repo。如果类内部自己写 new MySQLUserRepository(),它就和具体实现绑死了;而依赖注入意味着:这个 $repo 是别人造好、再塞进来的——你只管用,不操心怎么来。
常见错误现象:
– 单元测试时无法替换模拟对象(mock),因为类自己 new 了真实数据库连接
– 换数据库驱动要改七八个文件,因为每个用到的地方都写了 new RedisCache()
– 一个类里混着初始化逻辑、业务逻辑、日志逻辑,越改越脆
实操建议:
– 构造函数参数只接收接口(如 UserRepositoryInterface),不接收具体类
– 禁止在业务类中出现 new 第三方服务实例(new Logger()、new HttpClient() 都算)
– 如果某个类确实需要动态决定用哪个实现(比如按环境切 Redis/ArrayCache),那就把“选择逻辑”提到容器配置里,而不是类内部 if-else
控制反转(IoC)容器不是魔法盒,它本质是一张“如何造对象”的配置表
所谓“反转”,是把“谁控制对象生命周期”这件事,从类自己手里拿走,交给容器。容器本身可以极简:一个 array 存 key → callable,调用时执行对应闭包即可。Laravel 的 Container 或 PHP-DI 的 Container 都是这思路的放大版。
使用场景:
– 多处需要同一个单例(如数据库连接),容器确保只初始化一次
– 类 A 依赖 B,B 又依赖 C,容器自动递归解析并组装
– 运行时根据条件绑定不同实现($container->bind(LoggerInterface::class, FileLogger::class))
性能影响:
– 初次解析依赖链有开销(反射读构造函数、判断类型),但现代容器基本会缓存已解析结果
– 不要用容器做高频短生命周期对象的工厂(比如每次 HTTP 请求都 make(Request::class) 一百次),这类对象应直接 new 或复用
容易踩的坑:
– 把容器当成全局服务定位器(Service Locator),在任意方法里写 $this->container->get(...) —— 这等于没解耦,只是把 new 换成 get
– 绑定闭包时捕获了外部变量(function () use ($config) { return new Db($config); }),导致配置无法热更新或测试难隔离
– 循环依赖不报错却卡死(A 依赖 B,B 依赖 A),多数容器会在解析时抛出 CircularReferenceException
PHP 手写简易 IoC 容器只需 20 行,重点在 bind / make / resolve 三步
不需要一开始就上 Symfony DI 或 PHP-DI。下面这段代码能跑通绝大多数小项目:
class Container {
private $bindings = [];
public function bind($abstract, $concrete = null) {
$this->bindings[$abstract] = $concrete ?: function ($c) use ($abstract) {
return new $abstract();
};
}
public function make($abstract) {
$concrete = $this->bindings[$abstract] ?? function () use ($abstract) {
return new $abstract();
};
if (is_callable($concrete)) {
return $concrete($this);
}
return $this->resolve($concrete);
}
private function resolve($concrete) {
$reflector = new ReflectionClass($concrete);
$constructor = $reflector->getConstructor();
if (is_null($constructor)) return $reflector->newInstance();
$params = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if ($type && !$type->isBuiltin()) {
$params[] = $this->make($type->getName());
} else {
throw new Exception("Cannot resolve non-object dependency: {$param->name}");
}
}
return $reflector->newInstanceArgs($params);
}
}
说明:
– bind() 注册“怎么造”,支持传类名、闭包、或接口→实现映射
– make() 是入口,触发整个解析流程
– resolve() 用反射读构造函数参数,递归 make() 其依赖项
– 不处理标量参数(如 string $host)、不支持属性注入、不缓存实例 —— 这些都是可选增强点,不是核心
真正难的不是写容器,而是设计好接口边界和生命周期范围
写完容器,90% 的问题其实在这之前:接口是否正交?哪些该单例?哪些该每次 new?哪些该 request-scoped?
例如:
– 数据库连接必须单例(否则事务、连接池失效)
– HTTP 请求上下文对象(Request、Response)必须每次请求新建,不能放进容器默认单例池
– 日志器(LoggerInterface)通常单例,但如果要按模块分通道,就得设计成工厂模式 + 命名绑定($container->make('user.logger'))
最容易被忽略的一点:
容器配置本身也是代码,要和业务逻辑一样做版本管理、做测试覆盖。别把所有 bind() 堆在 bootstrap.php 里硬编码,拆成按领域组织的配置文件(auth_bindings.php、cache_bindings.php),否则半年后没人敢动。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











