PHP 8.1: Enumerations

Artikel geschrieben am 29.01.2022 um 20:45 Uhr.

Die neue Enum-Definition, die PHP im Kern erhält, sorgt für eine bessere Struktur im Code. War es doch bisher notwendig, dass jeder Entwickler sich seine eigenen Enum-Klassen implementieren musste.
Der neue Standard ist damit eine sinnvolle Ergänzung, gerade wenn man vorhandene Projekte sinnvoll umstellt oder ganz neue Projekte beginnen möchte.

Der Syntax ist auf den ersten Blick ungewöhnlich, doch werfen wir einen Blick darauf. Als Beispiel nehmen wir den Status eines Newsletters. Der ist standardmäßig im Entwurf, geht danach in die Warteschlange und mit einem freien Slot oder zu einem bestimmten Zeitpunkt beginnt gewöhnlich der Versand. Nach dem Versand ist er allgemein beendet und geht nach einer gewissen Aufbewahrungszeit in das Archiv über.

enum NewsletterEMailStatus
{
	case ENTWURF;
	case WARTESCHLANGE;
	case VERSAND;
	case BEENDET;
	case ARCHIV;
}

Der Enum-Typ kann wie eine Klasse überall als Typdeklaration genutzt werden, z.B. bei setzeStatus( NewsletterEMailStatus $Status ). Wichtig zu wissen ist, dass z.B. ENTWURF einem Singleton-Objekt entspricht. Diese sind darüber hinaus nur lesbar und nicht veränderbar!
Entsprechend wird ein strikter Vergleich, bei der Verwendung von zwei gleichen Enum-Werten, auch immer erfolgreich sein. Im Gegensatz zu den üblichen Klassen-Implementierungen, die man bisher oft gesehen hat, kann es hier nicht unterschiedliche "gleiche" Instanzen von einem Wert geben. Um die Existenz zu prüfen gibt es extra die neue Funktion \enum_exists( string $enum, bool $autoload = true ), analog zur schon bekannten Funktion class_exists() für Klassen.
Die case-Bezeichner werden intern wie Konstanten behandelt, d.h. ein Zugriff über static::BEZEICHNER ist jederzeit möglich.

Bei allen Instanzen kann auf das Attribut ->name zugegriffen werden. Dies gibt einem den Namen / Bezeichner innerhalb der Deklaration zurück, z.B. $Status = ENUM::ENTWURF; echo $Status->name; // string(7) "ENTWURF". Außerdem gibt es noch die statische Methode ENUM::cases();, die alle möglichen Werte zurückliefert. Für unser Beispiel würde das Ergebnis mit var_dump() wie folgt aussehen:

array(5) {
  [0]=>
  enum(NewsletterEMailStatus::ENTWURF)
  [1]=>
  enum(NewsletterEMailStatus::WARTESCHLANGE)
  [2]=>
  enum(NewsletterEMailStatus::VERSAND)
  [3]=>
  enum(NewsletterEMailStatus::BEENDET)
  [4]=>
  enum(NewsletterEMailStatus::ARCHIV)
}

Im Beispiel hat der ENTWURF nicht automatisch den Wert '0'! Ein wichtiger Punkt für die Nutzung, den man sich merken sollte. Natürlich lässt sich der Enum-Typ entsprechend erweitern. Nehmen wir an, wir wollen diesen Eigenschaften einen Wert zuweisen, den wir auch in einer Datenbank später speichern können. Die Möglichkeiten sind allerdings auf int und string beschränkt. Es ist nicht möglich die Typen innerhalb zu mischen. Man muss sich also für einen entscheiden. Diese Deklaration wird Backed Enum genannt, da die einzelnen Eigenschaften als Backed Case bezeichnet werden und einen Wert beinhalten.

enum NewsletterEMailStatus: string
{
	case ENTWURF = "entwurf";
	case WARTESCHLANGE = "warteschlange";
	case VERSAND = "versand";
	case BEENDET = "beendet";
	case ARCHIV = "archiv";
}

Damit dieser Wert ausgelesen werden kann gibt es eine Eigenschaft value die direkt \NewsletterEMailStatus::ENTWURF->value oder über eine Variable (Instanz) $Status->value zugegriffen wird. (In unserem Beispiel wäre die Rückgabe entwurf)
Diese Varianten besitzen auch zwei zusätzliche Methoden, um das entsprechende Singleton-Objekt anhand eines Wertes zu erhalten. Die Methoden-Signaturen sind from( int|string $value ) : static und tryFrom( int|string $value ) : ?static. Beide würden versuchen den Wert entwurf innerhalb des Enums zu suchen und das entsprechende Objekt zurückzugeben. Im Fall von ::from() erzeugt das einen ValueError, der wirklich geworfen wird, im Fall von tryFrom() würde der gleiche Versuch in einem NULL-Wert enden. Je nach aktuellem Bedarf kann ausgewählt werden, welche Variante für den Programmablauf besser ist.

Interfaces

Ein Enum kann beliebig viele Interfaces als Abhängigkeit besitzen und muss dessen Methoden implementieren. Diese können selbstverständlich auch bei jeder Instanz aufgerufen werden. Auch die Deklaration und Verwendung von statischen Methoden sind erlaubt, genauso wie gewöhnliche Klassenkonstanten. Traits sind erlaubt, dürfen aber keine Klassenvariablen definiert haben. Ansonsten endet die Ausführung in einem fatal error.