12.15. 服务和依赖注入。
当我们使用 Drupal 并且需要在自定义模块中使用贡献模块或核心模块的代码时,我们会使用 hook(钩子)和 services(服务)。我们已经在这篇文章中使用过钩子:
现在让我们来了解一下 services。Service 是一个 PHP 对象。因此,当你在自定义模块中创建一个新的 PHP 类时,最好立即将其设计为 service,这样以后你的代码就可以在其他模块中以标准方式使用。
Drupal 将所有的 services 收集到一个 PHP 对象 Service Container 中,所以 Drupal 会在一个地方保存关于所有可用和已使用的服务的信息。你可以调用这个对象并查看有哪些服务正在使用:
<?php
$container = \Drupal::getContainer();
?>
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x
你可以通过 has/get/set 方法操作这个对象,但通常我们会通过模块中的 *.services.yml 文件来向容器中添加服务。
让我们来看一下 getContainer()
方法的实现:
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container is not initialized yet. \\Drupal::setContainer() must be called with a real container.');
}
return static::$container;
}
?>
Service 容器的变量被定义为静态的,这意味着在 index.php 调用之后到请求处理结束之前,我们可以在调用流程中的任何文件中获取这个变量的值:它可以是任何类、模块中的钩子,甚至是主题的 .theme 文件。
如何在 Drupal 中使用服务?
现在我们来看一下如何在 Drupal 中使用 Service Container。在 $container 对象中保存着 services 的对象,这允许我们在构造函数中完成对象所需的全部逻辑,并将一个已经准备好使用的对象传递给我们的自定义模块。例如,如果我们需要写一个 SQL 查询到数据库,我们只需从 Service 容器中调用数据库服务对象,这个对象会使用我们 settings.php 文件中的凭据并在执行 SQL 查询时与 MySQL 建立连接:
$query = \Drupal::database()->select('node_field_data', 'n');
$query->addField('n', 'nid');
$query->condition('n.title', 'About Us');
$query->range(0, 1);
$nid = $query->execute()->fetchField();
如果你查看 database()
方法的实现,就会看到我们使用的是来自 Service 容器的 database 服务对象:
https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3Adatabase/9.2.x
<?php
public static function database() {
return static::getContainer()
->get('database');
}
?>
这样我们只会加载代码中所需的类。正是为此我们使用了 Service 容器作为统一的对象存储。
如何将服务添加到 Service container?
当我们创建 *.services.yml 文件时,Drupal 会加载这些文件中的服务并将它们的对象保存到 Service container 中。
https://api.drupal.org/api/drupal/core%21modules%21syslog%21syslog.services.yml/9.2.x
core/modules/syslog/syslog.services.yml:
services:
logger.syslog:
class: Drupal\syslog\Logger\SysLog
arguments: ['@config.factory', '@logger.log_message_parser']
tags:
- { name: logger }
你可以通过 set() 方法将服务添加到 $container,但这通常用于在测试中 mock(模拟)依赖:
什么是 Dependency Injection?
如果你运行 Code Sniffer,它会提示错误,要求你修改 Drupal::database() 并在使用 Service 容器对象的类的构造函数中调用 database。当你在类的构造函数中从 Service Container 调用对象时,这就叫做 Dependency Injection (DI),例如:
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
/**
* Implements the WisenetConfigurationForm form controller.
*
* This example demonstrates a simple form with a singe text input element. We
* extend FormBase which is the simplest form base class used in Drupal.
*
* @see \Drupal\Core\Form\FormBase
*/
class WisenetGetCourseForm extends FormBase {
/**
* Active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a WisenetGetCourseForm object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
...
/**
* Implements course save handler.
*
* Function for save course data in course content type.
*/
public function saveCourse($courses) {
...
$query = $this->database->select('node__field_course_wisenet_id', 'nc');
$query->addField('n', 'nid');
$query->join('node_field_data', 'n', 'nc.entity_id = n.nid');
$query->condition('nc.field_course_wisenet_id_value', $course['CourseOfferId']);
$query->range(0, 1);
$nid = $query->execute()->fetchField();
...
}
在这个例子中,表单需要对数据库的查询,因此我们添加了一个额外的 create() 方法,用于创建类的实例。create() 方法可能存在于不同的类和接口中,但它的参数始终包含一个 ContainerInterface 类型的 $container 变量。如果在 create() 方法中调用了 Service container 中的对象 $container->get('myservice.name'),那么返回的对象会作为参数传递到构造函数 __construct() 中(在我们的例子中是 $container->get('database') 和参数 Connection $database)。
关于如何在控制器(Controller)、区块(Block)、表单(BaseForm)、配置表单(ConfigForm)以及自定义类/服务的构造函数中正确调用 Service 容器中的对象,我们将在接下来的文章中进行讲解。
在学习如何正确加载和使用 Service container 中的对象之后,我们将学习如何创建自定义服务。
我们还将学习如何重写服务的类,从而可以用自定义模块的类替换贡献模块的类。
为什么需要 Service container 和 Dependency Injection?
我们可以使用 namespaces 并直接加载模块中的代码,在需要的地方实例化对象而不通过 Service container。但这会带来代码更新的问题。例如,如果我们需要替换发送邮件的类,而这个类在 200 个地方被调用,那么我们就必须在这 200 个地方修改路径。为了便于代码更新,我们会创建 Service,而不是直接引入文件。这样当我们想通过 smtp 而不是 PHP mail() 发送邮件时,只需要修改服务的类,而不是在 200 个地方修改代码。
Dependency Injection 解决了在一个类中多次调用服务的问题。如果我们在一个类的不同方法中使用服务,就不需要每次都访问 Service container。我们只需将服务对象保存到类的属性中,并通过 $this->serviceName 来使用。
当然,我们也可以不使用 Service container 和 Dependency Injection,但这些模式统一了我们的代码,使其更简洁并且更易于维护和更新。
我在哪里可以看到服务的名称?
在我们的例子中,有一个名为 "database" 的服务:
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
// Here we add service name.
$container->get('database'),
);
}
但如果你添加的是来自 contrib/custom 模块的服务,它可能会有这样的名称:
module_name.service_name
你可以在 *.services.yml 文件中检查服务的名称。服务名称不一定以 module_name.* 开头,但通常是这样的。例如:
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('commerce_cart.cart_provider'),
$container->get('entity_type.manager')
);
}