Enums in 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.
Auch hier möchte ich wie im Ebene7-Blog noch einmal auf meine Implementierung hinweisen, die übrigens genau den selben Gedanken wie deiner verfolgt (Klassenmethoden als Konstanten-Ersatz), aber ohne Reflections und ohne __callStatic auskommt (PHP 5.2 kompatibel):
http://www.phpclasses.org/browse/package/6021.html
Enum::define('UserStatus','ACTIVE','NOTACTIVE','DELETED');
$user->setStatus(UserStatus::ACTIVE());
Verglichen werden die Enum-Instanzen dann direkt mit dem === Operator
@Fabian ich hab mir mal deine Implementierung grob angeschaut. Dein Ansatz ist noch eher der von Java, du erzeugst direkt Quellcode, welcher die Klasse definiert, schreibst den Code in eine Datei und includest diese dann. Bei Java wird die Enumklasse ja intern auch erst durch den Compiler erzeugt. Dies wird durch deine Klasse ebenso bewerkstelligt. Es ist im Grunde eher eine Art Codegenerator.
Der Vorteil ist in der Tat, dass man die Klassen direkt mit dem === Operator vergleichen kann und das ganze auch PHP 5.2 Kompatibel ist. Nachteil ist, dass die Dateien mit den Klassen erst mal ins Dateisystem geschrieben und dann included werden (was aber auch schön gecached wird).
Wobei ich persönlich nicht so der Freund von zuviel Magic und impliziter Logik bin. Andererseits kann man es auch einfach als Blackbox ansehen, die man dafür sehr bequem nutzen kann.