logo

额外区块类型 (EBT) - 全新的布局构建器体验❗

额外区块类型 (EBT) - 样式化、可定制的区块类型:幻灯片、标签页、卡片、手风琴等更多类型。内置背景、DOM Box、JavaScript 插件的设置。立即体验布局构建的未来。

演示 EBT 模块 下载 EBT 模块

❗额外段落类型 (EPT) - 全新的 Paragraphs 体验

额外段落类型 (EPT) - 类似的基于 Paragraph 的模块集合。

演示 EPT 模块 滚动

滚动

12.15. 服务和依赖注入。

29/09/2025, by Ivan

Menu

当我们使用 Drupal 并且需要在自定义模块中使用贡献模块或核心模块的代码时,我们会使用 hook(钩子)和 services(服务)。我们已经在这篇文章中使用过钩子:

12.11.3. 用于处理 Entity 的钩子。

现在让我们来了解一下 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

Get container

Service container

你可以通过 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(模拟)依赖:

https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/mocking-entities-and-services-with-phpunit-and-mocks

什么是 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')
  );
}