12.15. Services und Dependency Injection.
Wenn wir Drupal verwenden und Code eines Contributed-Moduls oder Kernmoduls in einem Custom-Modul verwenden müssen, dann nutzen wir Hooks und Services. Hooks haben wir bereits in diesem Artikel verwendet:
12.11.3. Hooks für die Arbeit mit Entities.
Schauen wir uns nun Services an. Ein Service ist ein PHP-Objekt. Wenn Sie also eine neue PHP-Klasse in Ihrem Custom-Modul erstellen, ist es besser, diese direkt als Service zu gestalten, damit Ihr Code später standardmäßig in einem anderen Modul verwendet werden kann.
Drupal sammelt alle Services im PHP-Objekt Service Container, so dass Drupal alle Informationen über verfügbare und genutzte Services an einem Ort speichert. Sie können dieses Objekt aufrufen und sehen, welche Services Sie verwenden:
<?php
$container = \Drupal::getContainer();
?>
https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x
Sie können mit diesem Objekt über die Methoden has/get/set arbeiten, aber normalerweise fügen wir Services über *.services.yml-Dateien in unseren Modulen zum Container hinzu.
Schauen wir uns die Implementierung der Methode getContainer() an:
<?php
public static function getContainer() {
if (static::$container === NULL) {
throw new ContainerNotInitializedException('\\Drupal::$container ist noch nicht initialisiert. \\Drupal::setContainer() muss mit einem echten Container aufgerufen werden.');
}
return static::$container;
}
?>
Die Variable des Service Containers ist als statisch definiert, das bedeutet, dass wir nach dem Aufruf von index.php bis zum Ende der Anfrage von überall im Code den Wert dieser Variablen abrufen können: sei es eine Klasse, ein Hook im Modul oder sogar eine .theme-Datei im Theme.
Wie verwendet man Services in Drupal?
Gehen wir nun dazu über, wie man den Service Container in Drupal verwendet. Im Objekt $container sind Service-Objekte gespeichert, was es erlaubt, die gesamte Logik für das Erzeugen eines Objekts im Konstruktor auszuführen und uns dann das gebrauchsfertige Objekt in unser Custom-Modul zu übergeben. Zum Beispiel wollen wir eine SQL-Abfrage an die Datenbank schreiben; wir rufen einfach das Datenbankobjekt aus dem Service Container auf. Dieses Datenbankobjekt nutzt die Zugangsdaten aus unserer settings.php-Datei und stellt bei Ausführung der SQL-Abfrage die Verbindung zu MySQL her:
$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();
Wenn Sie die Implementierung der Methode database() anschauen, sehen Sie, dass wir den Datenbankservice aus dem Service Container verwenden:
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');
}
?>
So binden wir nur die Klassen für unseren Code ein, die wir gerade benötigen. Deshalb verwenden wir das einheitliche Objekt Service Container.
Wie fügt man einen Service zum Service Container hinzu?
Wenn wir eine *.services.yml-Datei erstellen, lädt Drupal die Services aus diesen Dateien und speichert die Objekte im 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 }
Man kann dem $container mit der Methode set() einen Service hinzufügen, aber das wird normalerweise verwendet, um Abhängigkeiten in Tests zu mocken (Mocking):
Was ist Dependency Injection?
Wenn Sie Code Sniffer ausführen, bekommen Sie eine Fehlermeldung, dass Drupal::database() angepasst werden muss und database im Konstruktor der Klasse, in der das Service-Objekt verwendet wird, aufgerufen werden soll. Wenn Sie ein Objekt aus dem Service Container im Konstruktor einer Klasse abrufen, nennt man das Dependency Injection (DI), zum Beispiel:
<?php
namespace Drupal\wisenet_connect\Form;
use Drupal\Core\Database\Connection;
/**
* Implementiert den Formular-Controller WisenetConfigurationForm.
*
* Dieses Beispiel zeigt ein einfaches Formular mit einem Texteingabefeld.
* Wir erweitern FormBase, die einfachste Formular-Basisklasse in Drupal.
*
* @see \Drupal\Core\Form\FormBase
*/
class WisenetGetCourseForm extends FormBase {
/**
* Aktive Datenbankverbindung.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Konstruktor des WisenetGetCourseForm-Objekts.
*
* @param \Drupal\Core\Database\Connection $database
* Die zu nutzende Datenbankverbindung.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
);
}
...
/**
* Implementiert den Speichervorgang für Kurse.
*
* Funktion zum Speichern von Kursdaten im Inhaltstyp Kurs.
*/
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();
...
}
In diesem Beispiel wird eine Datenbankabfrage im Formular benötigt, deshalb haben wir eine zusätzliche Methode create() hinzugefügt, die zur Erstellung der Klasseninstanz verwendet wird. Die create()-Methode kann in verschiedenen Klassen und Interfaces existieren, sie hat aber immer einen Parameter $container vom Typ ContainerInterface. Wenn in der Methode create() ein Objekt aus dem Service Container $container->get('myservice.name') aufgerufen wird, wird das zurückgegebene Objekt an den Konstruktor __construct() als Argument übergeben (in unserem Fall $container->get('database') als Argument Connection $database).
Wie man Objekte aus dem Service Container im Konstruktor von Controllern, Blöcken, Formularen (BaseForm), Konfigurationsformularen (ConfigForm) oder benutzerdefinierten Klassen/Services aufruft, werden wir in den nächsten Artikeln behandeln.
Nachdem wir betrachtet haben, wie man Objekte aus dem Service Container korrekt einbindet und verwendet, werden wir lernen, wie man eigene Services erstellt.
Wir werden auch anschauen, wie man Klassen für Services überschreibt, um statt der Klasse eines Contributed-Moduls eine Klasse aus dem Custom-Modul zu verwenden.
Wozu braucht man Service Container und Dependency Injection?
Wir könnten Namespaces verwenden und Code aus Modulen direkt einbinden, Objekte für externe Klassen dort erzeugen, wo wir sie brauchen – ganz ohne Service Container. Das führt aber zu Problemen bei der Code-Wartung. Zum Beispiel müssen wir eine Klasse zum Versenden von E-Mails ersetzen, die an 200 verschiedenen Stellen aufgerufen wird. Für eine leichtere Wartung erstellen wir einen Service anstelle des direkten Dateieinbindens. Wenn wir also zukünftig E-Mails via SMTP statt über PHP mail() versenden wollen, ändern wir einfach die Klasse des Services und müssen nicht 200 Stellen im Code anpassen.
Dependency Injection löst auch das Problem, einen Service in einer Klasse mehrfach zu holen. Wir müssen nicht mehrmals auf den Service Container zugreifen, wenn wir den Service in mehreren Methoden einer Klasse verwenden. Stattdessen speichern wir das Service-Objekt in einer Eigenschaft der Klasse und verwenden es dann über $this->serviceName.
Natürlich kann man auch ohne Service Container und Dependency Injection arbeiten, aber diese Patterns vereinheitlichen unseren Code, vereinfachen ihn und erleichtern die Wartung.
Wo finde ich den Namen eines Services?
In unserem Beispiel haben wir den Service "database":
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
// Hier fügen wir den Service-Namen hinzu.
$container->get('database'),
);
}
Wenn Sie aber einen Service aus einem contrib- oder custom-Modul hinzufügen, kann der Name so aussehen:
modulname.servicename
Sie können den Service-Namen in der Datei *.services.yml überprüfen. Der Service-Name beginnt nicht zwingend mit dem Modulnamen, aber normalerweise ist das so. Zum Beispiel:
/**
* {@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')
);
}