Drupal-Modulentwicklung: ein Leitfaden für Enterprise-Projekte
Wissen
Die häufigste Fehlentscheidung in Enterprise-Drupal-Projekten ist, ein Custom-Modul für ein Problem zu schreiben, das ein Contrib-Modul bereits gelöst hat. Bevor Sie eine Zeile Code schreiben, suchen Sie auf drupal.org. Auch wenn das bestehende Contrib-Modul nicht zu 100 % passt, ist es fast immer günstiger, es zu konfigurieren oder über einen schlanken Adapter zu erweitern, als ein paralleles Modul zu pflegen.
Dieser Artikel ist das Playbook, das wir bei jedem Drupal-Engagement bei Softescu anwenden: wann ein Custom-Modul sinnvoll ist, wie es in modernem Drupal 10/11 strukturiert wird, wie man es mit Services, Konfiguration und Tests sauber verdrahtet, und welche Performance-Fallen man vermeiden sollte.
Wann ein Custom-Modul die richtige Wahl ist — und wann nicht
Ein Custom-Modul ist die richtige Antwort, wenn:
- Die Geschäftslogik wirklich domänenspezifisch ist und nicht in eine generische Lösung gehört — Versicherungstarif-Berechnung, regulatorisches Reporting, branchenspezifische Workflows.
- Ein bestehendes Contrib-Modul fundamentale Architekturentscheidungen anders trifft, als das Projekt es benötigt.
- Eine Integration mit einem internen System gebraucht wird, das nie öffentlich wird.
Anti-Pattern: ein Custom-Modul für "Telefonnummer-Feldvalidierung", wenn telephone plus field_validation das Problem bereits abdecken. Wir haben Projekte übernommen, in denen drei Entwickler-Generationen jeweils ein neues Custom-Modul für denselben Anwendungsfall geschrieben haben — keines davon getestet. Suchen, bevor man schreibt.
Modulstruktur in modernem Drupal (10/11)
Ein modernes Drupal-Modul besteht aus klar getrennten Schichten, die per info.yml deklariert und über den Service-Container verdrahtet werden. Die Mindeststruktur:
my_module/
my_module.info.yml # Modul-Metadaten
my_module.module # Hooks (so wenig wie möglich)
my_module.services.yml # Service-Definitionen
my_module.routing.yml # Routen
my_module.permissions.yml # Berechtigungen
src/
Controller/ # Route-Handler
Service/ # Geschäftslogik
Plugin/ # Plugins (Block, Field, ...)
Form/ # Formulare
EventSubscriber/ # Event-Listener
config/
install/ # Defaults bei Installation
schema/ # Konfigurationsschema
tests/
src/
Unit/ # PHPUnit
Kernel/ # Kernel-Tests
Functional/ # Browser-Tests
Die info.yml deklariert Abhängigkeiten explizit:
name: 'My Module'
type: module
description: 'Domain-specific business logic for ...'
package: 'Custom'
core_version_requirement: ^10 || ^11
dependencies:
- drupal:node
- drupal:user
- paragraphs:paragraphs
Anti-Pattern: alles in die .module-Datei zu stopfen. Die .module-Datei ist heute fast nur noch ein Container für Hook-Deklarationen. Geschäftslogik gehört in Services unter src/Service/.
Hooks, Services, Dependency Injection — der moderne Weg
Drupal 10/11 ist ein Symfony-basiertes Framework. Hooks bleiben der Vertragspunkt zwischen Core und Modulen, aber sie sollten so dünn wie möglich sein und sofort an einen Service delegieren:
// my_module.module
function my_module_node_presave(NodeInterface $node) {
\Drupal::service('my_module.node_normalizer')->normalize($node);
}
Der Service selbst ist eine einfache Klasse mit dependency-injected Abhängigkeiten:
# my_module.services.yml
services:
my_module.node_normalizer:
class: Drupal\my_module\Service\NodeNormalizer
arguments:
- '@entity_type.manager'
- '@logger.channel.my_module'
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;
final class NodeNormalizer {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly LoggerInterface $logger,
) {}
public function normalize(NodeInterface $node): void { ... }
}
Anti-Pattern: \Drupal::service(...) mitten in der Geschäftslogik aufrufen. Das ist das Service-Locator-Pattern — es macht Tests unmöglich und verschleiert Abhängigkeiten. Constructor-Injection ist 2026 die einzig vertretbare Form.
Logger-Channels (logger.channel.my_module) müssen explizit als Service deklariert werden, damit drush watchdog:show --type=my_module funktioniert.
Konfigurationsmanagement — config/install vs. config/schema
Drupals Configuration Management ist eine der unterschätzten Stärken des Frameworks — und gleichzeitig die häufigste Quelle für Migrations-Probleme zwischen Umgebungen. Zwei Verzeichnisse sind entscheidend:
config/install/ enthält Default-Konfiguration, die bei drush en my_module einmalig in die aktive Konfiguration kopiert wird. Diese Dateien werden danach nie wieder gelesen — sie sind Installations-Defaults, keine fortlaufende Quelle der Wahrheit.
config/schema/ enthält das Schema (*.schema.yml), das die Struktur Ihrer Konfiguration beschreibt. Ohne Schema kann Drupal Konfiguration nicht serialisieren, nicht validieren und nicht über die Configuration-API exportieren. Wir haben Projekte übernommen, in denen Custom-Konfiguration jahrelang ohne Schema lief und still beim ersten drush config:export zerbrach.
# config/schema/my_module.schema.yml
my_module.settings:
type: config_object
label: 'My Module settings'
mapping:
api_endpoint:
type: string
label: 'API endpoint URL'
timeout:
type: integer
label: 'Timeout in seconds'
Anti-Pattern: Konfigurationsänderungen direkt in config/install/ machen und erwarten, dass sie in bestehende Sites einfließen. Das passiert nicht — bestehende Konfiguration muss per hook_update_N migriert werden.
Tests — PHPUnit, Kernel-Tests, Functional-Tests
Drupal hat drei Test-Schichten:
- Unit-Tests (
tests/src/Unit/) — schnell, kein Drupal-Bootstrap. Geeignet für reine Wertberechnung, Validierung, Format-Konvertierung. Mocks für alle Drupal-Services. - Kernel-Tests (
tests/src/Kernel/) — schnell genug (~Sekunden), bootet den Drupal-Kernel ohne Web-Stack. Geeignet für Service-Logik, Entity-Operationen, Hook-Reaktionen. - Functional-Tests (
tests/src/Functional/) — langsam, vollständiger HTTP-Stack. Nur für Endpunkt-Verhalten und Browser-Workflows.
Faustregel: jede neue Service-Klasse bekommt einen Kernel-Test mit den drei wichtigsten Pfaden (Happy Path, Edge Case, Error Case). Functional-Tests reservieren Sie für Submit-Flows und Berechtigungs-Tests.
namespace Drupal\Tests\my_module\Kernel;
use Drupal\KernelTests\KernelTestBase;
final class NodeNormalizerTest extends KernelTestBase {
protected static $modules = ['node', 'user', 'my_module'];
public function testNormalizeStripsTrailingWhitespace(): void {
$node = Node::create(['type' => 'page', 'title' => 'Test ']);
$this->container->get('my_module.node_normalizer')->normalize($node);
$this->assertSame('Test', $node->getTitle());
}
}
Anti-Pattern: nur den Happy Path testen. Edge Cases sind das, was nachts um 3 Uhr in Produktion ausfällt.
Performance und häufige Fallstricke
Drei wiederkehrende Performance-Probleme in Custom-Modulen, die wir bei Enterprise-Kunden regelmäßig sehen:
- N+1-Queries in
hook_entity_load— jeder geladene Node löst eine zusätzliche Query aus. Ein Listing mit 50 Nodes wird zu 51+ Queries. Lösung: Batch-Load mitentity_load_multipleund ein Per-Request-Cache. - Container-Rebuild bei jedem Request — eine falsch geschriebene
services.yml(z. B.factory:auf einem statebehafteten Singleton) zwingt Drupal, den Container bei jedem Request neu zu bauen. Symptom: TTFB ~500 ms statt ~50 ms. Lösung:drush cache:rebuildund Container-Compilation prüfen. - Fehlende Cache-Invalidierung — Custom-Blocks und -Controller, die Entity-Daten rendern, ohne
cacheTagszu setzen, werden bei Inhalts-Updates nicht invalidiert. Symptom: veralteter Inhalt im anonymen Cache. Lösung: jedes Render-Array bekommt explizite#cache.tags,#cache.contextsund#cache.max-age.
Anti-Pattern: das Cache-System abschalten, um diese Probleme zu umgehen. Das verlagert die Performance-Schulden nur, statt sie zu lösen.
Wann ein Modul als Contrib veröffentlicht werden sollte
Wenn Ihr Custom-Modul über mehrere Projekte hinweg dasselbe Problem löst, ist es Kandidat für die Veröffentlichung auf drupal.org. Wir haben über die Jahre mehrere Module von Softescu in die Contrib-Community gebracht. Der Prozess lohnt sich, weil er erzwingt, was guten Code ohnehin auszeichnet: explizite Konfiguration statt hartcodierter Annahmen, Tests für jeden nicht-trivialen Pfad, dokumentierte Hooks für Erweiterungen.
Prüfen vor dem Upstreaming:
- Gibt es bereits ein vergleichbares Modul auf drupal.org? Wenn ja, ist Co-Maintainership meist besser als ein Konkurrenz-Fork.
- Ist die Konfiguration ausreichend abstrahiert, dass Sie ihr Schema dokumentieren können?
- Sind alle Klassen-Namespaces, Service-IDs und Konfigurations-Keys generisch genug, dass sie nicht den Namen Ihres Kunden enthalten?
Wie wir das bei Softescu handhaben
In den letzten zehn Jahren haben wir Drupal-Module für Enterprise-Plattformen in den Bereichen Versicherung, Behörden und Bildung gebaut. Unsere Position: Custom-Module sind eine teure Investition — schreiben Sie sie nur, wenn die Geschäftslogik wirklich domänenspezifisch ist, testen Sie sie, und planen Sie das Upstreaming, wenn das Pattern wiederverwendbar ist. Unser ergänzender Artikel zu Enterprise-Drupal-Support behandelt die langfristige Wartungsseite derselben Gleichung.
Wenn Ihr Team Unterstützung bei der Drupal-Modulentwicklung sucht — Architektur-Reviews, dedizierte Entwickler oder ein vollständiges Drupal-Enterprise-Engagement — sprechen Sie uns an.