Verwendung der PHP-Konstruktor-Property-Promotion in benutzerdefinierten Drupal-Modulen
PHP 8 hat die Konstruktor-Property-Promotion eingeführt, eine Funktion, die die Definition und Zuweisung von Klassen-Eigenschaften vereinfacht, indem Sie Eigenschaften direkt in der Signatur des Konstruktors deklarieren und initialisieren können. In diesem Tutorial wird gezeigt, wie Sie die Konstruktor-Property-Promotion in benutzerdefinierten Drupal-Modulen (die PHP 8.0+ voraussetzen) einsetzen, insbesondere um die Dependency Injection in Ihren Services und Controllern zu vereinfachen. Wir vergleichen das traditionelle Drupal-Muster (verwendet mit PHP 7 und frühen Drupal 9-Versionen) mit dem modernen Ansatz ab PHP 8+, jeweils mit vollständigen Codebeispielen. Am Ende werden Sie sehen, wie diese moderne Syntax Boilerplate-Code reduziert, den Code klarer macht und sich an aktuellen Best Practices orientiert.
Drupal 10 (das PHP 8.1+ benötigt) nutzt diese modernen PHP-Features bereits im Core, daher werden Entwickler benutzerdefinierter Module ermutigt, dasselbe zu tun. Beginnen wir mit dem traditionellen Dependency-Injection-Muster in Drupal und refaktorieren es anschließend mit der Konstruktor-Property-Promotion.
Traditionelle Dependency Injection in Drupal (vor PHP 8)
In Drupal-Services und -Controllern besteht das traditionelle Muster für Dependency Injection aus drei Schritten:
-
Deklarieren Sie jede Abhängigkeit als Klassen-Eigenschaft (meistens
protected
) mit einem entsprechenden Docblock. -
Geben Sie für jede Abhängigkeit einen Type-Hint im Konstruktor-Parameter an und weisen Sie sie innerhalb des Konstruktors der Klassen-Eigenschaft zu.
-
Für Controller (und einige Plugin-Klassen) implementieren Sie eine statische
create(ContainerInterface $container)
-Methode, um Services aus Drupals Service Container abzurufen und die Klasse zu instanziieren.
Das führt zu ziemlich viel Boilerplate-Code. Betrachten wir zum Beispiel einen einfachen Custom-Service, der die Konfigurations-Factory und eine Logger-Factory benötigt. Traditionell würde der Code so aussehen:
Traditionelle Service-Klassen-Implementierung
<?php
namespace Drupal\example;
/**
* Beispielservice, der den Seitennamen protokolliert.
*/
class ExampleService {
/**
* Die Konfigurations-Factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Die Logger-Channel-Factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Konstruiert ein ExampleService-Objekt.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* Die Konfigurations-Factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* Die Logger-Channel-Factory.
*/
public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
// Gespeicherte Injected Services.
$this->configFactory = $config_factory;
$this->loggerFactory = $logger_factory;
}
/**
* Protokolliert den Seitennamen als Beispielaktion.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
Hier deklarieren wir die zwei Eigenschaften $configFactory
und $loggerFactory
und weisen sie im Konstruktor zu. Der zugehörige Service muss außerdem in der services YAML des Moduls definiert werden (mit den benötigten Services als Argumente), zum Beispiel:
# example.services.yml
services:
example.example_service:
class: Drupal\example\ExampleService
arguments:
- '@config.factory'
- '@logger.factory'
Wenn Drupal diesen Service instanziiert, werden die konfigurierten Argumente in der angegebenen Reihenfolge an den Konstruktor übergeben.
Traditionelle Controller-Klassen-Implementierung
Auch Controller in Drupal können Dependency Injection nutzen. Typischerweise erbt eine Controller-Klasse von ControllerBase
(für Komfortfunktionen) und implementiert Drupals Container-Injektion durch die Definition einer create()
-Methode. Die create()
-Methode ist eine Factory, die Services aus dem Container zieht und den Konstruktor aufruft. Zum Beispiel:
<?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 für Example-Routen.
*/
class ExampleController extends ControllerBase {
/**
* Die Entity-Type-Manager-Service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Der String-Translation-Service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* Konstruiert ein ExampleController-Objekt.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Der Entity-Type-Manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* Der 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 {
// Benötigte Services aus dem Container holen und an den Konstruktor übergeben.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Baut eine einfache Seitenantwort.
*/
public function build(): array {
// Beispielnutzung der injizierten Services.
$node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
return [
'#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
];
}
}
Verwendung der Konstruktor-Property-Promotion (PHP 8+) in Drupal
Konstruktor-Property-Promotion vereinfacht das oben genannte Muster, indem Sie Eigenschaften in einem Schritt direkt in der Signatur des Konstruktors deklarieren und zuweisen. In PHP 8 können Sie dem Konstruktorparameter eine Sichtbarkeit (und andere Modifizierer wie readonly
) voranstellen, und PHP erstellt und weist diese Eigenschaften automatisch zu. Das bedeutet, Sie müssen die Eigenschaft nicht mehr separat deklarieren oder die Zuweisung im Konstruktor schreiben – PHP übernimmt das für Sie.
Wichtig: Das ist reiner syntaktischer Zucker. Es ändert nichts an der Funktionsweise der Dependency Injection in Drupal; es reduziert lediglich den von Ihnen zu schreibenden Code. Sie registrieren Services weiterhin in YAML (oder lassen Drupal sie automatisch injizieren), und bei Controllern nutzen Sie weiterhin die create()
-Fabrikmethode (außer Sie registrieren den Controller als Service). Der Unterschied besteht nur in der Art, wie Sie die Klassen schreiben. Das Ergebnis: deutlich weniger Boilerplate, wie auch in einem Drupal Core-Issue gezeigt wurde, in dem Dutzende Zeilen Deklarationen und Zuweisungen auf wenige Zeilen im Konstruktor reduziert wurden.
Lassen Sie uns unsere Beispiele mit der Konstruktor-Property-Promotion refaktorieren.
Service-Klasse mit Konstruktor-Property-Promotion
So sieht die ExampleService
mit der neuen Syntax aus:
<?php
namespace Drupal\example;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Beispielservice, der den Seitennamen protokolliert (Property Promotion).
*/
class ExampleService {
/**
* Konstruiert ein ExampleService-Objekt mit injizierten Services.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* Die Konfigurations-Factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* Die Logger-Channel-Factory.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected LoggerChannelFactoryInterface $loggerFactory
) {
// Kein Body notwendig; Eigenschaften werden automatisch gesetzt.
}
/**
* Protokolliert den Seitennamen als Beispielaktion.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
Controller-Klasse mit Konstruktor-Property-Promotion
So sieht der ExampleController
nach der Umstellung aus:
<?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 für Example-Routen (mit Property Promotion).
*/
final class ExampleController extends ControllerBase {
/**
* Konstruiert ein ExampleController-Objekt.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Der Entity-Type-Manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* Der String-Translation-Service.
*/
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
private TranslationInterface $stringTranslation
) {
// Keine Zuweisungen notwendig; Eigenschaften werden automatisch gesetzt.
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Services aus dem Container an den Konstruktor übergeben.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Baut eine einfache Seitenantwort.
*/
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]),
];
}
}
Vorteile der Konstruktor-Property-Promotion
Die Konstruktor-Property-Promotion in Ihren Drupal-Klassen bringt mehrere Vorteile mit sich:
-
Weniger Boilerplate: Sie schreiben deutlich weniger Code. Die manuelle Deklaration und Zuweisung von Eigenschaften entfällt, was besonders bei mehreren Abhängigkeiten viele Zeilen spart. Ihre Module werden übersichtlicher und leichter wartbar.
-
Klarerer und prägnanter Code: Die Abhängigkeiten der Klasse sind sofort ersichtlich – direkt in der Signatur des Konstruktors – und nicht zwischen Eigenschaftsdeklarationen und Konstruktor-Body verstreut. Das erhöht die Lesbarkeit und macht sofort klar, welche Services die Klasse benötigt.
-
Weniger Doc Comments erforderlich: Da die Eigenschaften im Konstruktor typisiert deklariert werden, können Sie auf überflüssige
@var
- und@param
-Annotationen verzichten (sofern der Zweck durch Benennung offensichtlich ist). Der Code dokumentiert sich in großen Teilen selbst. Nicht offensichtliche Aspekte können weiterhin dokumentiert werden, aber die Wiederholung entfällt. -
Moderne PHP-Syntax: Die Nutzung der Property Promotion hält Ihren Code auf dem neuesten Stand der PHP-Entwicklung. Der Drupal-10-Core nutzt diese Syntax bereits für neuen Code, und die Verwendung in Custom-Modulen macht Ihren Code konsistenter mit Core und Community-Beispielen. Außerdem bereitet es Ihre Codebasis auf künftige Features vor (ab PHP 8.1 kann z.B. das
readonly
-Schlüsselwort für unveränderliche Abhängigkeiten genutzt werden).
Performance und Funktionalität bleiben identisch mit der klassischen Injektion – Property Promotion ist rein ein Sprach-Feature. Sie erhalten weiterhin voll typisierte Eigenschaften, die Sie in der gesamten Klasse verwenden können (z.B. $this->entityTypeManager
im Controller-Beispiel). Unter der Haube ist das Ergebnis das gleiche wie beim längeren Code, nur mit weniger Aufwand erreicht.
Fazit
Die Konstruktor-Property-Promotion ist ein einfaches, aber leistungsstarkes PHP-8-Feature, das Drupal-Entwickler nutzen können, um die Entwicklung benutzerdefinierter Module zu vereinfachen. Indem Sie Boilerplate eliminieren, können Sie sich auf die eigentliche Logik Ihrer Klasse konzentrieren, statt auf das Verbinden von Services. Wir haben gezeigt, wie Sie eine typische Drupal-Service-Klasse und Controller-Klasse auf die Verwendung von Property Promotion umstellen und mit dem klassischen Ansatz vergleichen. Das Ergebnis ist ein prägnanterer und wartungsfreundlicherer Code, ohne an Klarheit oder Funktionalität zu verlieren. Da Drupal künftig auf moderne PHP-Anforderungen setzt, hilft die Verwendung solcher Features in Ihren eigenen Modulen, Ihren Code sauber, klar und nach aktuellen Best Practices zu halten. Nutzen Sie die moderne Syntax, um Ihre Drupal-Entwicklung einfacher und eleganter zu machen.