@test
assert
PHPUnit · Architektur · PHP-Arch · Magento
Architekturtests mit PHPUnit
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.

18 Min. Lesezeit PHP-Arch · Reflection · Modulgrenzen · Layer-Tests PHPUnit 10/11 · PHP 8.2+ · Magento 2

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.

11. FAQ: Architekturtests mit PHPUnit

1Was sind Architekturtests?
PHPUnit-Tests, die strukturelle Code-Eigenschaften prüfen: Namespace-Abhängigkeiten, Interfaces, Klassenlokation. Machen implizite Architekturregeln maschinenprüfbar.
2Was ist PHP-Arch?
Composer-Bibliothek für deklarative Architekturtests. Regeln als PHPUnit-Testmethoden. Liest PHP-Dateien statisch ohne Codeausführung. Sehr schnell auch für große Codebases.
3Architekturtests ohne externe Bibliothek?
Ja. Reflection API und PHP-Tokenizer reichen. ReflectionClass prüft Interfaces. token_get_all() extrahiert Use-Statements ohne Klassenladung.
4Zyklische Abhängigkeiten erkennen?
Abhängigkeitsgraph aufbauen, Tiefensuche auf Zyklen. PHPUnit schlägt mit vollständigem Zyklus-Pfad fehl. Interfaces als Lösung zur Zyklusauflösung.
5Magento-Modulabhängigkeiten prüfen?
module.xml lesen, deklarierte Abhängigkeiten extrahieren. PHP-Dateien auf referenzierte Namespaces prüfen. Undeklararierte Abhängigkeiten als PHPUnit-Fehler melden.
6Wo laufen Architekturtests in CI?
Als eigener CI-Job nach den Unit-Tests. Langsamer, müssen nicht bei Pre-Commit laufen. PR-gebundener Lauf ist ausreichend.
7Unterschied Architekturtests vs. PHPStan?
PHPStan prüft Typkorrektheit. Architekturtests prüfen Schichtenregeln, Abhängigkeiten, Naming. Ergänzen sich – beide in der CI-Pipeline verwenden.
8Schichtenregeln in PHP-Arch formulieren?
PHPAt::rule()->classes(Selector::inNamespace('A'))->shouldNotDependOn()->classes(Selector::inNamespace('B')). Rückgabewert ist Rule-Objekt, das PHP-Arch auswertet.
9Welche Namenskonventionen prüfbar?
Alle. Suffix-Regeln, Interface-Implementierung, Namespace-Lokation. Tokenizer extrahiert Klassennamen, Reflection prüft Interfaces und Elternklassen.
10Ersetzen Architekturtests Code-Reviews?
Für geprüfte Regeln ja. Kein Reviewer muss mehr manuelle Checks für Namenskonventionen und Abhängigkeiten machen. Gibt Reviewern Zeit für inhaltliche Aspekte.