依赖注入
在本章的最后,我们将介绍 MVC 模式和一般 OOP 中最有趣、最具争议性的主题之一:依赖注入。我们将向你展示为什么依赖注入如此重要,以及如何实现适合我们特定应用程序的解决方案,尽管有很多不同的实现方式可以满足不同的需求。
为什么需要依赖注入?
我们还需要介绍对代码进行单元测试的方法,因此您还没有亲身体验过。但潜在问题源的迹象之一是,当您在代码中使用 new 语句创建一个不属于您代码库的类的实例时,也就是所谓的依赖。使用 new 来创建像 Book 或 Sale 这样的领域对象是没有问题的。用它来实例化模型也是可以接受的。但手动实例化其他东西,如模板引擎、数据库连接或日志记录器,则是应该避免的。支持这一观点的原因有很多:
-
如果您想在两个不同的地方使用一个控制器,而每个地方都需要不同的数据库连接或日志文件,那么在控制器内部实例化这些依赖关系将无法实现这一目的。同一个控制器将始终使用相同的依赖关系。
-
在控制器内部实例化依赖关系意味着控制器完全了解其每个依赖关系的具体实现,也就是说,控制器知道我们正在使用带有 MySQL 驱动程序的 PDO 以及连接凭证的位置。这意味着应用程序中的耦合度很高—所以,这是个坏消息。
-
如果您在任何地方都显式地实例化依赖关系,那么用另一个实现相同接口的依赖关系替换一个依赖关系并不容易,因为您必须搜索所有这些地方,并手动更改实例化。
基于上述原因以及更多原因,提供控制器等类所需的依赖关系,而不是让它自己创建依赖关系,总是有好处的。这是每个人都同意的。问题是如何实现解决方案。有不同的选择:
-
我们有一个构造函数,它(通过参数)预期控制器或任何其他类所需的所有依赖关系。构造函数会将每个参数赋值给类的属性。
-
我们有一个空的构造函数,取而代之的是添加与类的依赖关系相同数量的设置器方法。
-
这两种方法的混合体,我们通过构造函数设置主要的依赖关系,并通过设置器设置其余的依赖关系。
-
发送一个包含所有依赖项的对象作为构造函数的唯一参数,控制器就能从该容器中获取所需的依赖项。
每种解决方案都各有利弊。如果我们的类有很多依赖项,通过构造函数注入所有依赖项会有悖直觉,因此最好使用设置器注入依赖项,尽管一个有很多依赖项的类看起来是个糟糕的设计。如果只有一两个依赖关系,使用构造函数也是可以接受的,而且我们编写的代码也会更少。对于有多个依赖项但并非所有依赖项都是强制性的类,使用混合版本可能是一个不错的解决方案。第四种方案在注入依赖关系时更容易,因为我们不需要知道每个对象期望什么。问题是,每个类都应知道如何获取其依赖项,即依赖项名称,这并不理想。
实现我们自己的依赖注入器
依赖关系注入器的开源解决方案已经面世,但我们认为,自己动手实现一个简单的依赖关系注入器也是一种不错的体验。我们的依赖注入器是一个包含代码所需的依赖实例的类。该类基本上是依赖项名称到依赖项实例的映射,将有两个方法:依赖项的 getter 和 setter。我们不想在依赖关系数组中使用静态属性,因为我们的目标之一是让多个依赖关系注入器使用不同的依赖关系集。在 src/Utils/DependencyInjector.php
中添加以下类:
<?php
namespace Bookstore\Utils;
use Bookstore\Exceptions\NotFoundException;
class DependencyInjector {
private $dependencies = [];
public function set(string $name, $object) {
$this->dependencies[$name] = $object;
}
public function get(string $name) {
if (isset($this->dependencies[$name])) {
return $this->dependencies[$name];
}
throw new NotFoundException(
$name . ' dependency not found.'
);
}
}
php
有了依赖注入器,我们就可以在每次请求给定类时都使用相同的实例,而不是每次都创建一个。这意味着不再需要单例实现;事实上,正如第 4 章 "使用 OOP 创建简洁代码" 中提到的,最好避免使用单例实现。那就让我们摆脱它们吧。在配置读取器中,我们就曾使用过。用以下代码替换 src/Core/Config.php
文件中的现有代码:
<?php
namespace Bookstore\Core;
use Bookstore\Exceptions\NotFoundException;
class Config {
private $data;
public function __construct() {
$json = file_get_contents(
__DIR__ . '/../../config/app.json'
);
$this->data = json_decode($json, true);
}
public function get($key) {
if (!isset($this->data[$key])) {
throw new NotFoundException("Key $key not in config.");
}
return $this->data[$key];
}
}
php
我们使用单例模式的另一个地方是 DB
类。事实上,该类的目的只是为我们的数据库连接提供一个单例,但如果我们不使用它,就可以删除整个类。因此,请删除 src/Core/DB.php
文件。
现在,我们需要定义所有这些依赖关系,并将它们添加到依赖关系注入器中。在路由请求之前,index.php
文件是依赖注入器的好地方。在实例化 Router
类之前添加以下代码:
$config = new Config();
$dbConfig = $config->get('db');
$db = new PDO(
'mysql:host=127.0.0.1;dbname=bookstore',
$dbConfig['user'],
$dbConfig['password']
);
$loader = new Twig_Loader_Filesystem(__DIR__ . '/../../views');
$view = new Twig_Environment($loader);
$log = new Logger('bookstore');
$logFile = $config->get('log');
$log->pushHandler(new StreamHandler($logFile, Logger::DEBUG));
$di = new DependencyInjector();
$di->set('PDO', $db);
$di->set('Utils\Config', $config);
$di->set('Twig_Environment', $view);
$di->set('Logger', $log);
$router = new Router($di);
//...
php
现在我们需要做一些更改。其中最重要的是 AbstractController
,该类将大量使用依赖注入器。在该类中添加名为 $di
的属性,并用以下代码替换构造函数:
public function __construct(
DependencyInjector $di,
Request $request
) {
$this->request = $request;
$this->di = $di;
$this->db = $di->get('PDO');
$this->log = $di->get('Logger');
$this->view = $di->get('Twig_Environment');
$this->config = $di->get('Utils\Config');
$this->customerId = $_COOKIE['id'];
}
php
其他更改涉及 Router
类,因为我们现在将其作为构造函数的一部分发送,并且需要将其注入到我们创建的控制器中。在该类中也添加一个 $di
属性,并将构造函数改为如下所示:
public function __construct(DependencyInjector $di) {
$this->di = $di;
$json = file_get_contents(__DIR__ . '/../../config/routes.json');
$this->routeMap = json_decode($json, true);
}
php
还要更改 executeController
和 route
方法的内容:
public function route(Request $request): string {
$path = $request->getPath();
foreach ($this->routeMap as $route => $info) {
$regexRoute = $this->getRegexRoute($route, $info);
if (preg_match("@^/$regexRoute$@", $path)) {
return $this->executeController(
$route, $path, $info, $request
);
}
}
$errorController = new ErrorController(
$this->di,
$request
);
return $errorController->notFound();
}
private function executeController(
string $route,
string $path,
array $info,
Request $request
): string {
$controllerName = '\Bookstore\Controllers\\' . $info['controller'] . 'Controller';
$controller = new $controllerName($this->di, $request);
if (isset($info['login']) && $info['login']) {
if ($request->getCookies()->has('user')) {
$customerId = $request->getCookies()->get('user');
$controller->setCustomerId($customerId);
} else {
$errorController = new CustomerController(
$this->di,
$request
);
return $errorController->login();
}
}
$params = $this->extractParams($route, $path);
return call_user_func_array(
[$controller, $info['method']], $params
);
}
php
最后还有一个地方需要修改。CustomerController
的 login
方法也在实例化一个控制器,因此我们也需要在这里注入依赖注入器:
$newController = new BookController($this->di, $this->request);
bash