logo

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

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

演示 EBT 模块 下载 EBT 模块

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

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

演示 EPT 模块 滚动

滚动

在 Drupal 自定义模块中使用 PHP 构造函数属性提升(Constructor Property Promotion)

02/09/2025, by Ivan

Menu

PHP 8 引入了 构造函数属性提升(constructor property promotion)功能,它通过允许你在构造函数签名中声明和初始化属性,从而简化了类属性的定义与赋值过程。本教程演示了如何在 Drupal 自定义模块中使用构造函数属性提升(需要 PHP 8.0 及以上),特别是用于简化服务和控制器中的依赖注入。我们将对比传统的 Drupal 模式(用于 PHP 7 和早期的 Drupal 9)与现代的 PHP 8+ 方法,分别提供完整代码示例。最终,你将看到这种现代语法如何减少样板代码,使代码更清晰,并符合当前最佳实践。

Drupal 10(需要 PHP 8.1+)核心已经开始采用这些现代 PHP 特性,因此鼓励自定义模块开发者也这样做。我们先来回顾 Drupal 中传统的依赖注入模式,然后使用构造函数属性提升进行重构。

Drupal 中的传统依赖注入方式(PHP 8 之前)

在 Drupal 的服务和控制器中,传统依赖注入模式包括三个步骤:

  1. 将每个依赖项声明为类属性(通常为 protected),并加上相应的 docblock 注释。

  2. 在构造函数参数中使用类型提示,并在构造函数中将其赋值给类属性。

  3. 对于控制器(以及一些插件类),实现静态方法 create(ContainerInterface $container),从 Drupal 的服务容器中获取服务并实例化类。

这种方式会产生大量样板代码。例如,一个需要配置工厂和日志工厂的简单自定义服务,传统写法如下:

传统服务类示例

<?php

namespace Drupal\example;

/**
 * 示例服务:记录站点名称。
 */
class ExampleService {
  /**
   * 配置工厂服务。
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * 日志频道工厂服务。
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * 构造函数。
   */
  public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
    $this->configFactory = $config_factory;
    $this->loggerFactory = $logger_factory;
  }

  /**
   * 示例方法:记录站点名称。
   */
  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

在上面的代码中,我们声明了两个属性 $configFactory$loggerFactory 并在构造函数中赋值。对应的服务也需要在模块的 services YAML 文件中定义,并配置所需的服务参数:

# example.services.yml
services:
  example.example_service:
    class: Drupal\example\ExampleService
    arguments:
      - '@config.factory'
      - '@logger.factory'

当 Drupal 实例化该服务时,会按顺序将参数传入构造函数。

传统控制器类示例

Drupal 控制器也可以使用依赖注入。通常,控制器类继承自 ControllerBase(提供辅助方法),并通过实现 create() 方法来从容器中拉取服务。例如:

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 示例路由控制器。
 */
class ExampleController extends ControllerBase {
  protected $entityTypeManager;
  protected $stringTranslation;

  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
    $this->entityTypeManager = $entity_type_manager;
    $this->stringTranslation = $string_translation;
  }

  public static function create(ContainerInterface $container): self {
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  public function build(): array {
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

在 Drupal 中使用构造函数属性提升(PHP 8+)

构造函数属性提升 允许你在构造函数签名中同时声明和初始化属性。在 PHP 8 中,你可以在构造函数参数前添加可见性(如 protectedprivate),PHP 会自动为你创建和赋值属性。你无需再单独声明属性或在构造函数中赋值。

需要注意,这只是 语法糖,并不改变 Drupal 的依赖注入机制。你仍需在 YAML 中注册服务(或依赖自动注入),控制器仍需使用 create() 方法(除非你将控制器注册为服务)。不同之处仅在于类的写法更简洁。

我们将使用属性提升重写上面的示例。

使用属性提升的服务类

<?php

namespace Drupal\example;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * 使用构造函数属性提升的服务。
 */
class ExampleService {
  public function __construct(
    protected ConfigFactoryInterface $configFactory,
    protected LoggerChannelFactoryInterface $loggerFactory
  ) {}

  public function logSiteName(): void {
    $site_name = $this->configFactory->get('system.site')->get('name');
    $this->loggerFactory->get('example')->info('Site name: ' . $site_name);
  }
}

使用属性提升的控制器类

<?php

namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 使用构造函数属性提升的控制器。
 */
final class ExampleController extends ControllerBase {
  public function __construct(
    private EntityTypeManagerInterface $entityTypeManager,
    private TranslationInterface $stringTranslation
  ) {}

  public static function create(ContainerInterface $container): self {
    return new self(
      $container->get('entity_type.manager'),
      $container->get('string_translation')
    );
  }

  public function build(): array {
    $node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
    return [
      '#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
    ];
  }
}

构造函数属性提升的优势

  • 减少样板代码: 无需重复声明和赋值属性,尤其是依赖项多时,可显著减少代码量,使模块更清晰、易维护。
  • 代码更简洁明了: 所有依赖项都集中在构造函数签名中,更容易阅读与理解。
  • 减少注释: 属性类型在构造函数中已明确,通常不再需要额外的 @var@param 注释。
  • 现代 PHP 语法: 与 Drupal 10 核心代码保持一致,提升代码一致性与可维护性,也为将来如 readonly 等特性做好准备。

注意: 属性提升不会改变功能或性能,仅是书写更简洁。你仍可像以前一样使用 $this->entityTypeManager 等属性,底层逻辑与传统方式完全等效。

结论

构造函数属性提升是 PHP 8 中一个简单但强大的功能,Drupal 开发者可以通过它简化自定义模块开发。通过减少样板代码,你可以专注于类的实际功能。我们演示了如何将传统的服务类和控制器类重构为使用属性提升的形式。结果是代码更简洁、可维护性更强,并符合现代 PHP 与 Drupal 最佳实践。拥抱现代语法,让你的 Drupal 开发更加高效优雅。