von der starren Wertliste zum polymorphen Baustein
Wer Enums nur als typsichere Konstanten-Liste nutzt, lässt die eigentliche Stärke von PHP-Enums liegen: Enums mit Interfaces implementieren echtes objektorientiertes Verhalten pro Case, ersetzen verstreute match-Blöcke im Aufrufercode durch eine einzige Methode im Enum selbst und machen neue Fälle über den Compiler statt über Grep-Suchen im Projekt sichtbar.
Inhaltsverzeichnis
- 1. Warum reine Enums oft nicht ausreichen
- 2. Enums implementieren Interfaces wie jede andere Klasse
- 3. Eigene Methoden direkt im Enum definieren
- 4. Mehrere Interfaces gleichzeitig implementieren
- 5. Statische Methoden und Fabrik-Pattern in Enums
- 6. match-Ausdrücke in Enum-Methoden für fallspezifisches Verhalten
- 7. Enums mit Interfaces als leichtgewichtiger Strategy-Pattern-Ersatz
- 8. Typsicherheit durch Interface als Parameter- und Rückgabetyp
- 9. Grenzen und Fallstricke: keine Properties, keine Vererbung
- 10. Zusammenfassung
- 11. FAQ
1. Warum reine Enums oft nicht ausreichen
Seit PHP 8.1 lösen Enums ein altes Problem: feste Wertemengen mussten vorher über Klassenkonstanten oder rohe Strings abgebildet werden, ohne dass der Compiler ungültige Werte erkannte. Ein OrderStatus als Enum stellt sicher, dass nur die definierten Cases existieren können. Doch sobald zu diesem Status auch Verhalten gehört, etwa eine Anzeigefarbe, ein übersetztes Label oder eine Berechtigungsprüfung, reicht die reine Wertemenge nicht mehr aus. Genau hier setzen Enums mit Interfaces an.
Ohne dieses Muster wandert die Fallunterscheidung in externe Funktionen: eine getStatusColor()-Funktion mit einem match($status)-Block hier, eine getStatusLabel()-Funktion mit einem fast identischen Block dort. Jede neue Ausprägung des Enums zwingt dazu, alle diese verstreuten Stellen im Projekt zu finden und zu ergänzen. Vergisst man eine Stelle, bricht die Anwendung erst zur Laufzeit, wenn genau dieser Case erstmals auftritt, häufig in Produktion und nicht im Test.
Enums mit Interfaces lösen dieses Problem an der Wurzel, indem sie das Verhalten direkt am Case verankern. Statt die Logik außerhalb des Enums zu verteilen, implementiert das Enum ein Interface und definiert die passende Methode selbst. Der Compiler erzwingt dabei, dass jede Methode des Interfaces vorhanden ist, und jeder neue Case erbt automatisch dieselbe Struktur. Das macht dieses Pattern zum natürlichen nächsten Schritt, sobald ein Enum mehr als nur einen Wert transportieren soll.
2. Enums implementieren Interfaces wie jede andere Klasse
Technisch ist ein Enum in PHP eine spezielle Klasse mit einer festen, vom Compiler kontrollierten Anzahl an Instanzen, den Cases. Genau deshalb funktioniert die Interface-Syntax bei Enums identisch zu gewöhnlichen Klassen: enum OrderStatus implements HasColor verpflichtet das Enum, jede im Interface deklarierte Methode zu implementieren. Diese Symmetrie zu Klassen ist der Kern dessen, was Enums mit Interfaces so nützlich macht, denn bestehendes Wissen über Interfaces überträgt sich eins zu eins.
Der entscheidende Unterschied zu einer gewöhnlichen Klasse liegt darin, dass jede Methode innerhalb des Enum-Bodys direkt auf $this zugreifen kann, wobei $this immer genau einen der definierten Cases repräsentiert. Eine Interface-Methode wie color(): string kann also je nach Case eine andere Zeichenkette liefern, ohne dass der Aufrufer wissen muss, wie viele Cases es gibt oder welcher davon gerade vorliegt. Das ist echter Polymorphismus, nicht nur eine Wertzuweisung.
<?php
declare(strict_types=1);
interface HasColor
{
public function color(): string;
}
enum OrderStatus: string implements HasColor
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
// Interface method implemented once, resolved per case via match($this)
public function color(): string
{
return match ($this) {
self::Pending => '#f59e0b',
self::Shipped => '#3b82f6',
self::Delivered => '#22c55e',
self::Cancelled => '#ef4444',
};
}
}
function renderBadge(HasColor $status): string
{
// Caller only depends on the interface, not on OrderStatus itself
return sprintf('<span style="color:%s">', $status->color());
}
echo renderBadge(OrderStatus::Shipped);
3. Eigene Methoden direkt im Enum definieren
Neben den durch ein Interface erzwungenen Methoden können Enums beliebige eigene Methoden definieren, die kein Interface verlangt. Das ist der zweite Baustein von Enums mit Interfaces: Interface-Methoden bilden den nach außen sichtbaren Vertrag, eigene Methoden kapseln interne Hilfslogik, die von mehreren Interface-Methoden gemeinsam genutzt wird. Eine private Hilfsmethode innerhalb eines Enums ist syntaktisch identisch zu einer privaten Methode in einer Klasse.
Auch Klassenkonstanten sind in Enums erlaubt und werden häufig unterschätzt. Eine const DEFAULT_LABEL = 'Unbekannt' innerhalb des Enums steht allen Methoden zur Verfügung, ohne dass sie außerhalb des Enums sichtbar sein muss, sofern sie als private const deklariert wird. Diese Kombination aus eigenen Methoden und privaten Konstanten macht ein Enum zu einer vollwertigen, in sich geschlossenen Verhaltenseinheit, statt nur ein Behälter für Rohwerte zu sein.
Ein häufiges Muster: eine öffentliche Interface-Methode label(): string ruft intern eine private Methode rawLabel(): string auf, die zusätzlich noch eine Formatierung anwendet, etwa Großschreibung oder das Anhängen eines Suffixes. So bleibt die öffentliche Schnittstelle stabil, während sich die interne Aufbereitung ändern kann, ohne den Aufrufercode zu berühren.
<?php
declare(strict_types=1);
enum Priority: int
{
case Low = 1;
case Medium = 2;
case High = 3;
private const string SUFFIX = ' priority';
// Public method delegates formatting to a private helper
public function label(): string
{
return ucfirst($this->rawLabel()) . self::SUFFIX;
}
// Private helper: not part of any interface, pure internal detail
private function rawLabel(): string
{
return strtolower($this->name);
}
}
echo Priority::High->label(); // "High priority"
4. Mehrere Interfaces gleichzeitig implementieren
Ein Enum ist nicht auf ein einziges Interface beschränkt. Genau wie eine Klasse kann ein Enum mehrere Interfaces gleichzeitig implementieren, getrennt durch Komma: enum OrderStatus implements HasColor, HasLabel, JsonSerializable. Diese Kombinierbarkeit ist einer der stärksten Gründe, warum Enums mit Interfaces in gewachsenen Codebasen so gut funktionieren, denn verschiedene Aufrufer können jeweils nur das Interface anfordern, das sie tatsächlich benötigen.
Diese Trennung nach Interface Segregation Principle bedeutet: eine Funktion, die nur die Anzeigefarbe braucht, deklariert ihren Parameter als HasColor, nicht als OrderStatus. Ein Test kann so problemlos ein einfaches Test-Enum mit nur diesem einen Interface durchreichen, ohne die komplette Produktionslogik mitzuschleppen. Ohne Enums mit Interfaces müsste ein Test entweder das komplette Enum instanziieren oder mit Mocking-Bibliotheken arbeiten, was bei einem finalen Enum ohnehin nicht funktioniert.
<?php
declare(strict_types=1);
interface HasLabel
{
public function label(): string;
}
enum OrderStatus: string implements HasColor, HasLabel, JsonSerializable
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
private const DEFAULT_LABEL = 'Unbekannt';
public function color(): string
{
return match ($this) {
self::Pending => '#f59e0b',
self::Shipped => '#3b82f6',
self::Delivered => '#22c55e',
self::Cancelled => '#ef4444',
};
}
public function label(): string
{
return match ($this) {
self::Pending => 'Ausstehend',
self::Shipped => 'Versandt',
self::Delivered => 'Zugestellt',
self::Cancelled => 'Storniert',
} ?: self::DEFAULT_LABEL;
}
// JsonSerializable is required because backed enums serialize to
// their scalar value only, never as a rich object, by default
public function jsonSerialize(): array
{
return ['value' => $this->value, 'label' => $this->label()];
}
}
5. Statische Methoden und Fabrik-Pattern in Enums
Neben Instanzmethoden erlauben Enums auch statische Methoden, die nicht an einen konkreten Case gebunden sind. Ein typischer Anwendungsfall im Kontext von Enums mit Interfaces ist eine statische Fabrikmethode, die aus einem externen, möglicherweise inkonsistenten Wert, etwa einem Legacy-Datenbankcode, den passenden Case ermittelt. Anders als from() und tryFrom() bei Backed Enums kann eine eigene statische Methode zusätzliche Normalisierung durchführen, bevor sie den Case bestimmt.
Auch das Zusammenstellen von Lookup-Strukturen aus self::cases() gehört zu den nützlichen statischen Methoden. Eine Methode wie labels(): array, die über alle Cases iteriert und ein assoziatives Array aus Wert und Label baut, wird an genau einer Stelle im Enum gepflegt statt in jedem Formular oder jeder API-Antwort neu aufgebaut zu werden. Das reduziert Duplikation erheblich, gerade wenn mehrere Frontends dieselbe Optionsliste benötigen.
<?php
declare(strict_types=1);
enum OrderStatus: string implements HasColor, HasLabel
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
// Static factory: normalizes legacy codes before resolving the case
public static function fromLegacyCode(string $legacyCode): self
{
$normalized = strtolower(trim($legacyCode));
return match ($normalized) {
'p', 'open' => self::Pending,
's', 'sent' => self::Shipped,
'd', 'done' => self::Delivered,
'c', 'void' => self::Cancelled,
default => throw new ValueError("Unknown legacy code: {$legacyCode}"),
};
}
// Static helper built from cases(), maintained in a single place
public static function labels(): array
{
$result = [];
foreach (self::cases() as $case) {
$result[$case->value] = $case->label();
}
return $result;
}
public function color(): string
{
return match ($this) {
self::Pending => '#f59e0b',
self::Shipped => '#3b82f6',
self::Delivered => '#22c55e',
self::Cancelled => '#ef4444',
};
}
public function label(): string
{
return match ($this) {
self::Pending => 'Ausstehend',
self::Shipped => 'Versandt',
self::Delivered => 'Zugestellt',
self::Cancelled => 'Storniert',
};
}
}
$status = OrderStatus::fromLegacyCode('OPEN');
print_r(OrderStatus::labels());
6. match-Ausdrücke in Enum-Methoden für fallspezifisches Verhalten
match($this) ist der zentrale Baustein, um innerhalb einer Enum-Methode zwischen den Cases zu unterscheiden. Anders als switch erlaubt match keinen Fallthrough und erzwingt strikten Vergleich, was Vergleichsfehler zwischen Cases praktisch ausschließt. Bei Enums mit Interfaces ist match($this) die naheliegende Methode, weil $this im Enum-Kontext immer exakt einer der definierten Cases ist, niemals ein beliebiger Wert von außen.
Ein wichtiger Effekt: Ein match-Ausdruck ohne default-Zweig wirft eine UnhandledMatchError, sobald ein Case auftritt, für den kein Arm existiert. Das ist bei Enums mit Interfaces ausdrücklich erwünscht, denn genau dieser Fehler macht sichtbar, wenn ein neuer Case ergänzt wurde, aber eine Methode im Enum vergessen wurde, sie zu behandeln. Statt eines stillen, falschen Default-Werts gibt es einen klaren, sofort sichtbaren Laufzeitfehler direkt an der Stelle, wo die Lücke tatsächlich existiert.
Wer bewusst einen Fallback will, kann trotzdem einen default-Zweig ergänzen, sollte sich aber bewusst sein, dass dieser Zweig neue Cases stillschweigend abfängt, statt auf sie aufmerksam zu machen. In den meisten Enum-Methoden ist der bewusste Verzicht auf default daher die robustere Wahl, weil die Exhaustivitätsprüfung zur Entwicklungszeit mehr Fehler verhindert, als sie zur Laufzeit verursacht.
7. Enums mit Interfaces als leichtgewichtiger Strategy-Pattern-Ersatz
Das klassische Strategy-Pattern löst austauschbares Verhalten über eine Interface-Hierarchie mit einer eigenen Klasse pro Strategie. Für zustandslose, feste Verhaltensvarianten ist das oft mehr Struktur, als tatsächlich benötigt wird. Enums mit Interfaces bieten hier eine deutlich leichtgewichtigere Alternative: Statt vier separater Klassen mit jeweils eigener Datei genügt ein Enum mit vier Cases und den passenden Methoden, ohne dass Ausdruckskraft verloren geht.
Der Vorteil zeigt sich besonders bei der Instanziierung: Strategien als Klassen benötigen häufig eine Fabrik oder einen Dependency-Injection-Container, um die richtige Instanz aufzulösen. Enum-Cases existieren dagegen als Singletons bereits zur Compile-Zeit, sie müssen niemals konstruiert werden und können direkt als Wert übergeben, verglichen und in Arrays verwendet werden. Für Strategien ohne eigenen inneren Zustand ist das Enum damit fast immer die einfachere Lösung.
Sobald eine Strategie jedoch eigenen veränderlichen Zustand oder konstruktorabhängige Kollaborateure benötigt, etwa eine injizierte Abhängigkeit, stößt das Enum an seine Grenze, denn Enums können keine Instanzeigenschaften mit eigenem Zustand halten. In diesem Fall bleibt die klassische Strategy-Klasse die richtige Wahl, während Enums mit Interfaces für die zustandslosen, endlichen Fälle reserviert bleiben sollten.
8. Typsicherheit durch Interface als Parameter- und Rückgabetyp
Sobald ein Enum ein Interface implementiert, kann dieses Interface überall dort als Typ verwendet werden, wo bisher nur das konkrete Enum denkbar war. Eine Funktionssignatur wie function renderBadge(HasColor $status): string akzeptiert jedes Enum und jede Klasse, die HasColor implementiert, nicht nur OrderStatus. Diese Entkopplung ist einer der praktischen Vorteile von Enums mit Interfaces gegenüber einem hart typisierten Parameter.
Für Tests bedeutet das: Anstelle des kompletten Produktions-Enums kann ein separates, minimales Test-Enum mit demselben Interface durchgereicht werden, um Randfälle zu prüfen, ohne die eigentliche Fachlogik zu berühren. Diese Art der Typsicherheit wird vom Compiler durchgesetzt, nicht nur durch Konvention oder Dokumentation, wodurch Verstöße bereits bei der statischen Analyse mit PHPStan auffallen, lange bevor der Code ausgeführt wird.
| Aufgabe | Ohne Enums mit Interfaces | Mit Enums mit Interfaces | Vorteil |
|---|---|---|---|
| Statusfarbe ermitteln | match($status) an jeder Aufrufstelle |
$status->color() |
Zentrale Logik, keine Duplikate |
| Neuen Status hinzufügen | Alle match-Blöcke im Code suchen und ergänzen | Neuen Case plus Methodenzweig im Enum ergänzen | Enum bleibt einzige Änderungsstelle |
| Testbarkeit der Statuslogik | Mehrere freie Funktionen einzeln testen | Enum-Methoden isoliert und direkt testen | Weniger Testaufwand, klar gekapselt |
| Polymorphes Verhalten | if/else-Ketten über ->value |
Interface-Typehint plus Methodenaufruf | Echter Polymorphismus ohne instanceof |
| Austauschbare Implementierung | Enum-Wert an vielen Stellen abgefragt | Strategie über Interface-Methode gekapselt | Open-Closed-Prinzip eingehalten |
9. Grenzen und Fallstricke: keine Properties, keine Vererbung
So mächtig Enums mit Interfaces sind, sie bleiben in wichtigen Punkten eingeschränkt. Enums können keine Instanzeigenschaften mit veränderlichem Zustand deklarieren, nur Konstanten. Wer versucht, eine normale Property in ein Enum zu schreiben, erhält einen Parse-Fehler. Wer Zustand pro Case benötigt, muss ihn entweder als Rückgabewert einer Methode berechnen oder auf eine klassische Klasse ausweichen.
Ebenso wenig kann ein Enum von einem anderen Enum oder einer Klasse erben. Zwischen Enums gibt es keine Vererbungshierarchie, nur Interfaces und, seit PHP 8.1, Traits mit reinen Methoden. Ein Trait, der versucht, eine Property zu definieren, funktioniert in einem Enum ebenfalls nicht, denn die Beschränkung auf zustandslose Methoden gilt unabhängig davon, ob der Code direkt im Enum oder in einem eingebundenen Trait steht.
Ein weiterer Fallstrick betrifft Serialisierung: Ein json_encode() eines Backed Enums liefert standardmäßig nur den skalaren value, niemals ein Objekt mit den Ergebnissen der eigenen Methoden. Wer Label oder Farbe im JSON benötigt, muss das Enum zusätzlich JsonSerializable implementieren lassen und die Methode jsonSerialize() selbst schreiben, wie im Beispiel aus Abschnitt 4 gezeigt.
<?php
declare(strict_types=1);
enum Priority: int
{
case Low = 1;
case Medium = 2;
case High = 3;
// Allowed: methods via a trait, since traits may only add behavior
use ComparableTrait;
// NOT allowed: a property would be a parse error inside an enum
// public array $tags = [];
// NOT allowed: enums cannot extend another enum or a class
// enum Priority extends BasePriority { ... }
}
trait ComparableTrait
{
public function isHigherThan(self $other): bool
{
return $this->value > $other->value;
}
}
var_dump(Priority::High->isHigherThan(Priority::Low)); // true
10. Zusammenfassung
Enums mit Interfaces verwandeln eine reine Wertemenge in einen echten polymorphen Baustein. Statt Fallunterscheidungen über match($status->value) quer über die Codebasis zu verteilen, implementiert das Enum ein Interface und definiert die passende Methode direkt am Case. Neue Cases werden vom Compiler erzwungen, jede Interface-Methode zu implementieren, wodurch vergessene Fallunterscheidungen zu einem sofort sichtbaren Fehler statt zu einem stillen Bug werden.
Statische Methoden ergänzen Instanzmethoden für Fabrik-Logik und Lookup-Tabellen, mehrere Interfaces lassen sich kombinieren, und match($this) bleibt das zentrale Werkzeug für fallspezifisches Verhalten. Die Grenzen liegen bei zustandsbehafteten Anforderungen: Sobald ein Case eigene veränderliche Daten oder injizierte Abhängigkeiten braucht, ist eine klassische Strategy-Klasse die richtige Wahl statt eines Enums.
Enums mit Interfaces, eigenen Methoden - Das Wichtigste auf einen Blick
Interfaces implementieren
enum X implements Y funktioniert wie bei Klassen. Mehrere Interfaces lassen sich kombinieren, getrennt durch Komma.
Eigene Methoden
match($this) ist der zentrale Baustein für fallspezifisches Verhalten. Private Konstanten und Hilfsmethoden sind erlaubt.
Strategy-Pattern-Ersatz
Für zustandslose Varianten oft leichter als eine Klassenhierarchie. Cases sind Singletons ohne Instanziierungsaufwand.
Grenzen
Keine Instanzeigenschaften, keine Vererbung zwischen Enums. JsonSerializable nötig, um mehr als den Rohwert zu serialisieren.
11. FAQ: Enums mit Interfaces und eigenen Methoden
1Können Enums in PHP Properties haben?
2Kann ein Enum von einem anderen Enum erben?
3Methode im Enum vs. externe Hilfsfunktion?
4Mehrere Interfaces gleichzeitig implementieren?
5Interface-Methode auf einem Case aufrufen?
6Neuer Case, match() deckt ihn nicht ab?
7Enum als Interface-Typehint verwenden?
8Können Enums Traits verwenden?
9Performanter als klassische Klassenhierarchie?
10Enum mit Methoden zu JSON serialisieren?
Mironsoft
PHP-Architektur, Codequalität und Magento-Entwicklung
Enums mit Interfaces sauber im eigenen Projekt einsetzen?
Wir prüfen bestehenden PHP-Code auf verstreute match-Logik und ersetzen sie durch saubere Enums mit Interfaces und eigenen Methoden, mit vollständiger Typsicherheit und PHPStan-Absicherung auf Level 5 und höher.
Code-Review
Analyse verstreuter match-Blöcke und Vorschlag für Enum-Refactorings
Refactoring
Migration bestehender Statuswerte zu Enums mit Interfaces und Methoden
PHPStan-Absicherung
Statische Analyse auf Level 5 und höher für neue Enum-Strukturen