动态调用类的依赖注入实现问题——自制MVC框架相关
嘿,我完全懂你自己搭MVC框架时卡在依赖注入上的那种困惑——亲手造框架真的是吃透底层原理的绝佳方式,先给你点个赞!
既然你的路由解析(拆分$_SERVER['REQUEST_URI']取类名、方法、参数)已经跑通了,那咱们就聚焦在如何给控制器自动注入依赖这件事上,用最直白的方式一步步实现。
核心思路:用容器+反射实现构造函数注入
依赖注入的核心其实就是:不要让类自己去new依赖的对象,而是把依赖从外部传进去。在你的MVC场景里,就是实例化控制器时,自动把它需要的模型、服务等对象塞进去,不用手动写new UserModel()这种代码。
第一步:写一个极简的依赖容器
这个容器的作用是管理依赖关系,并且能自动解析类的构造函数参数,帮你实例化对象。
class Container { // 存储依赖绑定关系 private $bindings = []; // 绑定依赖:可以绑定类名,也可以绑定自定义的实例化逻辑 public function bind($abstract, $concrete = null) { if ($concrete === null) { $concrete = $abstract; } $this->bindings[$abstract] = $concrete; } // 解析类实例:核心是用反射处理构造函数的依赖 public function resolve($abstract) { // 如果没绑定过,默认绑定自身 if (!isset($this->bindings[$abstract])) { $this->bind($abstract); } $concrete = $this->bindings[$abstract]; // 如果是闭包,直接执行闭包获取实例 if (is_callable($concrete)) { return $concrete($this); } // 用反射获取类的构造函数信息 $reflector = new ReflectionClass($concrete); if (!$reflector->isInstantiable()) { throw new Exception("无法实例化类 {$abstract}"); } $constructor = $reflector->getConstructor(); // 如果没有构造函数,直接new实例 if ($constructor === null) { return new $concrete(); } // 解析构造函数的所有参数 $parameters = $constructor->getParameters(); $dependencies = []; foreach ($parameters as $parameter) { $type = $parameter->getType(); // 如果是基础类型(比如string、int)或者没有类型提示,处理默认值 if (!$type || $type->isBuiltin()) { if ($parameter->isDefaultValueAvailable()) { $dependencies[] = $parameter->getDefaultValue(); } else { throw new Exception("无法解析参数 {$parameter->getName()} 的依赖"); } } else { // 递归解析类型提示的依赖类 $dependencies[] = $this->resolve($type->getName()); } } // 用解析好的依赖实例化类 return $reflector->newInstanceArgs($dependencies); } }
第二步:修改你的路由逻辑,用容器实例化控制器
原来你可能是直接new $controllerName(),现在换成用容器来解析,自动注入依赖:
// 1. 解析URI(你原来的逻辑,保持不变) $uriParts = explode('/', trim($_SERVER['REQUEST_URI'], '/')); $controllerName = ucfirst($uriParts[0]) . 'Controller'; $methodName = $uriParts[1] ?? 'index'; $params = array_slice($uriParts, 2); // 2. 初始化容器 $container = new Container(); // 3. 用容器解析控制器(自动注入依赖) $controller = $container->resolve($controllerName); // 4. 调用控制器方法(和原来一样) call_user_func_array([$controller, $methodName], $params);
第三步:给控制器添加依赖,测试效果
比如你的HomeController需要依赖UserModel,直接在构造函数里声明类型提示就行:
// 依赖的模型类 class UserModel { public function getUser($id) { // 模拟数据库查询 return ['id' => $id, 'name' => '张三']; } } // 控制器类,构造函数注入UserModel class HomeController { private $userModel; public function __construct(UserModel $userModel) { $this->userModel = $userModel; } public function index($userId) { $user = $this->userModel->getUser($userId); print_r($user); } }
现在访问/home/index/1,容器会自动实例化UserModel,注入到HomeController里,最终输出Array ( [id] => 1 [name] => 张三 ),完全不用手动new依赖!
进阶玩法:绑定接口到具体实现
如果想遵循依赖倒置原则,还可以把接口绑定到具体实现。比如:
// 定义接口 interface UserRepository { public function getUser($id); } // 接口的MySQL实现 class MySQLUserRepository implements UserRepository { public function getUser($id) { return ['id' => $id, 'name' => '李四(MySQL)']; } } // 控制器依赖接口 class HomeController { private $userRepo; public function __construct(UserRepository $userRepo) { $this->userRepo = $userRepo; } public function index($userId) { $user = $this->userRepo->getUser($userId); print_r($user); } }
然后在容器里绑定接口和实现:
$container->bind(UserRepository::class, MySQLUserRepository::class);
这样容器会自动把MySQLUserRepository的实例注入到控制器里,后续如果换成Redis实现,只需要改绑定逻辑就行,控制器完全不用动。
内容的提问来源于stack exchange,提问作者ConleeC




