Abhängigkeiten und Modulgrenzen automatisch prüfen
Architekturregeln, die nur in Dokumenten existieren, werden gebrochen. Mit PHPUnit, PHP-Arch und der Reflection API lassen sich Schichtengrenzen, Modulabhängigkeiten und Designregeln als ausführbare Tests formulieren – die sofort anschlagen, wenn eine Klasse die falsche Schicht aufruft oder ein Modul eine verbotene Abhängigkeit einführt.
Inhaltsverzeichnis
- 1. Warum Architektur ohne Tests irgendwann erodiert
- 2. Was Architekturtests leisten und was nicht
- 3. PHP-Arch: Schichtenregeln deklarativ formulieren
- 4. Reflection API: Abhängigkeiten zur Laufzeit inspizieren
- 5. Layer-Tests für Clean Architecture und DDD
- 6. Modulabhängigkeiten in Magento automatisch prüfen
- 7. Zyklische Abhängigkeiten erkennen und auflösen
- 8. Namenskonventionen und Klassenlokation prüfen
- 9. Werkzeuge für Architekturtests im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Architektur ohne Tests irgendwann erodiert
Jedes PHP-Projekt beginnt mit einer klaren Architekturidee: eine Service-Schicht, die keine Infrastruktur-Klassen kennt. Module, die nur über definierte Interfaces miteinander kommunizieren. Eine Domänenschicht, die keine HTTP-Requests oder Datenbankverbindungen importiert. Ohne automatische Überprüfung dieser Regeln entstehen schrittweise Abkürzungen: Eine Service-Klasse importiert direkt eine Repository-Implementierung. Ein Magento-Modul ruft Klassen eines anderen Moduls ohne Service Contract auf. Nach einem Jahr ist die Architektur noch als Diagram vorhanden, aber im Code längst nicht mehr erkennbar.
Architekturtests formalisieren diese Regeln als ausführbare PHPUnit-Tests. Wenn eine Klasse gegen eine Architektugregel verstößt, schlägt der Test in der CI-Pipeline fehl – genau wie ein funktionaler Test, der eine falsche Berechnung aufdeckt. Der entscheidende Unterschied zu Code-Reviews ist die Kontinuität: Architekturtests laufen bei jedem Commit, nicht nur wenn ein erfahrener Entwickler gerade Zeit hat. Sie machen implizites Architekturwissen explizit und maschinenprüfbar.
2. Was Architekturtests leisten und was nicht
Architekturtests prüfen strukturelle Eigenschaften des Codes: Welche Klassen importieren welche anderen Klassen? Liegen Klassen im richtigen Namespace? Implementieren alle Klassen in einem bestimmten Verzeichnis das erwartete Interface? Halten sich alle Service-Klassen an die Namenskonvention *Service? Diese Fragen lassen sich statisch oder zur Laufzeit per Reflection beantworten, ohne dass der Code ausgeführt wird.
Was Architekturtests nicht leisten: Sie prüfen keine funktionale Korrektheit und keinen Geschäftslogik. Eine Klasse kann alle Architekturregeln einhalten und trotzdem falsch rechnen. Architekturtests sind eine ergänzende Sicherheitsschicht neben Unit- und Integration-Tests – keine Alternative. Das PHPUnit-Pattern für Architekturtests ist eine separate Testsuite, die in der CI-Pipeline nach den Unit-Tests läuft und bei Architekturverstößen den Build bricht. Sie laufen typischerweise langsamer als Unit-Tests, weil sie viele Klassen inspizieren, müssen aber nicht so häufig laufen.
3. PHP-Arch: Schichtenregeln deklarativ formulieren
PHP-Arch (via Composer phpat/phpat) ist das mächtigste Werkzeug für Architekturtests in PHP. Es erlaubt, Regeln in einer deklarativen Syntax zu formulieren: Klassen in Namespace A dürfen keine Klassen in Namespace B importieren. Die Regeln werden als reguläre PHPUnit-Testklasse geschrieben, die von PHPAr\Test\ArchitectureTest erbt. PHP-Arch liest die PHP-Dateien statisch und prüft die Imports ohne Code auszuführen – das macht es sehr schnell auch für große Codebases.
Ein konkretes Beispiel für ein Magento-Projekt: Die Domain-Schicht eines Moduls darf keine Magento-Framework-Klassen direkt importieren. Die ViewModel-Schicht darf nicht die Repository-Implementierungen importieren, sondern nur die Interfaces. Diese Regeln lassen sich in zehn Zeilen PHP-Arch-Code ausdrücken und werden für jede neue Klasse automatisch geprüft. Das ist das Architekturtests-Pattern: Regeln einmal definieren, nie wieder manuell prüfen.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Test\Architecture;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;
/**
* Architecture rules for the Catalog module.
* Ensures layer boundaries are respected automatically.
*/
final class CatalogArchitectureTest
{
/**
* Domain classes must not depend on infrastructure or Magento framework.
*/
public function testDomainDoesNotDependOnInfrastructure(): Rule
{
return PHPat::rule()
->classes(Selector::inNamespace('Mironsoft\Catalog\Domain'))
->shouldNotDependOn()
->classes(
Selector::inNamespace('Mironsoft\Catalog\Infrastructure'),
Selector::inNamespace('Magento\Framework\App'),
Selector::inNamespace('Magento\Framework\DB'),
)
->because('Domain layer must be framework-agnostic and infrastructure-free');
}
/**
* ViewModels may only use service contracts, not concrete implementations.
*/
public function testViewModelsUseOnlyContracts(): Rule
{
return PHPAt::rule()
->classes(Selector::inNamespace('Mironsoft\Catalog\ViewModel'))
->shouldNotDependOn()
->classes(Selector::inNamespace('Mironsoft\Catalog\Model\ResourceModel'))
->because('ViewModels must depend on Api contracts, not ResourceModel implementations');
}
/**
* All repository implementations must implement the corresponding Api interface.
*/
public function testRepositoriesImplementContracts(): Rule
{
return PHPAt::rule()
->classes(
Selector::inNamespace('Mironsoft\Catalog\Model'),
Selector::classNameMatches('/Repository$/')
)
->shouldImplement()
->classes(Selector::inNamespace('Mironsoft\Catalog\Api'))
->because('Repositories must always expose a service contract interface');
}
}
4. Reflection API: Abhängigkeiten zur Laufzeit inspizieren
Ohne PHP-Arch lassen sich Architekturtests direkt mit PHPUnit und der PHP-Reflection API schreiben. Die Reflection API erlaubt, Klassen, ihre Interfaces, Elternklassen, Konstruktorparameter und Attribute zur Laufzeit zu inspizieren. Das PHPUnit-Pattern für eigenständige Architekturtests: Eine abstrakte Basisklasse liefert Hilfsmethoden wie getClassesInNamespace(), assertClassImplements() und assertNoDirectDependency(). Konkrete Testklassen erben und formulieren die domänenspezifischen Regeln in lesbarem PHPUnit-Code.
Ein praktisches Beispiel: Alle Klassen im ViewModel-Namespace sollen das ArgumentInterface implementieren. Man iteriert per glob() oder Symfony Finder über alle PHP-Dateien im Verzeichnis, lädt die Klassen per require und prüft mit ReflectionClass::implementsInterface(). Das schlägt fehl, sobald jemand ein neues ViewModel ohne das Interface erstellt. Diese Art von Architekturtests ist einfach zu schreiben und benötigt keine externe Abhängigkeit.
5. Layer-Tests für Clean Architecture und DDD
Clean Architecture definiert klare Abhängigkeitsregeln: Abhängigkeiten zeigen immer von außen nach innen. Infrastruktur kennt Anwendungsschicht, Anwendungsschicht kennt Domäne, Domäne kennt niemanden außer sich selbst. Diese Regeln können verletzt werden, ohne dass der Code fehlschlägt – der Compiler oder der PHP-Interpreter prüft sie nicht. Nur ein Architekturtest macht diese Verletzung sichtbar und verhindert, dass sie in die Codebase einfließt.
Das PHPUnit-Pattern für Layer-Tests: Man definiert für jede Schicht einen Namespace-Pfad und schreibt eine Testmethode, die prüft, dass keine Use-Statements aus verbotenen Namespaces existieren. Das ist einfacher als es klingt: Tokenizierer oder statische Analyse per token_get_all(file_get_contents($file)) liefern alle use-Statements einer Datei, ohne die Klasse laden zu müssen. Wenn eines der Use-Statements in einen verbotenen Namespace zeigt, schlägt der Test fehl und gibt den Dateinamen aus.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Test\Architecture;
use PHPUnit\Framework\TestCase;
/**
* Layer boundary tests using PHP tokenizer — no external dependencies.
* Ensures Domain layer remains framework-agnostic.
*/
final class LayerBoundaryTest extends TestCase
{
private const DOMAIN_PATH = __DIR__ . '/../../../Domain';
private const FORBIDDEN_IN_DOMAIN = [
'Magento\\Framework',
'Magento\\Catalog',
'Mironsoft\\Catalog\\Infrastructure',
'Mironsoft\\Catalog\\Model\\ResourceModel',
];
public function testDomainLayerHasNoForbiddenImports(): void
{
$violations = [];
foreach ($this->findPhpFiles(self::DOMAIN_PATH) as $file) {
$tokens = token_get_all(file_get_contents($file));
foreach ($this->extractUseStatements($tokens) as $use) {
foreach (self::FORBIDDEN_IN_DOMAIN as $forbidden) {
if (str_starts_with($use, $forbidden)) {
$violations[] = sprintf(
'%s imports forbidden %s',
basename($file),
$use
);
}
}
}
}
self::assertEmpty(
$violations,
"Domain layer boundary violations:\n" . implode("\n", $violations)
);
}
/** @return string[] */
private function findPhpFiles(string $dir): array
{
return glob($dir . '/**/*.php') ?: [];
}
/** @return string[] */
private function extractUseStatements(array $tokens): array
{
$uses = [];
$inUse = false;
$current = '';
foreach ($tokens as $token) {
if (is_array($token) && $token[0] === T_USE) { $inUse = true; $current = ''; continue; }
if ($inUse && is_array($token) && in_array($token[0], [T_STRING, T_NS_SEPARATOR], true)) {
$current .= $token[1];
} elseif ($inUse && $token === ';') {
$uses[] = $current;
$inUse = false;
}
}
return $uses;
}
}
6. Modulabhängigkeiten in Magento automatisch prüfen
In Magento definiert die module.xml-Datei die deklarierten Abhängigkeiten eines Moduls. Ein häufiges Problem: Der Code des Moduls referenziert Klassen aus anderen Modulen, ohne dass diese Abhängigkeit in module.xml deklariert ist. Das führt zu Installationsproblemen, die erst beim Kunden auftreten. Ein Architekturtest liest alle Use-Statements der PHP-Dateien eines Moduls, extrahiert die referenzierten Vendor-Namespaces und vergleicht sie mit den in module.xml deklarierten Abhängigkeiten.
Das PHPUnit-Pattern für Magento-Modulabhängigkeiten: Eine Testklasse liest die module.xml mit SimpleXML, extrahiert alle <module name="...">-Einträge und vergleicht sie mit den tatsächlich referenzierten Modul-Namespaces aus dem Code. Undeklararierte Abhängigkeiten werden als PHPUnit-Fehler gemeldet. Dieser Test läuft in der CI-Pipeline und verhindert, dass Magento-Module deployed werden, die zur Laufzeit Klassen aus nicht deklarierten Modulen benötigen.
7. Zyklische Abhängigkeiten erkennen und auflösen
Zyklische Abhängigkeiten zwischen PHP-Klassen oder Modulen sind einer der häufigsten Architekturverstöße in gewachsenen PHP-Projekten. Modul A importiert Klasse aus Modul B, Modul B importiert Klasse aus Modul A. Der Code funktioniert, solange PHP den Klassenlader in der richtigen Reihenfolge aufruft – aber es macht das System schwer testbar, weil jede Testinstanziierung den gesamten Zyklus laden muss. Refaktorisierungen werden risikobehaftet, weil eine Änderung in einem Modul unerwartete Auswirkungen auf das andere hat.
Das PHPUnit-Pattern für Zykluserkennung: Man baut einen gerichteten Graphen der Abhängigkeiten und prüft auf Zyklen per Tiefensuche (DFS). In PHPUnit lässt sich das als Test schreiben, der alle PHP-Dateien eines Projekts einliest, den Abhängigkeitsgraph aufbaut und bei Zyklen mit vollständigem Pfad fehlschlägt: Cycle detected: ModuleA → ModuleB → ModuleC → ModuleA. Diese Ausgabe zeigt sofort, wo die Abhängigkeit gebrochen werden muss. Interfaces als Brücke zwischen Modulen sind die häufigste Lösung.
8. Namenskonventionen und Klassenlokation prüfen
Naming-Konventionen und Klassenlokation sind in PHP-Projekten oft nur implizit bekannt. Magento definiert: Repository-Implementierungen gehören in Model/, Interfaces in Api/, ViewModels in ViewModel/. Ein Test, der alle Klassen mit dem Suffix Repository außerhalb von Model/ und Api/ meldet, verhindert, dass neue Entwickler Klassen am falschen Ort anlegen. Dasselbe gilt für das Suffix Interface: Alle Interfaces sollen im Api/-Namespace liegen.
Das PHPUnit-Pattern für Naming-Tests: Man iteriert per Glob über alle PHP-Dateien, extrahiert den Klassenname und Namespace per Tokenizer und prüft die Übereinstimmung zwischen Dateiinhalt und Dateiname. Zusätzlich kann man prüfen, ob der vollständige Namespace dem Dateipfad entspricht – ein häufiger Fehler bei manuell erstellten Dateien. Diese Tests sind minimal aufwändig zu schreiben, aber sie decken eine ganze Kategorie von Fehlern ab, die erst zur Laufzeit durch den Autoloader sichtbar würden.
9. Werkzeuge für Architekturtests im Vergleich
Für Architekturtests in PHP stehen verschiedene Ansätze mit unterschiedlichen Stärken zur Verfügung. Die Wahl des richtigen Werkzeugs hängt vom Projekt ab: PHP-Arch für deklarative Regeln ohne viel Code, PHPStan-Regeln für statische Analyse, eigene PHPUnit-Tests für domänenspezifische Checks.
| Werkzeug | Stärke | Schwäche | Einsatz |
|---|---|---|---|
| PHP-Arch (phpat) | Deklarativ, schnell, lesbar | Nur Import-Abhängigkeiten | Schichtenregeln, Namespace-Isolation |
| PHPUnit + Reflection | Sehr flexibel, domänenspezifisch | Mehr Boilerplate-Code | Interface-Checks, Namenskonventionen |
| PHPStan Custom Rules | Tiefe statische Analyse | Steile Lernkurve | Typfehler, fehlerhafte Aufrufe |
| PHPUnit + Tokenizer | Keine Extra-Abhängigkeit | Manuell, fehleranfällig | Layer-Imports, Zyklus-Erkennung |
| Deptrac | Visualisierung, YAML-Konfiguration | Kein PHPUnit-Reporting | Große Projekte, Team-Überblick |
Für Magento-Projekte hat sich eine Kombination bewährt: PHP-Arch für Namespace-Isolationsregeln (welches Modul darf welches aufrufen), eigene PHPUnit-Tests mit Reflection für Magento-spezifische Konventionen (ViewModels implementieren ArgumentInterface, Repositories implementieren ihr Api-Interface) und PHPStan Level 8+ für statische Typsicherheit. Diese drei Ebenen decken verschiedene Klassen von Architekturerosion ab.
Mironsoft
Architekturberatung, PHPUnit-Strategie und Clean Architecture für PHP und Magento
Architekturregeln, die automatisch eingehalten werden?
Wir implementieren Architekturtests für PHP- und Magento-Projekte: Schichtenregeln mit PHP-Arch, Modul-Dependency-Tests und Naming-Convention-Checks – alles als ausführbare PHPUnit-Tests in der CI-Pipeline.
Architektur-Audit
Bestehende Abhängigkeiten analysieren, Schichtverletzungen und Zyklen identifizieren
Tests implementieren
PHP-Arch-Regeln, Reflection-Tests und Magento-Modul-Checks einführen
CI-Integration
Architekturtests als separaten CI-Job einrichten mit klaren Fehlermeldungen
10. Zusammenfassung
Architekturtests mit PHPUnit machen implizites Architekturwissen explizit und maschinenprüfbar. PHP-Arch formuliert Schichtenregeln in deklarativer Syntax ohne viel Boilerplate. Die Reflection API ermöglicht domänenspezifische Checks wie Interface-Überprüfungen und Namenskonventionen. Der PHP-Tokenizer erlaubt Layer-Grenz-Tests ohne externe Abhängigkeiten. Zyklus-Erkennung per DFS-Graph findet zirkuläre Abhängigkeiten, bevor sie Refaktorisierungen blockieren. Für Magento-Projekte ist die Prüfung undeklarierten Modul-Abhängigkeiten besonders wertvoll.
Der wichtigste Schritt ist nicht das Werkzeug, sondern das Formalisieren der Architekturregeln. Teams, die ihre Architekturregeln nur mündlich kommunizieren oder in Dokumenten festhalten, werden erleben, dass sie nach einem Jahr nicht mehr gelten. Teams, die dieselben Regeln als PHPUnit-Tests formulieren, haben sie dauerhaft in ihren Entwicklungsprozess integriert – ohne extra Aufwand für jedes Code-Review.
Architekturtests mit PHPUnit — Das Wichtigste auf einen Blick
PHP-Arch
Deklarative Schichtenregeln in PHPUnit-Testklassen. Klassen in Namespace A dürfen nicht Klassen in Namespace B importieren. Ohne Codeausführung, sehr schnell.
Reflection API
Interface-Checks, Klassenlokation und Naming-Konventionen zur Laufzeit prüfen. Keine externe Abhängigkeit nötig. Sehr flexibel für domänenspezifische Regeln.
Magento-Spezifika
Undeklararierte Modul-Abhängigkeiten prüfen, ViewModels auf ArgumentInterface, Repositories auf Api-Interface. module.xml gegen tatsächliche Importe vergleichen.
Zyklus-Erkennung
Abhängigkeitsgraph per DFS auf Zyklen prüfen. Vollständigen Zyklus-Pfad im Fehlerfall ausgeben. Interfaces als Brücke zur Zyklusauflösung.