Enums in PHP

Posted by in Pattern, PHP

Durch den Artikel von Daniel inspiriert habe ich auch mal etwas mit Enums herumgespielt. Aber eins nach dem anderen…

Einige kennen Enums eventuell schon von Java. Sie sind eine Art objektorientierte Konstanten. Auf diese Weise kann man einer Methode typsichere Konstanten übergeben.

Hier mal ein Beispiel wie das sonst in der Regel aussieht:

class User
{
    public function setStatus($status)
    {
        // ....
    }
}

Der Nachteil ist ganz klar. Man kann jeden beliebigen Status übergeben. Also auch Zahlen oder Arrays. Und hier kommen die Enums ins Spiel. Wir definieren also eine Statusklasse:

class UserStatus extends CustomEnum
{
    const ACTIVE = 1;
    const NOTACTIVE = 2;
    const DELETED = 3;
}

Und damit können wir dem User nun einen OO-Status übergeben:

$user = new User();
$user->setStatus(UserStatus::ACTIVE());

Was mir an diesem Ansatz besonders gefällt ist, daß die Konstante noch als Konstante erkennbar ist und man auch die Unterstützung der IDE nutzen kann. Man muss lediglich die Klammern hinter die Konstante schreiben, da wir dafür eine Funktion verwenden. Auch die Schreibweise ist eher PHP-Style und damit intuitiver.

So dann will ich auch mal die aufgepeppte User-Klasse zeigen:

class User
{
    private $_status = null;
 
    public function setStatus(UserStatus $status)
    {
        $this->_status = $status;
 
        // Das hier ist nur Optional und um zu zeigen wie man den Status
        // auswerten kann
        switch ($status->ordinal()) {
            case UserStatus::ACTIVE()->ordinal():
                echo 'active';
                break;
            case UserStatus::NOTACTIVE()->ordinal():
                echo 'notactive';
                break;
            case UserStatus::DELETED()->ordinal():
                echo 'deleted';
                break;
        }
 
        echo($status->equals(UserStatus::ACTIVE())) ? 'active' : 'other';
    }
}

Die Namen für die Methoden der Enumklasse habe ich dabei der Java-Enumklassen entnommen.

So und nun kommen wir zum spannenden Teil. Wie sieht die CustomEnum Klasse eigentlich aus?

Den Konstruktor setzen wir private da die Klasse nur über die Konstantenmethoden instanziiert werden soll.
Ebenso lesen wir beim Erstellen der Klasse die Konstanten aus, da wir sie dann brauchen.

    private function __construct()
    {
        $rc = new ReflectionClass($this);
        $this->_constants = $rc->getConstants();
    }

Die wichtigste Arbeit geschieht in der magischen Methode __callstatic(). Dort erzeugen wir ein neues Objekt und weisen ihm den Wert der Methode zu, was ja die Konstante repräsentiert.

    public static function __callstatic($method, $args)
    {
        $class = get_called_class();
        $enum = new $class();
 
        return $enum->_set($method);
    }

In der Set-Methode weisen wir den Wert zu (wer hätte das gedacht) und prüfen ob dieser Wert überhaupt gültig ist. Wenn nicht werfen wir eine Exception. Somit fallen fehlende Konstanten auch recht schnell auf.

    private function _set($index)
    {
        $this->_status = strtoupper($index);
        if (!isset($this->_constants[$this->_status])) {
            throw new UnexpectedValueException($this->_status . ' is not a valid value');
        }
        return $this;
    }

Das war auch schon fast die ganze Magie. Die Java-Klasse bietet noch die statische Methode values() welche alle verfügbaren Enumdefinitionen als Liste zurück gibt um darüber zu iterieren. Dafür müssen wir einen kleinen Kunstgriff machen. Wir erzeugen eine Klasse, damit wir alle Konstanten auslesen können. Für jede Konstante erzeugen wir noch ein Objekt und geben diese Liste zurück. Da wir den Namen der Klasse nur in einer Variablen haben, können wir an dieser Stelle nicht die statische Create-Methode nutzen. Deshalb müssen wir den Inhalt der __callstatic() in die __call() kopieren. Das finde ich zwar sehr unschön, aber mir ist keine bessere Lösung eingefallen.

    public static function values()
    {
        $list = array();
        $class = get_called_class();
        $enum = new $class();
 
        foreach ($enum->_constants as $name => $ordinal) {
            $list[] = $enum->$name();
        }
        return $list;
    }
 
    public function __call($method, $args)
    {
        $class = get_called_class();
        $enum = new $class();
 
        return $enum->_set($method);
    }

Die restlichen Methoden sind ja eher trivial. Und hier nun nochmal die gesamte Klasse in voller Schönheit.

abstract class CustomEnum
{
    /**
     * @var array
     */
    private $_constants = null;
 
    /**
     * @var string
     */
    private $_name;
 
    /**
     * Konstruktor soll nicht öffentlich aufgerufen werden.
     */
    private function __construct()
    {
        $rc = new ReflectionClass($this);
        $this->_constants = $rc->getConstants();
    }
 
    /**
     * Quasi der Konstruktor.
     *
     * @param string $method
     * @param mixed $args
     * @return CustomEnum
     */
    public static function __callstatic($method, $args)
    {
        $class = get_called_class();
        $enum = new $class();
 
        return $enum->_set($method);
    }
 
    public function __call($method, $args)
    {
        $class = get_called_class();
        $enum = new $class();
 
        return $enum->_set($method);
    }
 
    public function ordinal()
    {
        return $this->_constants[$this->_status];
    }
 
    public function name()
    {
        return (string) $this->_status;
    }
 
    public static function values()
    {
        $list = array();
        $class = get_called_class();
        $enum = new $class();
 
        foreach ($enum->_constants as $name => $ordinal) {
            $list[] = $enum->$name();
        }
        return $list;
    }
 
    public function __toString()
    {
        return $this->name();
    }
 
    private function _set($index)
    {
        $this->_status = strtoupper($index);
        if (!isset($this->_constants[$this->_status])) {
            throw new UnexpectedValueException($this->_status . ' is not a valid value');
        }
        return $this;
    }
 
    public function equals(CustomEnum $enum)
    {
        return $enum->_status == $this->_status && get_class($enum) == get_class($this);
    }
}

Diese Implementierung funktioniert jedoch nur ab PHP 5.3, weil erst dort die __callstatic() Methode eingeführt wurde.