12.15. Services(サービス)とDependency Injection。
Drupalを使っていて、カスタムモジュールでコントリビュートモジュールやコアモジュールのコードを使用する必要がある場合、hookとservices(サービス)を使用します。この記事では、すでにフックを使用しました:
では、サービスについて見ていきましょう。Service(サービス)はPHPオブジェクトです。そのため、カスタムモジュールで新しいPHPクラスを作成するときは、後でコードを標準的な方法で他のモジュールでも使用できるように、最初からserviceとして定義するのがよいでしょう。
Drupalはすべてのservices(サービス)をPHPオブジェクトのService Containerに集約するため、利用可能なすべてのサービスと使用中のサービスに関する情報を1つの場所に保存します。このオブジェクトを呼び出して、どのサービスが使用されているかを確認できます:
<?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コンテナの変数はstaticとして定義されています。つまり、index.php の呼び出し後からリクエスト処理の終了まで、呼び出しの過程で任意のファイルでこの変数の値を取得できます。それはモジュール内の任意のクラスやフック、さらにはテーマの .theme ファイルであることもあります。
Drupalでサービスをどのように使用するか?
次に、DrupalでService Containerをどのように使用するかを見てみましょう。$container オブジェクトにはサービスオブジェクトが保存されており、オブジェクトを作成するために必要なすべてのロジックをコンストラクターで実行し、使用可能な状態のオブジェクトをカスタムモジュールに渡すことができます。たとえば、データベースへの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 }
$container 変数には set() メソッドでサービスを追加できますが、これは通常、テストで依存関係をモック(mocking)するために使用されます:
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') が呼び出される場合、返されたオブジェクトはコンストラクター __contruct() にメソッドの引数として渡されます(この例では、$container->get('database') と引数 Connection $database)。
Controller(コントローラー)、Block(ブロック)、BaseForm(フォーム)、ConfigForm(設定フォーム)、カスタムクラス/サービスでServiceコンテナからオブジェクトを正確に呼び出す方法については、次の記事で説明します。
Service containerからオブジェクトを正しく接続して使用する方法を見た後、独自のサービスを作成する方法を見ていきます。
また、コントリビュートモジュールのクラスの代わりにカスタムモジュールのクラスを使用できるように、サービスのクラスをオーバーライドする方法も見ていきます。
Service containerとDependency Injectionはなぜ必要なのか?
namespaces(名前空間)を使用してモジュールのコードを直接インポートし、Serviceコンテナなしでも必要な場所でサードパーティのクラスのオブジェクトを作成して呼び出すことができます。しかし、これはコード更新の問題を引き起こします。たとえば、メール送信用のクラスを置き換える必要があり、このクラスが200箇所で呼び出されているとします。コードの更新を容易にするために、ファイルを直接インポートするのではなくServiceを作成します。これで、PHP mail() ではなくsmtpでメールを送信したい場合、新しいクラスへのパスを200箇所で変更するのではなく、サービスのクラスを変更するだけですみます。
Dependency Injectionは、1つのクラスでサービスを二重に呼び出す問題を解決します。同じクラスのさまざまなメソッドでサービスを使用する場合、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モジュールからサービスを追加する場合、その名前は次のようになります:
モジュール名.サービス名
サービス名は *.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')
);
}