8.3 依赖注入
Drupal 8 使用 Symfony 的依赖注入容器管理服务实例以及把这些服务注入到需要他们的类内。依赖注入可以解耦功能和改善可测试性。仅出于这两个原因就有必要了解清楚依赖注入是什么及怎样使用它。
上节你已经创建和使用了一个服务,现在你可能想更深入的理解依赖注入和服务定义。
服务定义
Symfony 文档的 Service Container 部分定义了服务(Services)。
简单的说,一个服务是执行某种全局任务的一些 PHP 对象。它是计算机术语中的一个通用名称, 用于描述为特定目的而创建的一个对象(例如投递邮件)。
服务是执行了一项任务的对象(你想在全局范围内重复使用,但又无需多次重复书写相同的代码)。这是实用的,例如设置一个数据库联接或者创建一个邮件发送器。服务把功能分割为可重用的组件。一个服务是执行了一些全局操作的 PHP 类,并且被服务容器注册以便能够被访问。
服务容器是存在的另一个对象 —– 它管理服务的集合。你使用 [module_name].services.yml
实例化服务,之后这些信息会保存在容器对象里。通过服务容器的 get 和 set 方法访问这些服务。
通常,你使用依赖注入把服务容器传递到自己的方法内,之后调用那些服务。
通常服务的例子: 配置和 Config 对象
Drupal 8 提供了一个 Config 对象,我们能够使用它与配置通信。
就象 services and dependency injection in Drupal 8 描述的那样,一些类已经通过依赖注入生效了。
Drupal 8 为了解耦合可重用的功能引入了服务的概念,把它们注册到服务容器使它们可插拔、可替换。 作为开发者,通过服务容器访问 Drupal 提供的服务是比较好的实践方法。
Symfony 做了类似的陈述:
“使类依赖明确,并以注入的方式实现这种依赖关系”是使类可重用、可测试和解耦合的一种比较好的方式。
在第 3 课,我们讨论了配置,为模块 services_example 引入了 ConfigFormBase
类。这个类就是使用依赖注入提供了 Config 对象的类。使用父类的 config 方法返回了填充 demo.settings
简单配置的 config 对象。因此你可以说这个方法注入了这个配置变量(设置)。
依赖注入
什么是依赖注入?维基百科这样定义:
依赖注入是一种软件设计模式,用于把一个或多个依赖(或服务)注入或传递给依赖它们的对象。 这种模式把创建依赖从自己的行为中分离出去,从而达到解耦合的目的,遵循了依赖倒置和单一职责的原则。
Drupal.org 文档这样描述依赖注入:
依赖注入是 Drupal 8 中使用服务的首选方法,应该尽可能的使用它。
为什么使用依赖注入?
在某些情况下,需要使用依赖注入覆盖核心服务。如果一个开发者要取代核心服务实现自己的逻辑,比如校验页面的访问权限,那么可以使用依赖注入。这种情况的使用例子可以查看 Access checking on routes。最通常需要依赖注入的情况是当使用象 PHPUnit 这样的工具进行单体测试的时候。
不应该做什么 —– 静态地加载服务
许多 Drupal 8 指南演示通过 \Drupal
类静态加载服务。使用过 Drupal 7 的开发者可能发现这么写比较快,但使用依赖注入是技术上首选的方式。如果你不理解服务容器和依赖注入那么你可能还会静态的请求服务,象过程式编程那样耦合你的代码。这违背最佳实践,阻碍你开发干净的模块和代码。例如:(见辅助内容区)
服务、依赖注入和服务容器是 Drupal 的未来。这些新概念比较有挑战性,但对于理解 Drupal 的核心如何演变为新的东西至关重要。
$serviceExampleService = \Drupal::service('service_example.example_service');;
$value = $serviceExampleService->getServiceExampleValue();
// or
$value = \Drupal\service_example\ServiceExampleService::getServiceExampleValu
服务或依赖注入容器
下一个概念是服务容器。我们在 Drupal 应用中使用服务以标准、集中的方式进行对象的实例化、组织和取得。
在控制器里访问容器很容易。我们可以继承 ControllerBase
类,或者实现 ContainerInjectionInterface
接口,这都能叫我们访问到容器。
<?php
/**
* @file
* Contains \Drupal\service_example\ServiceExampleController.
*/
namespace Drupal\service_example\Controller;
use Drupal\service_example\ServiceExampleService;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ServiceExampleController extends ControllerBase {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('service_example.example_service')
);
}
}
在构造器和 create 方法内加载两个或更多的服务
控制器中的 create() 方法得到传递给它的容器,之后能把容器中的任何东西注入到构造方法。因此如果你知道你的控制器要访问什么服务,你可以向辅助内容区的代码这样做:
class QueryExampleController extends ControllerBase {
protected $entity_query;
protected $entity_manager;
public function __construct(QueryFactory $entity_query, EntityManagerInterface $entity_manager) {
$this->entity_query = $entity_query;
$this->entity_manager = $entity_manager;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.query'),
$container->get('entity.manager')
);
}
之后的代码,你可以访问 entity manager,你可以使用它加载节点:
这种方式与核心加载它所需要服务的方式类似。
$nids = $this->advancedQuery();
/** @var $node_storage \Drupal\node\NodeStorage */
$node_storage = $this->entity_manager->getStorage('node');
$nodes = $node_storage->loadMultiple($nids);