Zum Hauptinhalt springen

Devlog · Phase 0 · 5/7

Die Modul-Architektur des Web-Repos

Fünf Module, ein einziger Ausgangspunkt zur Cloud und Reviews, die sich am Dateipfad ablesen lassen: die Laravel-Architektur des Web-Repos, zu über neunzig Prozent von KI-Agenten generiert.

Veröffentlicht am 10 August 20264 Min. Lesezeit
  • laravel
  • architecture
  • octane

Eine Website ohne Datenbank ist ein paar Seiten und ein Formular. Die Versuchung, bei einem so kleinen Umfang, ist es, alles in zwei oder drei Controller zu stapeln und weiterzumachen. Ich habe von der ersten Projektwoche an den entgegengesetzten Weg gewählt. Als ich die Landingpage im April aufsetzte, habe ich jede Verantwortung in ihr eigenes Modul einsortiert. Heute zählt das Repo fünf Domänen: Cloud, Locale, Seo, Waitlist und nun Blog. Hier ist, warum diese Disziplin trägt, selbst in diesem Maßstab.

Ein Ordner pro Domäne, nicht pro technischem Typ

Die Ausgangsstruktur von Laravel ordnet den Code nach Art: alle Controller zusammen, alle Services zusammen, alle Hilfsklassen zusammen. Auf aubia.dev bin ich zu einer Ordnung nach Geschäftsdomäne gewechselt. Jeder Ordner von app/Modules/ trägt eine vollständige Produktabsicht, mit seinen Unterordnern Actions/, DataTransferObjects/, Http/ und Enums/, wo sie nützlich sind.

  • Cloud: der einzige HTTP-Ausgangspunkt zu api.aubia.cloud.
  • Locale: die Sprachauflösung vor dem Routing.
  • Seo: die Erzeugung der Sitemap und der Indexierungssignale.
  • Waitlist: der Anmelde-Proxy per Double-Opt-in.
  • Blog: die Flat-File-Engine, die diesen Artikel ausliefert.

Wenn eine Absicht sich ändert, weiß ich genau, wo ich nachsehen muss. Keine Datei spielt auf zwei Feldern.

Die animierte Karte der Module des Web-Repos: Locale, Seo, Waitlist und Blog laufen auf das Modul Cloud zu, den einzigen Ausgangspunkt zu api.aubia.cloud.

Die Actions: eine Absicht, eine Klasse

Jede Geschäftshandlung lebt in ihrer eigenen Action-Klasse. Kein Sammelsurium-Service, der über die Monate fünfzehn Methoden anhäuft: Eine Klasse tut eine Sache, stellt sie über eine Eintrittsmethode bereit und lässt sich außerhalb des HTTP-Stacks testen. Die Aubia-Konvention will diese Klassen standardmäßig final readonly, mit einem benannten (execute) oder aufrufbaren Eintrittspunkt.

Die Locale-Auflösung ist ein gutes Beispiel dafür. Sie spielt dieselbe Kaskade wie die Middleware durch, ohne das URL-Segment zu berühren, isoliert, um testbar zu bleiben:

public function __invoke(Request $request): string
{
    $candidates = [
        $request->cookie(SetLocale::COOKIE_NAME),
        $this->extractAcceptLanguage($request),
        config('app.locale'),
    ];

    foreach ($candidates as $candidate) {
        if (is_string($candidate) && in_array($candidate, SetLocale::SUPPORTED_LOCALES, true)) {
            return $candidate;
        }
    }

    return SetLocale::DEFAULT_LOCALE;
}

Der Controller, der diese Action aufruft, bleibt trivial: Er delegiert und leitet weiter. Die gesamte testbare Logik liegt anderswo.

Die readonly-DTOs: ein typisierter Vertrag zwischen den Schichten

Zwischen einer Schicht und der nächsten lasse ich niemals ein vage typisiertes assoziatives Array durchlaufen. Jede strukturierte Payload läuft über ein Data Transfer Object in reinem PHP: eine readonly class mit promoted properties, mit einem fromArray() am Eingang und einer expliziten Ausgabemethode. Keine externe Abhängigkeit dafür, kein magisches Mapping: Die strikte Typisierung von PHP 8.5 genügt, und der Vertrag liest sich auf einen Blick.

Die Warteliste-Anmeldung veranschaulicht, warum dieser Vertrag zählt. Das Formular erfasst ein zeitbasiertes Honeypot-Feld, startedAt, das nur dazu dient, Bots auf der Validierungsseite abzufangen. Dieses Feld darf niemals an die Cloud gehen. Das DTO trägt es intern, schließt es aber aus der ausgehenden Payload aus:

public function toCloudPayload(): array
{
    return [
        'email' => $this->email,
        'utm_source' => $this->utmSource,
        'utm_medium' => $this->utmMedium,
        'utm_campaign' => $this->utmCampaign,
        'locale' => $this->locale,
    ];
}

Die Grenze zwischen dem, was das Web kennt, und dem, was die Cloud empfängt, ist schwarz auf weiß geschrieben, in einer Methode, deren Name genau ihre Rolle sagt.

Ein einziger Ausgangspunkt zur Cloud

Da die Phase 0 lokal nichts speichert, geht jedes persistente Datum über einen HTTP-Proxy an das Cloud-Repo. Ich habe diese Aufrufe in einer einzigen Klasse zentralisiert, CloudApiClient, damit die Actions nie die Details des nativen Http-Clients von Laravel kennen müssen: Timeouts, Backoff, Logging ohne personenbezogene Daten. Der Client wendet zwei verschiedene Retry-Richtlinien je nach Idempotenz des Verbs an. Ein GET wird bei Netzwerkfehler und bei 5xx wiederholt, ohne Nebenwirkung. Ein signierter POST wird nur bei einem klaren Netzwerkausfall wiederholt, niemals bei einer empfangenen Antwort, um keine doppelte Anmeldung zu riskieren.

Dieser Client ist final, aber bewusst nicht auf Klassenebene readonly: Unter Octane muss er von einer Anfrage zur nächsten zustandslos bleiben und in den Tests mockbar bleiben. Ein Detail der Stateless-Disziplin, das Lecks zwischen Workern vermeidet.

Warum diese Disziplin in einem so kleinen Repo

Die ehrliche Frage lautet: Ist das nicht zu viel für fünf Seiten?

Dieses Repo ist zu über neunzig Prozent von KI-Agenten generiert, und die menschliche Review bleibt der eigentliche Engpass. Eine saubere Modulgrenze ist ein Geländer in beide Richtungen: Ein auf Waitlist losgelassener Agent läuft nicht versehentlich in Seo über, und meine Reviews bleiben kurz zu lesen, weil sich der Umfang einer Änderung an ihrem Dateipfad erahnen lässt.

Die Phase 1 wird eine echte Produkt-Website bringen, und jedes heutige Modul ist ein Fundament, kein wegwerfbares Gerüst. Und diese Struktur ist von einem Aubia-Repo zum anderen kohärent: Ich finde dieselben Reflexe im Desktop-Cockpit und in der Cloud wieder. Diese Regelmäßigkeit ist, in einem allein geführten Projekt, ein paar Ordner mehr durchaus wert.

Hinein in die Haut der Website

Bleibt das Sichtbarste: die Palette, das Glas und die Bewegung. Um die Einladung zur Beta 0.1 gleich bei ihrer Öffnung zu erhalten, ist die Warteliste da.

Dieses Projekt wird hier erzählt, Artikel für Artikel. Kommen Sie an Bord: Ihr Feedback zeichnet den weiteren Weg.

Auf die Warteliste