Im PHP verwenden wir oft Konstanten, mehr als es uns lieb ist. Das ist aber ein Thema für einen anderen Artikel. Hier geht es eigentlich um definieren von Konstanten in einer Klasse und ums Prüfen dessen Gültigkeit. Ich habe diverse Artikel im Internet gefunden die dasselbe Thema bearbeiten und diverse Lösungen anbieten. Dabei habe ich Enum-Generatoren, Enum-Emulatoren so wie Micro-Frameworks für Enums gefunden und ausprobiert. Mal Ehrlich – das war mir viel zu kompliziert! Keine Frage – alle funktionieren ganz gut. Jedoch muss es doch viel einfacher gehen und auch mit viel weniger Code-Logic. Hier habe ich vier Lösungs-Beispiele aufgelistet – bin gespannt was ihr davon haltet. Wie implementiert ihr Enums?
Aufgabe
Gewollt ist ein UniversalClient der zwei Handler (JSON, SOAP) anbietet. Beide Handler sollen nur über Konstanten bzw. konstanten Werte gesetzt werden sollen. Wenn der Handler-Wert sich nicht innerhalb der Enumeration befindet, dann soll ein Fehler/Exception geworfen werden.
Die old school Variante
Beim setzen des Handler-Wertes wird geschaut ob der Wert stimmt. Das ist OK, aber für mich trotzdem viel Logic und wartungsbedürftig. Da neue Konstanten in der „setHandler(…)“ Methode nachgepflegt werden müssen.
< ?php class UniversalClient { const HANDLER_JSON = 'json'; const HANDLER_SOAP = 'soap'; /** * @var string */ protected $handler; /** * @param integer $handler A enumeration of UniversalClient * @throws UnexpectedValueException If invalid handler given. */ public function setHandler($handler) { if (!in_array($handler, array(self::HANDLER_JSON, self::HANDLER_SOAP), true)) { throw new UnexpectedValueException('Invalid data handler:' . $handler); } $this->handler = $handler; } } // TEST $client = new UniversalClient(); $client->setHandler('JSON'); // throws UnexpectedValueException $client->setHandler(UniversalClient::HANDLER_JSON); // throws NO UnexpectedValueException $client->setHandler('json'); // throws NO UnexpectedValueException
Die new school Variante
Wenn man die SPL extension SplTypes instaliert hat, kann man die SPL Klasse SplEnum einfach erweitern. Danach kann man seine Enumerations erstellen sowie eine default Enumeration bestimmen. Die SplEnum Klasse kümmert sich dann für das Prüfen der Werte und wirft eine Exception. Nichts desto trotz, diese dicke Warnung steht auf der SplTypes Seite bei php.net
„Diese Erweiterung ist EXPERIMENTELL. Das Verhalten dieser Erweiterung, einschließlich der Funktionsnamen, und alles Andere, was hier dokumentiert ist, kann sich in zukünftigen PHP-Versionen ohne Ankündigung ändern. Seien Sie gewarnt und verwenden Sie diese Erweiterung auf eigenes Risiko.“
Nach dem ich das gelesen habe, war die Entscheidung klar – ich werde es leider nicht für immer implementiert haben können. So habe ich nach weiteren Lösungsansätzen gesucht.
< ?php class HandlerType extends SplEnum { const __default = self::SOAP; const JSON = 'json'; const SOAP = 'soap'; } class UniversalClient { /** * @var string */ protected $handler; /** * @param HandlerType $handler */ public function setType(HandlerType $handler) { $this->handler = $handler; } } // TEST $client = new UniversalClient(); $client->setHandler(new HandlerType(3)); // throws UnexpectedValueException $client->setHandler(new HandlerType('JSON')); // throws UnexpectedValueException $client->setHandler(new HandlerType('json')); // throws NO UnexpectedValueException $client->setHandler(new HandlerType(HandlerType::JSON)); // throws NO UnexpectedValueException
Die Dependency Injection Variante
Nun haben wir es hier nicht direkt mit Klassen Konstanten zu tun, sondern mit einer Kollektion von Objekten, die ein bestimmtes Interface implementieren. Es werden also nur Handler Objekte akzeptiert die das Interface implementieren. Somit haben wir auch eine indirekte Enumeration.
< ?php interface Handler { /** * @abstract * @return string */ public function getType(); } class SoapHandler implements Handler { /** * @return string */ public function getType() { return 'soap'; } } class JsonHandler implements Handler { /** * @return string */ public function getType() { return 'json'; } } class UniversalClient { /** * @var string */ protected $handler; /** * @param Handler $handler */ public function setHandler(Handler $handler) { $this->handler = $handler->getType(); } } // TEST $client = new UniversalClient(); // PHP Catchable fatal error: // Argument 1 passed to UniversalClient::setHandler() must implement interface Handler $client->setHandler('json'); $client->setHandler(new SoapHandler()); // throws NO UnexpectedValueException $client->setHandler(new JsonHandler()); // throws NO UnexpectedValueException
Die Reflection API Variante
Kann sein, dass diese Lösung komplizierter ist. Aber was passiert hier. Mittels der Reflaction API wird geprüft ob eine Klasse die Konstante besitzt und liefert dann dessen Wert zurück. Wenn die Konstante nicht innerhalb der Klasse ist – gibt es eine Exception.
< ?php class Util { /** * Converts a value to enum handler. * * This method checks if the value is of the specified enumerable handler. * A value is a valid enumerable value if it is equal to the name of a constant * in the specified enumerable handler (class). * * @param string $value Enumerable value to be checked. * @param string $enumType Enumerable class name. * @return string Valid enumeration value * @throws UnexpectedValueException If the value is not a valid enumerable value. */ public static function ensureEnum($value, $enumType) { static $types = array(); if (!isset($types[$enumType])) { $types[$enumType] = new ReflectionClass($enumType); } if ($types[$enumType]->hasConstant($value)) { return $value; } throw new UnexpectedValueException( 'Invalid enumerable value "'.$value.'". '. 'Please make sure it is among (' .implode(', ', $types[$enumType]->getConstants()).').' ); } } class UniversalClient { const JSON = 'json'; const SOAP = 'soap'; /** * @var string */ protected $handler; /** * @param integer $handler A enumeration of UniversalClient */ public function setHandler($handler) { $this->handler = Util::ensureEnum(strtoupper($handler), get_class($this)); } } // TEST $client = new UniversalClient(); $client->setHandler('php'); // throws UnexpectedValueException $client->setHandler(UniversalClient::JSON); // throws No UnexpectedValueException $client->setHandler('json'); // throws No UnexpectedValueException
Die SPL-Version sieht interessant aus. Das Beispiel würde sich doch aber auch relativ einfach nachbauen lassen, ohne Experimentelle SPL-Dependency. Also eine abstrakte Klasse die im Constructer die class.constants checkt und eine Exception wirft. Oder übersehe ich hier irgendwas?
Eine PHP Implementierung von SplEnum gibt es auf GitHub: https://github.com/headzoo/php-enum
„Jedoch muss es doch viel einfacher gehen und auch mit viel weniger Code-Logic.“
Alle die hier aufgeführten Implementierungen sind m.E. unzureichend, da sie gegenüber dem „int Enum“ Muster keine Vorteile besitzen (sondern lediglich mehr Abstraktion einführen). Daher sollte eine Enum-Implementierung in einer objektorientierten Programmiersprache typsicher sein („Typesafe Enum“ Muster). Vgl. http://docs.oracle.com/javase/1.5.0/docs/guide/language/enums.html
Schamlose Eigenwerbung: https://github.com/FlorianWolters/PHP-Component-Core-Enum
Es gibt aber auch viele andere Implementierungen, die einen etwas anderen Ansatz verfolgen.
Also eine Typen-Sicherheit ist nur in zwei Variante gewährleistet. In der „old school Variante“ wird durch den dritten Parameter in der Funktion in_array(…) gewährleistet, dass der entsprechende Wert und Typ übergeben wird. In der „Dependency Injection Variante“ da gewährleistet das Interface, dass nur ein bestimmtes Typ von Instanz übergeben werden kann. Gut, in der „Reflection API Variante“ da wird nicht nach dem Typ geschaut, jedoch wird es geschaut ob die Konstante in der gegebenen Klasse sich befindet. Nicht desto, die PHP Implementierung von SplEnum von „headzoo“ auf GitHub schmeichelt mir am meisten.
Eine vereinfachte Variante, die dafür mindestens genauso effektiv ist:
https://github.com/marc-mabe/php-enum