目录¶
理解依赖注入容器¶
依赖注入(Dependency Injection, DI) 是一种设计模式,用于实现控制反转(Inversion of Control, IoC)。它的核心思想是将对象的依赖项(即它们所需的其他对象)从外部注入,而不是在对象内部创建。这有助于实现松散耦合,提高代码的可测试性和可维护性。
依赖注入容器(Dependency Injection Container, DIC) 是一个用于管理和自动解析对象依赖关系的工具。它负责实例化类、解析其依赖项,并将其注入到需要的地方。
实现简单的 DIC¶
下面我们将实现一个基础的依赖注入容器,支持以下功能:
服务注册:将类或接口与其具体实现或工厂方法关联。
服务解析:根据需要自动解析并实例化服务及其依赖。
单例支持:支持将某些服务定义为单例,即每次获取时返回同一实例。
创建 Container 类¶
首先,创建一个 Container
类,用于管理服务的注册和解析。
<?php
// src/Container.php
namespace MyFramework;
use ReflectionClass;
use ReflectionException;
class Container
{
/**
* 存储已注册的服务
*
* @var array
*/
protected $bindings = [];
/**
* 存储单例服务的实例
*
* @var array
*/
protected $instances = [];
/**
* 注册服务
*
* @param string $abstract 服务的接口或名称
* @param mixed $concrete 服务的实现,可以是类名、闭包或实例
* @param bool $singleton 是否为单例
*/
public function bind(string $abstract, $concrete, bool $singleton = false)
{
$this->bindings[$abstract] = [
'concrete' => $concrete,
'singleton' => $singleton,
];
}
/**
* 注册单例服务
*
* @param string $abstract 服务的接口或名称
* @param mixed $concrete 服务的实现,可以是类名、闭包或实例
*/
public function singleton(string $abstract, $concrete)
{
$this->bind($abstract, $concrete, true);
}
/**
* 解析服务
*
* @param string $abstract 服务的接口或名称
* @return mixed
* @throws ReflectionException
*/
public function make(string $abstract)
{
// 如果是单例且已实例化,直接返回实例
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 如果未绑定,假设 $abstract 是具体类名
if (!isset($this->bindings[$abstract])) {
return $this->build($abstract);
}
$concrete = $this->bindings[$abstract]['concrete'];
$singleton = $this->bindings[$abstract]['singleton'];
// 如果是闭包,调用闭包返回实例
if ($concrete instanceof Closure) {
$object = $concrete($this);
} else {
// 否则,构建实例
$object = $this->build($concrete);
}
// 如果是单例,存储实例
if ($singleton) {
$this->instances[$abstract] = $object;
}
return $object;
}
/**
* 构建实例
*
* @param string $concrete 具体类名
* @return mixed
* @throws ReflectionException
*/
protected function build(string $concrete)
{
$reflector = new ReflectionClass($concrete);
if (!$reflector->isInstantiable()) {
throw new Exception("Class {$concrete} is not instantiable.");
}
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $concrete;
}
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$dependency = $parameter->getClass();
if ($dependency === null) {
// 如果参数没有类型提示,使用默认值或抛出异常
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new Exception("Cannot resolve the unknown dependency {$parameter->name}.");
}
} else {
// 递归解析依赖
$dependencies[] = $this->make($dependency->name);
}
}
return $reflector->newInstanceArgs($dependencies);
}
}
?>
代码解释:
bind
方法:注册服务,可以指定是否为单例。singleton
方法:快捷注册单例服务。make
方法:解析并返回服务实例。如果是单例且已实例化,则直接返回实例;否则,根据绑定的具体实现或类名构建实例。build
方法:使用 PHP 的反射机制,自动解析构造函数的依赖,并递归实例化依赖项。
注册服务¶
使用 Container
类注册服务。可以在容器配置文件中定义所有服务的注册。
<?php
// config/container.php
use MyFrameworkContainer;
use MyFrameworkControllersHomeController;
use MyFrameworkControllersUsersController;
use MyFrameworkControllersAuthController;
use MyFrameworkMiddlewareAuthenticationMiddleware;
use MonologLogger;
use MonologHandlerStreamHandler;
use TwigEnvironment;
use TwigLoaderFilesystemLoader;
// 实例化容器
$container = new Container();
// 注册 Logger
$container->singleton(Logger::class, function ($container) {
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__ . '/../logs/app.log', Logger::DEBUG));
return $logger;
});
// 注册 Twig
$container->singleton(Environment::class, function ($container) {
$loader = new FilesystemLoader(__DIR__ . '/../views');
return new Environment($loader, [
'cache' => __DIR__ . '/../cache/twig',
'debug' => true,
]);
});
// 注册 Router
$container->singleton(MyFrameworkRouter::class, function ($container) {
return new MyFrameworkRouter($container);
});
// 注册 Controllers
$container->bind(HomeController::class, HomeController::class);
$container->bind(UsersController::class, UsersController::class);
$container->bind(AuthController::class, AuthController::class);
// 注册 Middlewares
$container->bind(AuthenticationMiddleware::class, AuthenticationMiddleware::class);
// 其他服务注册...
return $container;
?>
说明:
单例服务:
Logger
和TwigEnvironment
被注册为单例,确保整个应用中使用的是同一实例。闭包注册:通过闭包定义服务的创建逻辑,允许在注册时进行复杂的初始化。
自动装配:对于简单的类,可以直接绑定类名,让容器通过反射自动解析其依赖。
解析服务¶
在需要使用服务的地方,通过容器的 make
方法获取实例。例如,在 Router 中获取控制器实例。
集成 DIC 到框架¶
将 Container
集成到现有的框架中,需要修改 Router 和 Controller 类,使它们能够通过容器获取依赖项。
更新 Router 类¶
修改 Router
类,使其接受 Container
实例,并通过容器解析控制器和中间件。
<?php
// src/Router.php
namespace MyFramework;
use MyFrameworkMiddlewareMiddleware;
use ReflectionException;
class Router
{
private $routes = [];
private $namedRoutes = [];
private $container;
/**
* 构造函数
*
* @param Container $container 依赖注入容器
*/
public function __construct(Container $container)
{
$this->container = $container;
$this->loadCache(); // 如果实现了路由缓存
}
/**
* 添加路由规则
*
* @param string $method HTTP 方法
* @param string $uri 请求的 URI
* @param string $action 控制器和方法,例如 'UsersController@show'
* @param array $middlewares 中间件列表
* @param string|null $name 路由名称
*/
public function add(string $method, string $uri, string $action, array $middlewares = [], string $name = null)
{
// 转换 URI 模式为正则表达式,并提取参数名称
$pattern = preg_replace_callback('/{([a-zA-Z0-9_]+)(?)?}/', function ($matches) {
$param = $matches[1];
$optional = isset($matches[2]) && $matches[2] === '?';
if ($optional) {
return '(?P<' . $param . '>[a-zA-Z0-9_-]+)?';
} else {
return '(?P<' . $param . '>[a-zA-Z0-9_-]+)';
}
}, $uri);
// 支持可选参数后的斜杠
$pattern = preg_replace('#//+#', '/', $pattern);
$pattern = '#^' . $pattern . '(/)?$#';
// 提取参数名称
$params = $this->extractParams($uri);
$this->routes[] = [
'method' => strtoupper($method),
'pattern' => $pattern,
'action' => $action,
'params' => $params,
'middlewares' => $middlewares,
'name' => $name
];
if ($name) {
$this->namedRoutes[$name] = end($this->routes);
}
}
/**
* 分发请求到相应的控制器方法
*
* @param string $requestMethod HTTP 方法
* @param string $requestUri 请求的 URI
*/
public function dispatch(string $requestMethod, string $requestUri)
{
foreach ($this->routes as $route) {
if ($route['method'] === strtoupper($requestMethod)) {
if (preg_match($route['pattern'], $requestUri, $matches)) {
// 提取命名参数
$params = [];
foreach ($route['params'] as $param) {
if (isset($matches[$param]) && $matches[$param] !== '') {
$params[$param] = $matches[$param];
}
}
// 执行中间件
foreach ($route['middlewares'] as $middlewareClass) {
/** @var Middleware $middleware */
$middleware = $this->container->make($middlewareClass);
if (!$middleware->handle($params)) {
// 中间件中止请求
return;
}
}
// 解析控制器和方法
list($controllerName, $method) = explode('@', $route['action']);
$fullControllerName = "MyFramework\Controllers\$controllerName";
// 通过容器获取控制器实例
if ($this->container->make($fullControllerName)) {
$controller = $this->container->make($fullControllerName);
if (method_exists($controller, $method)) {
// 调用方法并传递参数
call_user_func_array([$controller, $method], $params);
return;
}
}
// 如果控制器或方法不存在,返回 404
$this->sendNotFound();
}
}
}
// 如果没有匹配的路由,返回 404
$this->sendNotFound();
}
/**
* 发送 404 响应
*/
private function sendNotFound()
{
header("HTTP/1.0 404 Not Found");
echo "404 Not Found";
}
/**
* 提取路由中的参数名称
*
* @param string $uri 路由 URI
* @return array 参数名称列表
*/
private function extractParams(string $uri): array
{
preg_match_all('/{([a-zA-Z0-9_]+)(?)?}/', $uri, $matches);
return $matches[1];
}
// ... 路由缓存相关方法 ...
}
?>
代码解释:
依赖注入容器:Router 类现在接受一个
Container
实例,通过容器解析控制器和中间件。中间件执行:通过容器实例化中间件,并调用其
handle
方法。控制器实例化:通过容器实例化控制器,确保其依赖项被正确注入。
更新 Controller 类¶
修改基础 Controller
类,使其接受依赖项(如 Twig 和 Logger)通过构造函数注入。
<?php
// src/Controller.php
namespace MyFramework;
use TwigEnvironment;
use MonologLogger;
class Controller
{
protected $twig;
protected $logger;
protected $router;
/**
* 构造函数
*
* @param Environment $twig Twig 环境实例
* @param Logger $logger Monolog Logger 实例
* @param Router $router 路由器实例
*/
public function __construct(Environment $twig, Logger $logger, Router $router)
{
$this->twig = $twig;
$this->logger = $logger;
$this->router = $router;
}
/**
* 渲染模板
*
* @param string $template 模板文件路径
* @param array $data 传递给模板的数据
*/
protected function render(string $template, array $data = [])
{
echo $this->twig->render($template, $data);
}
/**
* 发送 404 响应
*/
protected function sendNotFound()
{
header("HTTP/1.0 404 Not Found");
echo "404 Not Found";
}
}
?>
说明:
构造函数注入:通过构造函数接收
TwigEnvironment
、MonologLogger
和Router
实例,确保控制器可以使用这些依赖项。渲染方法:使用注入的 Twig 实例渲染模板。
使用 DIC¶
依赖注入容器已经集成到 Router 和 Controller 类中,现在可以在控制器中使用容器管理的服务。
在控制器中获取依赖¶
例如,在 HomeController
中使用 Router 生成 URL:
<?php
// src/Controllers/HomeController.php
namespace MyFrameworkControllers;
use MyFrameworkController;
class HomeController extends Controller
{
public function index()
{
$this->logger->info("访问主页");
$usersUrl = $this->router->generateUrl('users.list'); // 假设 generateUrl 方法已实现
$this->render('home/index.html.twig', ['usersUrl' => $usersUrl]);
}
// ... 其他方法 ...
}
?>
说明:
使用依赖:控制器可以直接访问注入的
router
、logger
和twig
实例,无需手动实例化。生成 URL:通过
router
的generateUrl
方法生成命名路由的 URL。
在视图中使用服务¶
在 Twig 模板中,可以使用传递的数据生成链接:
{# views/home/index.html.twig #}
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<h1>欢迎来到主页!</h1>
<p><a href="{{ usersUrl }}">查看用户列表</a></p>
</body>
</html>
说明:
模板变量:使用
{{ usersUrl }}
输出从控制器传递过来的 URL。
总结¶
通过实现一个简单的依赖注入容器,可以实现以下优势:
松散耦合:组件之间不直接依赖具体实现,而是通过接口或容器进行协作。
可测试性:更容易为组件编写单元测试,因为依赖项可以被轻松替换或模拟。
可维护性:集中管理依赖关系,使得代码更易于维护和扩展。
灵活性:更容易更换或升级依赖项,无需大规模修改代码。
关键步骤回顾:
实现 Container 类:管理服务的注册和解析。
注册服务:在容器配置文件中定义所有需要的服务及其依赖关系。
集成到框架:修改 Router 和 Controller 类,使其通过容器获取依赖项。
使用 DIC:在控制器中使用注入的服务,简化代码逻辑。
进一步扩展建议¶
为了进一步提升依赖注入容器的功能和灵活性,可以考虑以下扩展:
自动装配增强:
支持接口绑定:允许将接口绑定到具体的实现类,增强代码的抽象性。
支持参数绑定:为服务提供构造函数参数的显式绑定,解决复杂依赖关系。
生命周期管理:
除了单例,还可以支持工厂模式或延迟实例化(Lazy Loading)。
装饰器模式:
允许在不修改服务定义的情况下,添加额外的功能或行为。
配置驱动:
通过配置文件自动加载和注册服务,提升可维护性。
容器优化:
实现缓存机制,提升容器解析服务的性能。
支持编译容器配置,提高运行时性能。
错误处理和日志:
在服务解析过程中,添加详细的错误处理和日志记录,便于调试和维护。
集成其他组件:
将依赖注入容器与其他框架组件(如事件系统、路由缓存)集成,实现更强大的功能。