Fallback Image

Discovery-Based Architecture in PHP

Clean Integration mit PHP Attribut und Interface, erklärt anhand Webhook Event Handlers.

Je komplexer eine PHP-Anwendung wird, desto häufiger stellt sich die Frage: Wie leite ich dynamische Eingaben (z. B. Events, Kommandos, Typen) an die passende Logik weiter?


Typische Szenarien dafür sind: 

  • Webhooks
  • Command-Dispatching
  • Event-driven Systeme
  • Plugin- oder Modul-Architektur

 

Die meisten Entwickler beginnen mit if-/match-Blöcken. Das funktioniert – aber skaliert schlecht. In diesem Artikel zeige ich, wie du mit einem PHP-Attribut und einem Interface eine flexible, entkoppelte Struktur aufbaust, die auf automatischer Erkennung basiert – erklärt am Beispiel einer Webhook Client Integration.

💡 Achtung, jetzt wird’s techy!

Unser PHP-Entwickler zeigt dir, wie du Webhook-Events in PHP elegant und skalierbar verarbeitest - ein sehr technisches Thema. Perfekt für dich, wenn du Lust hast, deine Event-Logik von starren if-Statements zu befreien und smarte Discovery-Patterns kennenzulernen. 

Das Problem

Um eingehende Webhook-Requests anhand des beigefügten Events einem Event-Handler zu ordnen zu können, kann schnell auf If-Statements zurückgegriffen werden:

Eine einfachere Version dessen kann, seit PHP 8, auch mit einem match-Statement umgesetzt werden:

Doch das wird schnell unübersichtlich:

  • Neue Events vergrößern den Code
  • Wachsendes Risiko, Events zu übersehen
  • Zentrale Logik wird schwer wartbar
  • Erweiterungen durch Dritte sind fast unmöglich

Discovery-based Integration

Statt in if-Ketten oder langen match-Statements Events mit Event-Handlern zu verknüpfen, wollen wir:

1. Handler-Klassen mit einem Attribut versehen (z. B. #[Webhook('event.name')])
2. Ein gemeinsames Interface implementieren
3. Ein festgelegtes Verzeichnis bzw. Namespace rekursiv scannen
4. Den passenden Handler automatisch finden und aufrufen

Das Muster funktioniert nicht nur für Webhooks, sondern lässt sich auch übertragen auf:

  • CLI-Kommandos
  • Domain Events
  • Feature-Module
  • Plugin-Systeme

Schritt-für-Schritt: Umsetzung des Musters

1. Attribut definieren

Damit können Klassen mit einem bestimmten Event-Namen markiert werden. Diese Klassen sind dann für die Bearbeitung dieses Events zuständig.

2. Gemeinsames Interface erstellen

Damit stellen wir sicher, dass alle Handler die gleiche Signatur haben und generisch aufgerufen werden können.

3. Event-spezifische Handler-Klasse schreiben

Kommen wir zur eigentlich Event-Handler-Klasse, die ein Event bearbeitet. Hier ist ein Beispiel wie solch eine Klasse aussehen könnte:

Jede Klasse:

  • Ist auf ein Event fokussiert
  • Ist leicht testbar
  • Kann unabhängig entwickelt und deployed werden
  • Kann aus third-party packages reingeladen werden
    (bspw. indem entweder die Liste an namespaces für die automatic-discovery erweitertert werden – siehe nächstes Kapitel oder indem packages die Klassen im bisherigen namespace publishen)
  • Ohne Risiko die Registrierung zu vergessen oder das Event falsch zu verknüpfen

4. Service implementieren

Das Herzstück ist ein Service, der über Reflection passende Klassen findet:

5. Anwendung in einem Job oder Controller

Ob nur der erste gefundene Event-Handler oder alle gefundenen aufgerufen werden sollen, kann jeder Entwickler für sich entscheiden. In diesem Beispiel werden alle gefundenen Event-Handler ausgeführt.

Die Vorteile

Modular: Jeder Handler ist eine eigenständige Klasse

Skalierbar: Neue Events = neue Datei, keine Änderungen am globalen Aufruf nötig

Testbar: Handler lassen sich isoliert testen

Entkoppelt: Keine zentralisierte Routing-Logik

Dynamisch: Events und Handler lassen sich zur Laufzeit erfassen

Wiederverwendbarkeit

Du kannst dieses Prinzip für jede beliebige Dispatch-Logik verwenden:

 

  • CLI-Befehle (z. B. #[Command('sync:users')])
  • Domain Events (#[ListensTo(UserRegistered::class)])
  • Feature Flags
  • Hintergrundjobs
  • Plugin-Routing

Immer gilt: 

Kombiniere Attribute + Interface + Discovery = maximale Flexibilität

Erweiterbar

  • Caching für Reflection-Ergebnisse (getEventHandlers() → cache()->remember(...))
  • Unterstützung für Wildcards (invoice.*)
  • Mehrere Handler pro Event (Broadcasting)
  • Invocation via __invoke() statt handle()
  • Optional: Den Scan-Prozess in ein Artisan Command auslagern, das beim Deployment (z. B. via CI/CD) die Mapping-Datei erzeugt.
  • Falls du Laravel's Discovery erweitern willst, könntest du sogar ein Service Provider Pattern daraus machen.

Validierung

In dem Fall der Webhook Even-Handler habe ich beispielsweise eine Validierung anhand Laravels Validation-Klasse hinzugefügt, die vor der Bearbeitung aufgerufen werden kann und mitentscheidet, ob der Webhook-Request gespeichert bzw. bearbeitet werden soll. 


Als Erstes wird dem Interface eine validate methode definiert:

Statt direkt das Interface in einem spezifischen Event-Handler zu implementieren, vererbt jeder Event-Handler von einem abstrakten Event-Handler, der die validation method inkludiert:

Im spezifischen Event-Handler wird zusätzlich definiert welche Regeln der Inhalt des Payloads einhalten soll:

Die Vaildate method kann, wie hier gezeigt, bspw. direkt in der handler method verwendet werden oder dort wo eingehende Webhook-Requests ausgewertet werden – bspw. so:

So können bereits vor dem Speichern des Webhook-Requests – wie es im Webhook-Profile des Laravel Webhook Client Packages von Spatie vorgesehen ist – jene Requests herausgefiltert werden, deren Payload inhaltlich nicht validiert werden konnte.

Fazit

Das Zusammenspiel von PHP-Attributen, Interfaces und Discovery per Reflection bietet eine elegante Alternative zu klassischen if-/match-Konstrukten. Gerade in dynamischen Systemen wie Webhook-Integrationen, Plugins oder Event-Handling ist dieses Muster:

  • klar strukturiert
  • leicht wartbar
  • von Natur aus offen für Erweiterungen

Wann nicht empfehlenswert

  • High-Frequency-Usecases im Millisekundenbereich, bei denen Events in Echtzeit und in großer Menge verarbeitet werden müssen. In solchen Fällen können die Reflection-basierten Lookups (ohne Caching) zu Engpässen führen. Beispiel: Hochfrequente Telemetrie-Datenverarbeitung oder Trading-Engines.

 

  • Einfache, kleine Projekte, bei denen nur 2–3 Eventtypen existieren. In diesen Fällen ist die zusätzliche Komplexität durch Attribute, Interfaces und Discovery-Logik unnötig. Ein match-Statement ist hier vollkommen ausreichend.

 

  • Projekte ohne Composer-Autoload-Disziplin oder mit inkonsistenter Verzeichnisstruktur. Da die Discovery oft auf dem PSR-4-Autoload basiert, ist ein sauberer Namespace- und Klassenaufbau Voraussetzung für das Pattern.

Werde Teil unserer Laravel-Community

und entdecke unserer Meetups.

Zum Meetup