目录¶
理解依赖注入和依赖注入容器¶
依赖注入(Dependency Injection)¶
依赖注入 是一种设计模式,用于实现控制反转(Inversion of Control, IoC),即对象不再自行创建其依赖项,而是通过外部提供者注入这些依赖项。这样可以实现更松散的耦合,提高代码的可测试性和可维护性。
依赖注入容器(Dependency Injection Container)¶
依赖注入容器 是一个用于管理和自动解析对象依赖关系的工具。它负责实例化类、解析其依赖项,并将其注入到需要的地方。使用 DIC 可以简化对象的创建过程,尤其是在依赖关系复杂时。
选择依赖注入容器¶
有两种主要选择:
使用现有的依赖注入容器库:
PHP-DI:功能强大,支持自动装配、注解等。
Pimple:轻量级,适合小型项目。
Symfony DependencyInjection:功能全面,适用于复杂项目。
自行实现一个简单的依赖注入容器:
适合学习和小型项目。
更加灵活,但需要更多的手动配置。
推荐:对于生产项目,建议使用成熟的依赖注入容器库(如 PHP-DI),以利用其丰富的功能和社区支持。
集成依赖注入容器到框架¶
以下将以 PHP-DI 为例,说明如何将依赖注入容器集成到您的自定义 PHP 框架中。
安装和配置容器¶
通过 Composer 安装 PHP-DI:
在项目根目录下运行以下命令安装 PHP-DI:
composer require php-di/php-di
配置容器:
创建一个容器配置文件(例如
config/container.php
),定义服务和依赖关系。<?php // config/container.php use DIContainerBuilder; use MyFrameworkControllersHomeController; use MyFrameworkControllersUsersController; use MyFrameworkMiddlewareAuthenticationMiddleware; $containerBuilder = new ContainerBuilder(); // 可选:启用编译缓存以提升性能 // $containerBuilder->enableCompilation(__DIR__ . '/../cache'); // 定义依赖关系 $containerBuilder->addDefinitions([ // 控制器 HomeController::class => DIautowire(), UsersController::class => DIautowire(), // 中间件 AuthenticationMiddleware::class => DIautowire(), // 其他服务,例如 Logger MonologLogger::class => function () { $logger = new MonologLogger('app'); $logger->pushHandler(new MonologHandlerStreamHandler(__DIR__ . '/../logs/app.log', MonologLogger::DEBUG)); return $logger; }, // Twig Environment TwigEnvironment::class => function () { $loader = new TwigLoaderFilesystemLoader(__DIR__ . '/../views'); return new TwigEnvironment($loader, [ 'cache' => __DIR__ . '/../cache/twig', 'debug' => true, ]); }, ]); return $containerBuilder->build(); ?>
说明:
自动装配:使用
DIautowire()
让 PHP-DI 自动解析依赖项。自定义服务:对于需要特殊配置的服务(如 Logger 和 Twig),使用闭包定义其创建方式。
定义服务和依赖关系¶
在容器配置文件中,可以定义所有需要的服务及其依赖关系。例如:
控制器:通过自动装配,控制器的依赖项(如 Logger、Twig)将自动注入。
中间件:定义中间件的依赖项,确保它们也通过容器进行管理。
更新路由和控制器以使用依赖注入¶
修改 Router 类¶
为了利用依赖注入容器,需要在 Router 类中集成容器,并通过容器解析控制器实例。
引入容器:
修改
Router.php
,引入容器实例。<?php // src/Router.php namespace MyFramework; use DIContainer; use MyFrameworkMiddlewareMiddleware; class Router { private $routes = []; private $namedRoutes = []; private $container; public function __construct(Container $container) { $this->routes = []; $this->namedRoutes = []; $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($method, $uri, $action, $middlewares = [], $name = null) { // 现有的路由添加逻辑 // ... // 如果有路由名称,存储到 namedRoutes if ($name) { $this->namedRoutes[$name] = end($this->routes); } } /** * 分发请求到相应的控制器方法 * * @param string $requestMethod HTTP 方法 * @param string $requestUri 请求的 URI */ public function dispatch($requestMethod, $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) { $middleware = $this->container->get($middlewareClass); if ($middleware instanceof Middleware) { if (!$middleware->handle($params)) { // 中间件中止请求 return; } } } // 解析控制器和方法 list($controllerName, $method) = explode('@', $route['action']); $fullControllerName = "MyFramework\Controllers\$controllerName"; // 通过容器获取控制器实例 if ($this->container->has($fullControllerName)) { $controller = $this->container->get($fullControllerName); if (method_exists($controller, $method)) { // 调用方法并传递参数 call_user_func_array([$controller, $method], $params); return; } } // 如果控制器或方法不存在,返回 404 $this->sendNotFound(); } } } // 如果没有匹配的路由,返回 404 $this->sendNotFound(); } // ... 其他方法保持不变 ... } ?>
说明:
容器实例:通过构造函数注入容器实例,使 Router 类能够使用容器解析控制器和中间件。
控制器解析:使用
$this->container->get($fullControllerName)
获取控制器实例,确保其依赖项已被正确注入。
修改 Controller 类¶
基础 Controller
类无需显式处理依赖注入,因为依赖已经通过容器在控制器实例化时完成。可以在控制器中直接使用注入的依赖项。
<?php
// src/Controller.php
namespace MyFramework;
use TwigEnvironment;
use MonologLogger;
class Controller
{
protected $twig;
protected $logger;
protected $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($template, $data = [])
{
echo $this->twig->render($template, $data);
}
/**
* 发送 404 响应
*/
protected function sendNotFound()
{
header("HTTP/1.0 404 Not Found");
echo "404 Not Found";
}
}
?>
说明:
依赖注入:通过构造函数注入 Twig 环境、Logger 和 Router 实例,使得控制器方法可以直接使用这些依赖项。
渲染方法:利用注入的 Twig 实例渲染模板。
使用依赖注入容器进行服务解析¶
在框架中,使用依赖注入容器来解析和管理服务,确保各个组件能够轻松获取所需的依赖项。
示例:生成 URL 使用反向路由¶
假设已经实现了命名路由和反向路由功能,可以在控制器中使用容器解析 Router 实例并生成 URL。
<?php
// src/Controllers/HomeController.php
namespace MyFrameworkControllers;
use MyFrameworkController;
use MyFrameworkRouter;
class HomeController extends Controller
{
private $router;
public function __construct(Environment $twig, Logger $logger, Router $router)
{
parent::__construct($twig, $logger, $router);
$this->router = $router;
}
public function index()
{
$this->logger->info("访问主页");
$usersUrl = $this->router->generateUrl('users.list');
$this->render('home/index.html.twig', ['usersUrl' => $usersUrl]);
}
// ... 其他方法 ...
}
?>
说明:
生成 URL:通过依赖注入的 Router 实例,调用
generateUrl
方法生成命名路由的 URL,传递给视图模板。
示例:在视图中使用生成的 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。
总结¶
通过集成 依赖注入容器,可以实现以下优势:
松散耦合:组件之间不直接依赖具体实现,而是通过接口或依赖注入容器进行协作。
可测试性:更容易为组件编写单元测试,因为依赖项可以被轻松替换或模拟。
可维护性:集中管理依赖关系,使得代码更易于维护和扩展。
灵活性:更容易更换或升级依赖项,无需大规模修改代码。
进一步扩展建议¶
服务提供者:
实现服务提供者模式,集中管理服务的注册和配置,提升组织性。
注解支持:
如果使用 PHP-DI,探索其注解功能,进一步简化依赖配置。
多态和接口绑定:
利用容器的接口绑定功能,实现多态和依赖项的灵活替换。
懒加载(Lazy Loading):
配置容器以实现懒加载,提高性能,尤其是对于资源密集型服务。
中间件和控制器依赖注入:
确保所有中间件和控制器都通过容器进行实例化,享受依赖注入的好处。