DrupalカスタムモジュールでのPHPコンストラクタのプロパティプロモーションの使用
PHP 8 ではコンストラクタのプロパティプロモーションが導入されました。これは、コンストラクタのシグネチャ内でプロパティを宣言・初期化できるようにすることで、クラスプロパティの定義と代入を簡素化する機能です。このチュートリアルでは、Drupalカスタムモジュール(PHP 8.0以上が必要)でコンストラクタのプロパティプロモーションを使用する方法を説明します。具体的には、サービスやコントローラでの依存性注入を簡素化する方法を示します。従来のDrupalパターン(PHP 7や初期のDrupal 9で使用)と、最新のPHP 8以上のアプローチを、両方の完全なコード例を使って比較します。最後には、この最新構文がどのようにボイラープレートを減らし、コードを明確にし、最新のベストプラクティスに沿うものかをご理解いただけるでしょう。
Drupal 10(PHP 8.1以上が必要)ではコアでこうした最新のPHP機能の採用が始まっているため、カスタムモジュール開発者にも同様に採用することが推奨されています。まずはDrupalの従来の依存性注入パターンを確認し、その後でコンストラクタのプロパティプロモーションを使ってリファクタリングしましょう。
Drupalにおける従来の依存性注入(PHP 8以前)
Drupalのサービスやコントローラでは、依存性注入の従来パターンは以下の3つのステップからなります:
-
各依存関係をクラスプロパティとして宣言する(通常は
protected)、適切な docblock を付けます。 -
コンストラクタのパラメータで各依存関係に型ヒントを指定し、コンストラクタ内でクラスプロパティに代入します。
-
コントローラ(一部のプラグインクラス)では、静的メソッド
create(ContainerInterface $container)を実装して、Drupalのサービスコンテナからサービスを取得し、クラスをインスタンス化します。
この結果、かなりの量のボイラープレートコードが生じます。たとえば、設定ファクトリとロガーファクトリを必要とするシンプルなカスタムサービスを考えてみましょう。従来は、次のように書くことになります:
従来のサービスクラスの例
<?php
namespace Drupal\example;
/**
* Example service that logs the site name.
*/
class ExampleService {
/**
* The configuration factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The logger channel factory service.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Constructs an ExampleService object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger channel factory.
*/
public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
// Store the injected services.
$this->configFactory = $config_factory;
$this->loggerFactory = $logger_factory;
}
/**
* Logs the site name as an example action.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
上記では、$configFactoryと$loggerFactoryの2つのプロパティを宣言し、コンストラクタ内で代入しています。対応するサービスも、モジュールのservices YAMLに定義する必要があります(必要なサービスを引数として指定)。例えば:
# example.services.yml
services:
example.example_service:
class: Drupal\example\ExampleService
arguments:
- '@config.factory'
- '@logger.factory'
Drupalがこのサービスをインスタンス化すると、設定された引数がリストの順序でコンストラクタに渡されます。
従来のコントローラクラスの例
Drupalのコントローラでも依存性注入を使用できます。通常、コントローラクラスはControllerBaseを拡張し(便利なメソッド用)、create()メソッドを定義してDrupalのコンテナ注入を実装します。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;
/**
* Controller for Example routes.
*/
class ExampleController extends ControllerBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* Constructs an ExampleController object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Retrieve required services from the container and pass them to the constructor.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Builds a simple page response.
*/
public function build(): array {
// Example usage of the injected services.
$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+)を使用する
コンストラクタのプロパティプロモーションは、コンストラクタのシグネチャ内でプロパティを宣言・代入することを1ステップで可能にすることで、上記のパターンを合理化します。PHP 8では、コンストラクタのパラメータに可視性(やreadonlyのような他の修飾子)を前置でき、PHPが自動的にプロパティを作成・代入します。つまり、プロパティを別途宣言したり、コンストラクタ内で代入を書いたりする必要がなくなります。PHPが代わりに行ってくれます。
重要なのは、これは構文糖衣(シンタックスシュガー)である点です。Drupalの依存性注入の仕組みは変わりません。単に書くコード量を減らすだけです。サービスは引き続きYAMLで登録します(またはDrupalにオートワイヤリングさせます)。コントローラについても、create()ファクトリメソッドを使い続けます(コントローラをサービスとして登録しない限り)。違いはクラスのコードの書き方だけです。その結果、ボイラープレートが大幅に減ります。Drupalコアのissueでは、数十行に及ぶ宣言と代入がコンストラクタ内のわずか数行に削減されました。
例をコンストラクタのプロパティプロモーションを使ってリファクタリングしましょう。
コンストラクタのプロパティプロモーションを使ったサービスクラス
PHP 8のプロモートプロパティ構文を使って書き直したExampleServiceは次のとおりです:
<?php
namespace Drupal\example;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Example service that logs the site name (using property promotion).
*/
class ExampleService {
/**
* Constructs an ExampleService object with injected services.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* The logger channel factory.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected LoggerChannelFactoryInterface $loggerFactory
) {
// No body needed; properties are automatically set.
}
/**
* Logs the site name as an example action.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
コンストラクタのプロパティプロモーションを使ったコントローラクラス
次に、プロモートプロパティを使うようにリファクタリングしたExampleControllerを考えます:
<?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;
/**
* Controller for Example routes (using property promotion).
*/
final class ExampleController extends ControllerBase {
/**
* Constructs an ExampleController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* The string translation service.
*/
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
private TranslationInterface $stringTranslation
) {
// No need for assignments; properties are set automatically.
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Pass services from the container into the constructor.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Builds a simple page response.
*/
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のクラスでコンストラクタのプロパティプロモーションを使用すると、いくつかの利点があります:
-
ボイラープレートの削減:書くコードが大幅に減ります。プロパティを手動で宣言・代入する必要がなく、クラスに複数の依存関係がある場合には多くの行を削減できます。これにより、モジュールがよりクリーンで保守しやすくなります。
-
より明確で簡潔なコード:クラスの依存関係がすべて一箇所(コンストラクタのシグネチャ)に表示されます。プロパティ宣言とコンストラクタ本体に分散していた従来の方法とは異なります。これにより可読性が向上し、クラスがどのサービスを必要とするかが一目でわかります。
-
必要なドキュメントコメントが少ない:プロパティがコンストラクタ内で型付きで宣言されるため、それらのプロパティやコンストラクタパラメータについて冗長な
@varや@paramアノテーションを省略できます(命名から目的が明らかな場合)。コードはかなりの程度自己文書化されます。不明瞭な点については引き続きドキュメント化できますが、繰り返しは少なくなります。 -
モダンなPHP構文:プロパティプロモーションを採用することで、コードが最新のPHPプラクティスに沿ったものになります。Drupal 10以上のコアは新しいコードでこの構文を使い始めているため、カスタムモジュールで使用することで、コアやコミュニティの例とより一貫性のあるコードになります。また、将来の機能強化にも備えることができます(たとえばPHP 8.1以上では、真に不変な依存関係のためにプロモートプロパティへの
readonlyキーワードの使用が可能になります)。
パフォーマンスと機能は従来の注入と同じままです。プロパティプロモーションは純粋に言語上の利便性です。クラス全体で使用できる完全に型ヒント付きのプロパティが引き続き得られます(たとえばコントローラ例の$this->entityTypeManager)。内部では、結果はより長いコードと同等で、より少ない労力で実現されるだけです。
まとめ
コンストラクタのプロパティプロモーションは、シンプルながら強力なPHP 8の機能で、Drupal開発者がカスタムモジュール開発を簡素化するために活用できます。ボイラープレートを排除することで、サービスの配線ではなく、クラスが実際に行うことに集中できるようになります。典型的なDrupalのサービスクラスとコントローラクラスをプロモートプロパティを使うように変換する方法を示し、従来のアプローチと比較しました。その結果、明確さや機能性を損なうことなく、より簡潔で保守しやすいコードになります。Drupalが最新のPHP要件へと進むにつれて、カスタムモジュールでプロパティプロモーションのような機能を使用することは、コードをクリーンで明確に保ち、最新のベストプラクティスに沿ったものにするのに役立ちます。モダンな構文を採用して、Drupal開発をより簡単に、よりエレガントにしましょう。