27 Enumerations

Mit PHP 8.1 wurden Enumerations (Enums) als ein neues Sprachelement eingeführt, das eine typsichere Möglichkeit bietet, eine Menge von benannten Konstanten zu definieren. Enums adressieren ein langjähriges Problem in PHP und bringen die Sprache näher an die modernen Typkonzepte anderer Programmiersprachen.

27.1 Grundlagen von Enumerations

Eine Enumeration ist eine spezielle Art von Klasse, die eine feste, begrenzte Menge von möglichen Werten definiert. Jeder dieser Werte ist eine benannte Konstante, die als Instanz der Enum-Klasse behandelt wird.

enum Status
{
    case Pending;
    case Active;
    case Suspended;
    case Cancelled;
}

function updateStatus(Status $newStatus): void
{
    // Verarbeite den Status...
    echo "Status aktualisiert auf: " . $newStatus->name;
}

// Verwendung der Enum
updateStatus(Status::Active);  // Status aktualisiert auf: Active

// Typeprüfung verhindert ungültige Werte
updateStatus("Active");  // TypeError: updateStatus(): Argument #1 ($newStatus) must be of type Status

27.2 Vorteile gegenüber traditionellen Konstantendefinitionen

Vor PHP 8.1 wurden Mengen von zusammengehörigen Konstanten typischerweise als Klassen- oder Interfacekonstanten definiert:

// Vor PHP 8.1
class StatusConstants
{
    public const PENDING = 'pending';
    public const ACTIVE = 'active';
    public const SUSPENDED = 'suspended';
    public const CANCELLED = 'cancelled';
}

function updateStatus(string $status): void
{
    // Manuelle Validierung erforderlich
    if (!in_array($status, [
        StatusConstants::PENDING,
        StatusConstants::ACTIVE,
        StatusConstants::SUSPENDED,
        StatusConstants::CANCELLED
    ])) {
        throw new InvalidArgumentException('Ungültiger Status');
    }
    
    // Status verarbeiten...
}

// Kann mit beliebigen Strings aufgerufen werden
updateStatus(StatusConstants::ACTIVE);  // Funktioniert
updateStatus('unknown');  // Fehler zur Laufzeit

Enumerations bieten gegenüber diesem Ansatz mehrere Vorteile:

  1. Typsicherheit: Der Typ wird vom Compiler überprüft, nicht erst zur Laufzeit
  2. IDE-Unterstützung: Bessere Autovervollständigung und Dokumentation
  3. Reduzierte Fehlerquellen: Kein Vertippens bei Zeichenketten möglich
  4. Selbstdokumentierender Code: Der Zweck und die möglichen Werte sind klar definiert
  5. Namensraumintegrität: Keine Kollisionen mit anderen Konstanten

27.3 Arten von Enumerations

PHP unterstützt zwei Arten von Enumerations:

27.3.1 1. Pure Enums (Unit Enums)

Pure Enums definieren nur die möglichen Fälle ohne zugeordnete Werte:

enum Direction
{
    case North;
    case East;
    case South;
    case West;
}

function move(Direction $direction): void
{
    match ($direction) {
        Direction::North => echo "Bewege nach Norden",
        Direction::East => echo "Bewege nach Osten",
        Direction::South => echo "Bewege nach Süden",
        Direction::West => echo "Bewege nach Westen",
    };
}

move(Direction::East);  // Bewege nach Osten

27.3.2 2. Backed Enums

Backed Enums ordnen jedem Fall einen primitiven Wert (string oder int) zu:

enum HttpStatus: int
{
    case OK = 200;
    case Created = 201;
    case BadRequest = 400;
    case Unauthorized = 401;
    case Forbidden = 403;
    case NotFound = 404;
    case ServerError = 500;
}

function handleResponse(HttpStatus $status): void
{
    if ($status->value >= 400) {
        echo "Fehler: HTTP-Status {$status->value}";
    } else {
        echo "Erfolg: HTTP-Status {$status->value}";
    }
}

handleResponse(HttpStatus::NotFound);  // Fehler: HTTP-Status 404

// Konvertierung von int zu Enum ist möglich
$status = HttpStatus::from(404);  // Gibt HttpStatus::NotFound zurück

Bei Backed Enums können Sie: - Mit ->value auf den zugrunde liegenden Wert zugreifen - from() verwenden, um einen primitiven Wert in den entsprechenden Enum-Fall zu konvertieren - tryFrom() verwenden, um einen Wert zu konvertieren, der möglicherweise nicht existiert (gibt null zurück, wenn kein passender Fall existiert)

27.4 Methoden und Eigenschaften in Enums

Enumerationen können wie Klassen Methoden und Eigenschaften enthalten:

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Authorized = 'authorized';
    case Completed = 'completed';
    case Failed = 'failed';
    case Refunded = 'refunded';

    // Konstante für alle Zustände
    public const FINAL_STATUSES = [
        self::Completed,
        self::Failed,
        self::Refunded
    ];
    
    // Methode zur Überprüfung, ob der Status endgültig ist
    public function isFinal(): bool
    {
        return in_array($this, self::FINAL_STATUSES);
    }
    
    // Statische Factory-Methode
    public static function fromDbValue(?string $value): ?self
    {
        return match ($value) {
            'PEND' => self::Pending,
            'AUTH' => self::Authorized,
            'COMP' => self::Completed,
            'FAIL' => self::Failed,
            'RFND' => self::Refunded,
            default => null
        };
    }
    
    // Anwendungsspezifische Logik
    public function getNextPossibleStatuses(): array
    {
        return match($this) {
            self::Pending => [self::Authorized, self::Failed],
            self::Authorized => [self::Completed, self::Failed, self::Refunded],
            self::Completed => [self::Refunded],
            default => []
        };
    }
}

// Verwendung der Methoden
$status = PaymentStatus::Authorized;

echo $status->value;  // 'authorized'

if ($status->isFinal()) {
    echo "Status kann nicht mehr geändert werden";
} else {
    echo "Mögliche nächste Status: ";
    foreach ($status->getNextPossibleStatuses() as $nextStatus) {
        echo $nextStatus->value . " ";
    }
}

27.5 Interfaces und Traits mit Enums

Enums können Interfaces implementieren und Traits verwenden:

interface Processable
{
    public function canProcess(): bool;
    public function getDescription(): string;
}

trait StatusColorTrait
{
    public function getColor(): string
    {
        return match($this) {
            OrderStatus::New => 'blue',
            OrderStatus::Processing => 'orange',
            OrderStatus::Shipped => 'purple',
            OrderStatus::Delivered => 'green',
            OrderStatus::Cancelled => 'red',
            OrderStatus::Returned => 'gray',
        };
    }
}

enum OrderStatus: string implements Processable
{
    use StatusColorTrait;
    
    case New = 'new';
    case Processing = 'processing';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
    case Cancelled = 'cancelled';
    case Returned = 'returned';
    
    public function canProcess(): bool
    {
        return match($this) {
            self::New, self::Processing => true,
            default => false
        };
    }
    
    public function getDescription(): string
    {
        return match($this) {
            self::New => 'Bestellung eingegangen',
            self::Processing => 'Bestellung wird bearbeitet',
            self::Shipped => 'Bestellung wurde versandt',
            self::Delivered => 'Bestellung wurde zugestellt',
            self::Cancelled => 'Bestellung wurde storniert',
            self::Returned => 'Bestellung wurde zurückgegeben'
        };
    }
}

// Verwendung der Interface-Methoden und Traits
$status = OrderStatus::Shipped;

echo "Status: " . $status->getDescription() . "\n";
echo "Farbe: " . $status->getColor() . "\n";

if ($status->canProcess()) {
    echo "Status kann noch verarbeitet werden";
} else {
    echo "Status kann nicht mehr verarbeitet werden";
}

27.6 Praktische Anwendungsfälle

27.6.1 Zustandsautomaten (State Machines)

Enums eignen sich hervorragend für die Implementierung von Zustandsautomaten:

enum DocumentState: string
{
    case Draft = 'draft';
    case Review = 'review';
    case Approved = 'approved';
    case Published = 'published';
    case Archived = 'archived';
    
    public function canTransitionTo(self $target): bool
    {
        return match ($this) {
            self::Draft => in_array($target, [self::Review]),
            self::Review => in_array($target, [self::Draft, self::Approved]),
            self::Approved => in_array($target, [self::Review, self::Published]),
            self::Published => in_array($target, [self::Archived]),
            self::Archived => false
        };
    }
    
    public static function getInitialState(): self
    {
        return self::Draft;
    }
}

class Document
{
    private DocumentState $state;
    
    public function __construct()
    {
        $this->state = DocumentState::getInitialState();
    }
    
    public function getState(): DocumentState
    {
        return $this->state;
    }
    
    public function transitionTo(DocumentState $newState): bool
    {
        if (!$this->state->canTransitionTo($newState)) {
            throw new InvalidArgumentException(
                "Kann nicht von {$this->state->value} zu {$newState->value} wechseln"
            );
        }
        
        $this->state = $newState;
        return true;
    }
}

// Verwendung des Zustandsautomaten
$doc = new Document();
echo "Initialer Status: " . $doc->getState()->value . "\n";

$doc->transitionTo(DocumentState::Review);
echo "Neuer Status: " . $doc->getState()->value . "\n";

// Dieser Übergang würde fehlschlagen
try {
    $doc->transitionTo(DocumentState::Published);
} catch (InvalidArgumentException $e) {
    echo "Fehler: " . $e->getMessage() . "\n";
}

27.6.2 Validierung und Typumwandlung

Backed Enums bieten eingebaute Validierung für eingehende Daten:

enum UserRole: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Author = 'author';
    case Subscriber = 'subscriber';
    
    public function hasPermission(string $permission): bool
    {
        $rolePermissions = [
            self::Admin->value => ['create', 'read', 'update', 'delete', 'publish', 'manage_users'],
            self::Editor->value => ['create', 'read', 'update', 'delete', 'publish'],
            self::Author->value => ['create', 'read', 'update'],
            self::Subscriber->value => ['read']
        ];
        
        return in_array($permission, $rolePermissions[$this->value]);
    }
}

function assignRole(User $user, string $roleName): void
{
    // Sichere Konvertierung mit Validierung
    $role = UserRole::tryFrom($roleName);
    
    if ($role === null) {
        throw new InvalidArgumentException("Ungültige Rolle: $roleName");
    }
    
    $user->setRole($role);
    
    // Wir können jetzt typsicher mit der Rolle arbeiten
    if ($role->hasPermission('manage_users')) {
        echo "Benutzer hat Admin-Rechte erhalten";
    }
}

27.6.3 Konfigurationsoptionen

Enums sind ideal für die Definition von Konfigurationsoptionen:

enum LogLevel: string
{
    case Debug = 'debug';
    case Info = 'info';
    case Warning = 'warning';
    case Error = 'error';
    case Critical = 'critical';
    
    public function shouldLog(self $minimumLevel): bool
    {
        $levels = [
            self::Debug->value => 0,
            self::Info->value => 1,
            self::Warning->value => 2,
            self::Error->value => 3,
            self::Critical->value => 4
        ];
        
        return $levels[$this->value] >= $levels[$minimumLevel->value];
    }
}

class Logger
{
    private LogLevel $minimumLevel;
    
    public function __construct(LogLevel $minimumLevel = LogLevel::Info)
    {
        $this->minimumLevel = $minimumLevel;
    }
    
    public function log(LogLevel $level, string $message): void
    {
        if ($level->shouldLog($this->minimumLevel)) {
            echo "[$level->value] $message\n";
        }
    }
}

// Verwendung des Loggers
$logger = new Logger(LogLevel::Warning);

$logger->log(LogLevel::Debug, "Dies wird nicht angezeigt");
$logger->log(LogLevel::Info, "Dies wird nicht angezeigt");
$logger->log(LogLevel::Warning, "Dies wird angezeigt");
$logger->log(LogLevel::Error, "Dies wird angezeigt");

27.7 Enums in Datenbankoperationen

Enums lassen sich gut mit Datenbanken kombinieren:

enum SubscriptionType: string
{
    case Free = 'free';
    case Basic = 'basic';
    case Premium = 'premium';
    case Enterprise = 'enterprise';
    
    public function getMonthlyPrice(): int
    {
        return match($this) {
            self::Free => 0,
            self::Basic => 999,      // 9.99€
            self::Premium => 1999,   // 19.99€
            self::Enterprise => 4999, // 49.99€
        };
    }
    
    public function hasFeature(string $feature): bool
    {
        $features = [
            self::Free->value => ['basic_content'],
            self::Basic->value => ['basic_content', 'full_content', 'download'],
            self::Premium->value => ['basic_content', 'full_content', 'download', 'offline', 'priority_support'],
            self::Enterprise->value => ['basic_content', 'full_content', 'download', 'offline', 'priority_support', 'white_label', 'api_access'],
        ];
        
        return in_array($feature, $features[$this->value]);
    }
}

class SubscriptionRepository
{
    private PDO $pdo;
    
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    
    public function findByUser(int $userId): ?Subscription
    {
        $stmt = $this->pdo->prepare("SELECT id, user_id, type, status, expires_at FROM subscriptions WHERE user_id = :user_id");
        $stmt->execute(['user_id' => $userId]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$data) {
            return null;
        }
        
        return new Subscription(
            $data['id'],
            $data['user_id'],
            // Konvertiere den String aus der Datenbank in einen Enum
            SubscriptionType::from($data['type']),
            // Weitere Felder...
        );
    }
    
    public function save(Subscription $subscription): void
    {
        $stmt = $this->pdo->prepare("
            INSERT INTO subscriptions (user_id, type, status, expires_at) 
            VALUES (:user_id, :type, :status, :expires_at)
            ON DUPLICATE KEY UPDATE type = :type, status = :status, expires_at = :expires_at
        ");
        
        $stmt->execute([
            'user_id' => $subscription->getUserId(),
            'type' => $subscription->getType()->value, // Speichere den Enum als String
            // Weitere Felder...
        ]);
    }
}

27.8 Nützliche Methoden für die Arbeit mit Enums

PHP bietet einige eingebaute Funktionen zur Arbeit mit Enumerationen:

enum Size
{
    case Small;
    case Medium;
    case Large;
    case ExtraLarge;
}

// Alle Fälle einer Enumeration auflisten
$cases = Size::cases();
foreach ($cases as $case) {
    echo $case->name . "\n";
}

// Namen eines Enum-Falles abrufen
$size = Size::Medium;
echo $size->name;  // "Medium"

// Mit Strings vergleichen (falls notwendig)
$userInput = "Medium";
$selectedSize = null;

foreach (Size::cases() as $size) {
    if ($size->name === $userInput) {
        $selectedSize = $size;
        break;
    }
}

// Bessere Methode mit einer statischen Hilfsmethode
enum Size
{
    case Small;
    case Medium;
    case Large;
    case ExtraLarge;
    
    public static function fromName(string $name): ?self
    {
        foreach (self::cases() as $case) {
            if ($case->name === $name) {
                return $case;
            }
        }
        return null;
    }
}

$selectedSize = Size::fromName($userInput);

27.9 Best Practices und Tipps

  1. Aussagekräftige Namen verwenden: Wählen Sie für Enums und ihre Fälle Namen, die klar und selbsterklärend sind.

  2. Dokumentation hinzufügen: Nutzen Sie PHPDoc, um Ihre Enums zu dokumentieren, besonders wenn sie komplexe Zustände oder Geschäftsregeln repräsentieren.

    /**
     * Repräsentiert den Status einer Bestellung im System.
     */
    enum OrderStatus: string
    {
        /**
         * Die Bestellung wurde gerade erstellt, aber noch nicht bezahlt.
         */
        case New = 'new';
    
        /**
         * Die Bezahlung wurde erfolgreich verarbeitet.
         */
        case Paid = 'paid';
    
        // ...
    }
  3. Vermeiden Sie zu viele Fälle: Eine Enumeration mit zu vielen Fällen kann schwer zu verwalten sein. Wenn Sie mehr als etwa 10-15 Fälle haben, überlegen Sie, ob Sie die Enum aufteilen oder einen anderen Ansatz verwenden können.

  4. Nutzen Sie Backed Enums für Datenbankinteraktion: Wenn Ihre Enums mit Datenbanken interagieren, verwenden Sie vorzugsweise Backed Enums, um die Zuordnung zu Datenbankwerten zu erleichtern.

  5. Verwenden Sie Enums statt Konstanten für verwandte Werte: Wenn Sie eine Gruppe von zusammengehörigen Konstanten haben, verwenden Sie Enums anstelle von Klassenkonstanten, um Typsicherheit zu gewährleisten.

27.10 Einschränkungen von Enums

  1. Keine dynamische Erzeugung: Enumerationsfälle müssen zur Kompilierzeit definiert werden und können nicht dynamisch zur Laufzeit erstellt werden.

  2. Keine Vererbung zwischen Enums: Eine Enumeration kann nicht von einer anderen Enumeration erben.

  3. Keine Klassen als Backed-Type: Nur primitive Typen (int, string) können als Backing-Types verwendet werden.

  4. Keine instanziierbaren Enums: Es ist nicht möglich, Enums mit new zu instanziieren. Sie können nur auf die vordefinierten Fälle zugreifen.