Basisklassen, Marker-Interfaces und Schichtenarchitektur in PHP 8.4
Wer in wachsenden PHP-Projekten weiterhin mit generischen Exceptions oder wahllos vielen Einzelklassen arbeitet, verliert schnell den Überblick zwischen fachlichen und technischen Fehlern. Dieser Artikel zeigt, wie eigene Exception-Hierarchien mit abstrakten Basisklassen, Marker-Interfaces und typisierten Kontextdaten entstehen, die über Domain-, Application- und Infrastructure-Schichten hinweg tragen.
Inhaltsverzeichnis
- 1. Warum eine flache Exception-Struktur bei wachsenden Projekten scheitert
- 2. Fachliche vs. technische Fehler: die zentrale Trennlinie für Exception-Hierarchien
- 3. Eine abstrakte Basisklasse für die eigene Exception-Hierarchie entwerfen
- 4. Marker-Interfaces: Exceptions kategorisieren ohne Vererbungszwang
- 5. Strukturierte Kontextdaten mit Constructor Property Promotion
- 6. Schichtenarchitektur: Domain-, Application- und Infrastructure-Exceptions abgrenzen
- 7. Namenskonventionen und Granularität: wann eine neue Exception-Klasse sinnvoll ist
- 8. Exception-Übersetzung an Schichtgrenzen: technische in fachliche Fehler umwandeln
- 9. Zusammenspiel mit globalem Error-Handling und Logging an der Systemgrenze
- 10. Zusammenfassung
- 11. FAQ
1. Warum eine flache Exception-Struktur bei wachsenden Projekten scheitert
In den ersten Wochen eines Projekts reicht fast jede Fehlerbehandlung aus: ein paar SPL-Klassen, vielleicht eine einzige eigene Exception-Klasse für alles, was schiefgehen kann, und ein zentrales catch (Throwable $e) am Rand der Anwendung. Das funktioniert, solange die Zahl der Fehlerfälle klein und die Reaktion darauf immer gleich ist. Sobald ein Projekt wächst, wächst aber auch die Zahl der Stellen, an denen unterschiedlich auf Fehler reagiert werden muss, und genau hier beginnt eine flache Struktur zu scheitern.
Das typische Symptom ist ein Aufrufer, der per catch (Exception $e) fängt und dann anhand von $e->getMessage() oder str_contains() entscheidet, was eigentlich passiert ist. Ein Nutzer soll eine verständliche Meldung sehen, ein Entwickler braucht einen Stacktrace, das Monitoring braucht eine eindeutige Kategorie für Alerting, und alle drei Bedürfnisse hängen an derselben, fragilen Zeichenkette. Ohne eine durchdachte eigene Exception-Struktur wird jede Änderung an einer Fehlermeldung zu einem stillen Bruch an einer völlig anderen Stelle im Code.
Eigene Exception-Hierarchien lösen dieses Problem nicht durch mehr Klassen an sich, sondern durch eine bewusste Struktur: Klassen, die etwas über die Art des Fehlers aussagen, statt nur über seinen Text. Die folgenden Abschnitte bauen genau diese Struktur Schritt für Schritt auf, von der Trennung fachlicher und technischer Fehler bis zur Schichtenarchitektur im großen System.
2. Fachliche vs. technische Fehler: die zentrale Trennlinie für Exception-Hierarchien
Die wichtigste Entscheidung beim Entwurf eigener Exception-Hierarchien fällt, bevor die erste Klasse geschrieben wird: Ist ein Fehler fachlich oder technisch? Ein fachlicher Fehler ist Teil des normalen Geschäftsablaufs, ein Lagerbestand reicht nicht aus, ein Rabattcode ist abgelaufen, eine Kundennummer existiert nicht. Solche Fehler sind erwartbar, oft dem Endnutzer zumutbar zu erklären und in aller Regel kein Zeichen für einen Bug.
Ein technischer Fehler dagegen liegt außerhalb der fachlichen Logik: eine Datenbankverbindung bricht ab, ein externer Zahlungsdienst antwortet nicht, eine Konfigurationsdatei fehlt. Diese Fehler sind fast immer unerwartet, für Endnutzer nicht sinnvoll erklärbar und für den Betrieb alarmierungswürdig. Wenn Exception-Hierarchien diese Trennlinie nicht abbilden, landen fachliche und technische Fehler im selben catch-Block und im selben Log-Level, mit der Folge, dass entweder harmlose fachliche Fälle das Monitoring fluten oder echte Ausfälle in der Masse untergehen.
Diese Unterscheidung ist der rote Faden durch die gesamte restliche Architektur: Sie bestimmt, welchen HTTP-Statuscode eine API zurückgibt, welches Log-Level verwendet wird, ob ein Retry sinnvoll ist und ob überhaupt ein Mensch benachrichtigt werden muss. Jede weitere Design-Entscheidung in diesem Artikel, von der Basisklasse bis zum globalen Error-Handling, baut auf genau dieser fachlich/technisch-Trennung auf.
3. Eine abstrakte Basisklasse für die eigene Exception-Hierarchie entwerfen
Der erste Baustein eigener Exception-Hierarchien ist eine abstrakte Basisklasse pro fachlichem Kontext (Bounded Context), nicht eine einzige globale Basisklasse für die gesamte Anwendung. Eine Basisklasse pro Kontext, etwa OrderDomainException für alles rund um Bestellungen, gibt dem Aufrufer einen einzigen sinnvollen catch-Punkt für diesen Kontext, ohne alle Exceptions der Anwendung in einen Topf zu werfen.
Wichtiger als die Vererbung selbst ist, was die Basisklasse erzwingt. Eine abstrakte Methode wie errorCode() zwingt jede konkrete Exception, einen stabilen, maschinenlesbaren Bezeichner zu deklarieren, der unabhängig vom veränderlichen Nachrichtentext ist. Ein privates, aber über einen Getter zugängliches Kontext-Array im Konstruktor gibt jeder Exception der Hierarchie von Anfang an einen Platz für strukturierte Zusatzdaten, statt dass jede Klasse ihre eigene Lösung dafür erfindet.
<?php
declare(strict_types=1);
namespace App\Domain\Exception;
/**
* Abstract base class for all exceptions raised by the order domain.
* Every custom exception in this bounded context extends this class,
* which gives us a single catch point and a place for shared behavior.
*/
abstract class OrderDomainException extends \RuntimeException
{
/**
* @param string $message Human readable message (not shown to end users)
* @param array<string, mixed> $context Structured data for logging and debugging
* @param \Throwable|null $previous Original exception, if this wraps a lower layer error
*/
public function __construct(
string $message,
private readonly array $context = [],
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
/**
* Returns structured context data attached to this exception.
*
* @return array<string, mixed>
*/
public function getContext(): array
{
return $this->context;
}
/**
* Every concrete exception must declare a stable, machine readable
* error code that frontends and logs can rely on, independent of message text.
*/
abstract public function errorCode(): string;
}
Diese Basisklasse ist bewusst schlank gehalten: Sie erweitert RuntimeException, weil die meisten fachlichen und technischen Laufzeitfehler in diese Kategorie fallen, und sie reicht die $previous-Exception unverändert an die Elternklasse durch. Jede konkrete Exception der Hierarchie muss sich nur noch um zwei Dinge kümmern: eine sprechende Nachricht und einen stabilen errorCode, der Rest, inklusive Kontext-Handling, ist bereits gelöst.
4. Marker-Interfaces: Exceptions kategorisieren ohne Vererbungszwang
Vererbung allein reicht nicht aus, um Exceptions nach mehreren, sich überschneidenden Kriterien zu kategorisieren. Eine Exception kann gleichzeitig fachlich und wiederholbar sein, oder technisch und wiederholbar, eine einzelne Klassenhierarchie mit einfacher Vererbung kann solche Kombinationen nicht sauber abbilden. Genau hier kommen Marker-Interfaces ins Spiel: leere oder nahezu leere Interfaces wie BusinessException, TechnicalException und RetryableException, die eine Exception zusätzlich zu ihrer Klassenhierarchie kategorisieren.
Der entscheidende Vorteil von Marker-Interfaces gegenüber tieferer Vererbung: Eine konkrete Exception-Klasse kann implements BusinessException oder implements TechnicalException, RetryableException deklarieren, ohne dass die zugrunde liegende Klassenhierarchie verändert werden muss. Aufrufer fangen dann nicht mehr eine konkrete Klasse, sondern eine Fähigkeit, catch (RetryableException $e) funktioniert unabhängig davon, ob die konkrete Exception aus der Bestell-, Zahlungs- oder Versanddomäne stammt.
<?php
declare(strict_types=1);
namespace App\Domain\Exception;
/**
* Marker interface for all business/domain errors: expected, user-facing,
* and part of normal application flow (e.g. validation, business rule violations).
*/
interface BusinessException extends \Throwable
{
}
/**
* Marker interface for technical/infrastructure errors: unexpected,
* not meant to be shown to end users, always worth alerting on.
*/
interface TechnicalException extends \Throwable
{
}
/**
* Marker interface for errors where a retry might succeed
* (e.g. transient network or lock issues).
*/
interface RetryableException extends \Throwable
{
}
/**
* Raised when a referenced product no longer exists. Business error:
* expected, user-facing, no bug, no alert needed.
*/
final class ProductNotFoundException extends OrderDomainException implements BusinessException
{
public function errorCode(): string
{
return 'ORDER_PRODUCT_NOT_FOUND';
}
}
/**
* Raised when the payment gateway times out. Technical error, but
* retrying the same request is likely to succeed.
*/
final class PaymentGatewayTimeoutException extends OrderDomainException implements TechnicalException, RetryableException
{
public function errorCode(): string
{
return 'ORDER_PAYMENT_GATEWAY_TIMEOUT';
}
}
// Catching by capability, independent of class hierarchy position
function handle(\Throwable $e, LoggerInterface $logger): void
{
match (true) {
$e instanceof RetryableException => retryLater($e),
$e instanceof BusinessException => $logger->info($e->getMessage()),
$e instanceof TechnicalException => $logger->error($e->getMessage()),
default => throw $e,
};
}
In der Praxis bedeutet das: Ein globaler Error-Handler kann mit einem einzigen match (true) über instanceof-Prüfungen gegen Marker-Interfaces entscheiden, wie mit einer Exception umzugehen ist, ganz ohne die konkrete Klasse zu kennen. Neue konkrete Exceptions, die künftig hinzukommen, müssen nur die passenden Interfaces implementieren, der bestehende Error-Handling-Code ändert sich nicht.
5. Strukturierte Kontextdaten mit Constructor Property Promotion
Ein generisches Kontext-Array, wie es die Basisklasse anbietet, ist ein guter Anfang, aber am Ende nur ein weiteres assoziatives Array mit den bekannten Schwächen: keine Typprüfung, keine Autovervollständigung in der IDE, kein Fehler beim Zugriff auf einen falsch geschriebenen Schlüssel. Für Exceptions, die regelmäßig ausgewertet werden, etwa um eine Nutzermeldung zu bauen oder eine Retry-Entscheidung zu treffen, lohnt sich stattdessen ein typisiertes Kontextobjekt direkt im Konstruktor der konkreten Exception.
Constructor Property Promotion macht das ohne zusätzlichen Boilerplate möglich: Die Kontextdaten werden als readonly-Properties direkt im Konstruktor deklariert und gleichzeitig für die Nachricht sowie für das generische Kontext-Array der Basisklasse verwendet. So bekommt jeder Aufrufer typsichere Getter wie getSku() oder getRequestedQuantity(), während Logging-Code weiterhin auf das generische getContext() zurückgreifen kann, wenn er keine konkrete Klasse kennt.
<?php
declare(strict_types=1);
namespace App\Domain\Exception;
/**
* Raised when a requested product variant is out of stock.
* Carries strongly typed context instead of a generic array,
* so callers and logging code get IDE autocompletion and type safety.
*/
final class InsufficientStockException extends OrderDomainException implements BusinessException
{
public function __construct(
private readonly string $sku,
private readonly int $requestedQuantity,
private readonly int $availableQuantity,
?\Throwable $previous = null,
) {
parent::__construct(
message: sprintf(
'Cannot fulfill order for SKU "%s": requested %d, only %d available.',
$sku,
$requestedQuantity,
$availableQuantity,
),
context: [
'sku' => $sku,
'requested_quantity' => $requestedQuantity,
'available_quantity' => $availableQuantity,
],
previous: $previous,
);
}
public function getSku(): string
{
return $this->sku;
}
public function getRequestedQuantity(): int
{
return $this->requestedQuantity;
}
public function getAvailableQuantity(): int
{
return $this->availableQuantity;
}
public function errorCode(): string
{
return 'ORDER_INSUFFICIENT_STOCK';
}
}
// Usage: type-safe access to context, no array key lookups
try {
$orderService->placeOrder($cart);
} catch (InsufficientStockException $e) {
$logger->warning($e->getMessage(), [
'sku' => $e->getSku(),
'missing' => $e->getRequestedQuantity() - $e->getAvailableQuantity(),
]);
}
Ein wichtiger Nebeneffekt: Weil die Kontextdaten explizite, typisierte Properties sind, fällt es sofort auf, wenn versehentlich sensible Daten wie Zahlungsdaten oder Passwörter in eine Exception wandern, der Typ macht sichtbar, was tatsächlich transportiert wird. Bei einem unstrukturierten Array bleibt so ein Fehler dagegen oft unbemerkt, bis er in einem Log-Aggregator auftaucht.
6. Schichtenarchitektur: Domain-, Application- und Infrastructure-Exceptions abgrenzen
In größeren Anwendungen mit einer Trennung zwischen Domain-, Application- und Infrastructure-Schicht sollten auch Exception-Hierarchien dieser Schichtung folgen. Die Infrastructure-Schicht kennt Datenbanktreiber, HTTP-Clients und Dateisysteme und wirft entsprechend spezifische Exceptions wie DatabaseConnectionException. Die Domain-Schicht kennt nur fachliche Konzepte wie Bestellungen und Lagerbestände und darf von diesen technischen Details nichts wissen, ihre Exceptions wie OrderRepositoryUnavailableException drücken aus, was aus fachlicher Sicht schiefgegangen ist, nicht wie.
Die Application-Schicht übernimmt dabei die Rolle des Übersetzers zwischen beiden Welten: Sie orchestriert Use Cases, ruft Infrastruktur-Code auf und fängt dessen technische Exceptions ab, um sie in domänenspezifische Exceptions zu übersetzen. Würde die Domain-Schicht stattdessen direkt PDOException oder eine Guzzle-spezifische Exception fangen, wäre sie an eine konkrete Infrastruktur-Implementierung gekoppelt, ein Wechsel des Datenbanktreibers würde dann Änderungen tief im fachlichen Code erzwingen.
<?php
declare(strict_types=1);
namespace App\Infrastructure\Exception;
// Infrastructure layer: knows about the database, HTTP client, filesystem
final class DatabaseConnectionException extends \RuntimeException
{
}
namespace App\Domain\Exception;
// Domain layer: knows nothing about infrastructure, only business rules
// (constructor with context/errorCode omitted here for brevity, see section 3)
abstract class OrderDomainException extends \RuntimeException
{
}
final class OrderRepositoryUnavailableException extends OrderDomainException
{
}
namespace App\Application\Order;
// Application layer: orchestrates use cases, translates between layers
use App\Domain\Exception\OrderRepositoryUnavailableException;
use App\Infrastructure\Exception\DatabaseConnectionException;
use App\Infrastructure\Repository\OrderRepository;
final readonly class PlaceOrderHandler
{
public function __construct(
private OrderRepository $orderRepository,
) {
}
public function handle(PlaceOrderCommand $command): void
{
try {
$this->orderRepository->save($command->toOrder());
} catch (DatabaseConnectionException $e) {
// Translate the infrastructure detail into a domain-level exception;
// callers above this point must not need to know about PDO or SQL.
throw new OrderRepositoryUnavailableException(
'Order could not be persisted due to a repository failure.',
previous: $e,
);
}
}
}
Diese Schichtentrennung zahlt sich vor allem beim Testen aus: Domain- und Application-Tests müssen keine echten Infrastruktur-Exceptions simulieren, sondern arbeiten ausschließlich mit den eigenen, klar benannten Exceptions der jeweiligen Schicht. Das macht Tests robuster gegenüber Änderungen an der eingesetzten Infrastruktur und hält die Fehlerklassen jeder Schicht in sich konsistent.
7. Namenskonventionen und Granularität: wann eine neue Exception-Klasse sinnvoll ist
Der Name einer Exception-Klasse ist keine Kleinigkeit: InsufficientStockException sagt sofort, was passiert ist, während GeneralOrderException oder gar OrderProblemException nichtssagend bleibt und den Aufrufer zwingt, wieder in getMessage() zu suchen. Eine bewährte Konvention: der Name beschreibt die fachliche Situation, nicht die technische Implementierung, und endet konsequent auf Exception, damit im Code sofort erkennbar ist, womit man es zu tun hat.
Bei der Granularität hilft eine einfache Leitfrage: Muss der Aufrufer auf diesen Fehlerfall anders reagieren als auf einen bestehenden? Wenn ja, rechtfertigt das eine neue Klasse. Wenn der Unterschied nur im Wert einer Variable liegt, etwa welche SKU betroffen ist, reicht eine bestehende Klasse mit entsprechenden Konstruktor-Parametern völlig aus. Diese Regel verhindert zwei entgegengesetzte Fehler: eine einzige Gott-Exception für alles und eine unübersichtliche Explosion von fast identischen Klassen.
In der Praxis bewährt sich außerdem, Exception-Namen im gleichen Vokabular wie die Ubiquitous Language der Domäne zu halten. Wenn Fachexperten von einer "Stornierungssperre" sprechen, sollte die Exception CancellationBlockedException heißen und nicht etwa OrderStateException, so bleiben Code und Fachsprache über die gesamte Exception-Hierarchie hinweg deckungsgleich, was Reviews und das Onboarding neuer Entwickler erheblich erleichtert.
8. Exception-Übersetzung an Schichtgrenzen: technische in fachliche Fehler umwandeln
Exception-Übersetzung an Schichtgrenzen bedeutet: eine tiefliegende technische Exception wird abgefangen und durch eine höherwertige, fachlich sinnvolle Exception ersetzt, wobei die ursprüngliche Exception als $previous erhalten bleibt. Dieses Muster ist keine Ausnahme, sondern die Regel überall dort, wo Domain- oder Application-Code Infrastruktur aufruft, die Fehler werfen kann, die im aktuellen Kontext keine Bedeutung haben.
Der Gewinn liegt in der Entkopplung: Kein Aufrufer der Domain-Schicht muss jemals \PDOException oder eine Guzzle-spezifische Exception kennen oder importieren. Bricht das Team von PDO auf einen anderen Datenbank-Layer um, ändert sich nur die Übersetzung innerhalb des Repositorys, alle Domain- und Application-Exceptions bleiben unverändert, und mit ihnen jeder bestehende catch-Block.
<?php
declare(strict_types=1);
namespace App\Infrastructure\Repository;
use App\Domain\Exception\BusinessException;
use App\Domain\Exception\CustomerNotFoundException;
use App\Domain\Exception\OrderDomainException;
use App\Domain\Exception\OrderRepositoryUnavailableException;
final readonly class PdoOrderRepository
{
public function __construct(private \PDO $connection)
{
}
/**
* Loads a customer's order history. Translates low-level PDO failures
* into domain exceptions so callers never need to catch \PDOException.
*
* @return array<int, array<string, mixed>>
*/
public function findOrdersByCustomerId(string $customerId): array
{
try {
$statement = $this->connection->prepare(
'SELECT * FROM orders WHERE customer_id = :customer_id',
);
$statement->execute(['customer_id' => $customerId]);
$rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
// Connection lost, deadlock, syntax error: technical details
// the domain layer should never have to know about.
throw new OrderRepositoryUnavailableException(
sprintf('Could not load orders for customer "%s".', $customerId),
previous: $e,
);
}
if ($rows === []) {
// CustomerNotFoundException is a BusinessException, defined
// analogous to InsufficientStockException in an earlier example.
throw new CustomerNotFoundException($customerId);
}
return $rows;
}
}
// Global handler at the system boundary (e.g. a PSR-15 middleware)
set_exception_handler(function (\Throwable $e) use ($logger): void {
if ($e instanceof BusinessException) {
$logger->info('Business exception surfaced at the boundary', [
'code' => $e instanceof OrderDomainException ? $e->errorCode() : 'unknown',
]);
http_response_code(422);
} else {
$logger->critical('Unhandled technical exception', ['exception' => $e]);
http_response_code(500);
}
});
Wichtig ist, die ursprüngliche Exception niemals zu verschlucken: Der $previous-Parameter erhält den vollständigen Stacktrace der technischen Ursache für Logging und Debugging, während die neue Exception-Nachricht bewusst auf fachlicher Ebene formuliert bleibt. So bekommen Entwickler beim Debuggen die volle technische Tiefe, während Nutzer oder API-Konsumenten nur die fachlich relevante, übersetzte Fehlermeldung sehen.
9. Zusammenspiel mit globalem Error-Handling und Logging an der Systemgrenze
Am Rand des Systems, sei es ein PSR-15-Middleware-Stack, ein Framework-Kernel oder ein einfacher set_exception_handler(), laufen letztlich alle Exception-Hierarchien der Anwendung zusammen. Genau hier zahlt sich die Vorarbeit aus den vorherigen Abschnitten aus: Statt Dutzende konkrete Klassen einzeln zu behandeln, prüft der globale Handler nur gegen die wenigen Marker-Interfaces und Basisklassen, um Log-Level, HTTP-Statuscode und Alerting-Verhalten zu bestimmen.
Ein durchdachtes globales Error-Handling nutzt dabei konsequent die strukturierten Kontextdaten aus Abschnitt 5: statt einer reinen Textmeldung landen SKU, Kundennummer oder Bestellnummer als strukturierte Felder im Log-Eintrag und lassen sich mit einer Korrelations-ID über mehrere Log-Zeilen hinweg zusammenführen. Die folgende Tabelle vergleicht die vier gängigsten Ansätze für eigene Exception-Hierarchien hinsichtlich Wartbarkeit, Granularität und Catch-Aufwand.
| Ansatz | Wartbarkeit | Granularität | Catch-Aufwand |
|---|---|---|---|
| Eine Exception für alles | gering, verschleiert Ursachen | keine | minimal, aber nichtssagend |
| Pro Anwendungsfall eine Klasse | explodiert bei Wachstum | sehr hoch | hoch, viele catch-Blöcke |
| Basisklasse + Marker-Interfaces | hoch | gezielt steuerbar | gering, nach Interface fangen |
| Schichtenspezifische Hierarchien | hoch in großen Systemen | pro Schicht klar begrenzt | mittel, Übersetzung an Grenzen nötig |
| Nur eingebaute SPL-Exceptions | gering | keine fachliche Trennung | gering, aber ohne Kontext |
Für produktive Systeme empfiehlt sich fast immer eine Kombination: eine Basisklasse pro Bounded Context, Marker-Interfaces für fachlich/technisch sowie Wiederholbarkeit, und an größeren Schichtgrenzen zusätzlich eine Übersetzung zwischen Infrastructure- und Domain-Exceptions. Diese Kombination hält die Zahl der Klassen überschaubar, ohne auf Granularität oder Typsicherheit zu verzichten, und macht die eigene Fehlerklassen-Architektur zu einem Werkzeug, das mit dem Projekt mitwächst, statt es zu bremsen.
10. Zusammenfassung
Eigene Exception-Hierarchien lohnen sich, sobald ein Projekt über eine Handvoll Fehlerfälle hinauswächst und unterschiedliche Konsumenten, Nutzer, Entwickler, Monitoring, unterschiedliche Informationen aus demselben Fehler brauchen. Die zentrale Trennlinie zwischen fachlichen und technischen Fehlern entscheidet über Log-Level, HTTP-Status und Alerting und sollte die erste Design-Entscheidung jeder eigenen Exception-Hierarchie sein, noch vor der ersten konkreten Klasse.
Eine abstrakte Basisklasse pro Bounded Context, Marker-Interfaces für cross-cutting Kategorien wie Retryable, typisierte Kontextdaten per Constructor Property Promotion und eine bewusste Übersetzung an Schichtgrenzen ergeben zusammen eine Struktur, die weder in einer einzigen Gott-Exception noch in hunderten Fast-Duplikaten endet. Wer diese Bausteine von Anfang an konsequent einsetzt, spart sich ein aufwendiges Refactoring der Fehlerbehandlung, sobald das Projekt seinen ersten Wachstumsschub erlebt.
Eigene Exception-Hierarchien: Das Wichtigste auf einen Blick
Fachlich vs. Technisch
Die zentrale Trennlinie jeder Exception-Hierarchie: bestimmt Log-Level, HTTP-Status und ob ein Retry sinnvoll ist.
Basisklasse pro Kontext
Eine abstrakte Basisklasse je Bounded Context bündelt Kontext-Handling und erzwingt einen stabilen errorCode().
Marker-Interfaces
BusinessException, TechnicalException, RetryableException kategorisieren quer zur Klassenhierarchie, ganz ohne tiefe Vererbung.
Schichten & Übersetzung
Domain-Exceptions kennen keine Infrastruktur-Details; Application-Code übersetzt technische in fachliche Fehler an der Grenze.
11. FAQ: Eigene Exception-Hierarchien
1Was ist der Unterschied zu den SPL-Exceptions?
2Wann lohnt sich eine eigene Hierarchie, wann ist es Overkill?
3Exception oder RuntimeException als Basis?
4Vorteil von Marker-Interfaces gegenüber Vererbung?
5Wie viele Kontextdaten sollte eine Exception tragen?
6Wie übersetzt man technische in fachliche Fehler?
7Braucht jede Schicht eine eigene Hierarchie?
8Wie wirkt sich das auf globales Error-Handling aus?
9Ist eine Exception pro Anwendungsfall sinnvoll?
10BusinessException und TechnicalException gleichzeitig?
Mironsoft
PHP-Architektur, Domain-Driven Design und wartbare Fehlerbehandlung
Wächst euer PHP-Projekt aus der eigenen Fehlerbehandlung heraus?
Wir analysieren bestehende Codebasen, identifizieren fragile catch-Blöcke und entwerfen eigene Exceptions mit klaren Basisklassen, Marker-Interfaces und sauberer Schichtenarchitektur, die auch nach Jahren im Betrieb noch wartbar bleiben.
Architektur-Review
Analyse bestehender Fehlerklassen und Aufdecken fachlich/technischer Vermischung
Refactoring
Basisklassen, Marker-Interfaces und typisierte Kontextdaten schrittweise einführen
Schichtenarchitektur
Domain-, Application- und Infrastructure-Exceptions sauber abgrenzen und übersetzen