Enums in PHP (sicher ist sicher)

Wer hin und wieder auch mal in anderen Sprachen wie z.B. Java programmiert, dem werden Enums sicher nicht fremd sein. Wäre es nicht schön, ähnliches auch unter PHP zur Verfügung zu haben, um nicht jeden Status als String oder Int zu übergeben? Tja, das Leben ist aber kein Ponyhof und wenn PHP das nicht anbietet, dann schaffen wir uns eben eine eigene Lösung um besser und (typ-)sicherer zu programmieren.

Wir kennen es sicher alle: Wir haben irgendein Objekt und wollen dessen Status oder einen bestimmten Wert setzen oder abfragen. Was passiert sicher in 95% aller Fälle? Es wird ein String- oder Integer-Wert gesetzt oder verglichen. Aus meiner Sicht nicht besonders elegant und erst recht nicht sicher, da der Wert unnötig oft überprüft werden muss.

Mit Enums haben wir den Vorteil, dass wir bestimmte Werte vorgeben können und auch nur diese zu Verfügung stehen. Da wir unter PHP nur “normale” Klassen verwenden können, haben wir dadurch immer automatisch einen bestimmten Typen.

Wir wollen nun den Status eines Benutzers in unserem System definieren und legen uns dazu folgende Klasse mit den erforderlichen Werten an.

<?php
class UserStatus extends Custom_Enum
{
  const ACTIVE    = 1;
  const NOTACTIVE = 2;
  const DELETED   = 3;
}

Damit können wir unseren Benutzern einen eindeutigen Status setzen.

<?php
class User
{
  public function setStatus(UserStatus $status)
  {
    // ..
  }
}

$user = new User();

$status = new UserStatus();
$user->setStatus($status->active());

Durch das Type-Hinting können wir schon bei der Parameterübergabe den richtigen Objekttypen sicherstellen und müssen darauf später nicht mehr extra prüfen.

Die Klasse Custom_Enum funktioniert mit Reflektion und etwas PHP-Magie und ist bislang erst nur ein grober Entwurf. Ein paar Überprüfungen und eine bessere Fehlerbehandlung wären dann noch notwendige Maßnahmen.

<?php
abstract class Custom_Enum
{
  private $_constants;  // array
  private $_index;  // string

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

  public function __call($method, $args)
  {
    if (preg_match('/^is([a-zA-Z]*?)$/', $method, $matches)) {
      return $this->is($matches[1]);
    }
    $this->set($method);
    return $this;
  }

  public function __toString()
  {
    return (string)$this->getValue();
  }

  public function set($index)
  {
    $this->_index = strtoupper($index);
    return $this;
  }

  public function is($index)
  {
    return strtoupper($index) == $this->_index;
  }

  public function getValue()
  {
    return $this->_constants[$this->_index];
  }
}

Ich finde das schon wesentlich besser als lose String- oder Integerwerte auszutauschen, aber vielleicht geht es ja noch besser und sicherer? Eure Ideen, Kritik, Fragen oder was auch immer sind dann in den Kommentaren gut aufgehoben. Also keine Scheu und diskutiert mit! ;-)

19 Kommentare

  1. Hi,
    danke für den schönen Artikel. Ich hätte allerdings noch eine Frage: Was spricht denn dagegen das ganze nur mir Klassenkonstanten zu machen? Also z.B.

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

    Und dann die Parameterübergabe folgenermaßen aussehen zu lassen:

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

    In einem switch/case könne dann auf wieder auf diese Konstante geprüft werden. So verschwinden die Integer und Strings ebenfalls.

    Einzig mit dem Typehinting wird das dann vermutlich nichts.
    Wie seht ihr das?

    Schöne Grüße,
    Robert Vogel

  2. Die Implementierung sieht auf den ersten Blick ganz nett aus, müsste ich mal mit rumspielen, um sagen zu können ob ich etwas vermisse.

    @Robert: Das Type Hinting finde ich persönlich wichtig, da UserStatus::ACTIVE genauso 1 ist wie 1 oder bspw UserRight::READABLE (falls das existiert).

    Imho könnte man Enums in die PHP Sprachdefinition aufnehmen, aber das ist eine subjektive Ansicht.

  3. @Robert: Durch die Klassenkonstanten hast du zum einen wieder direkte Abhängigkeiten in deinem Code und auch weniger Kontrolle, falls du z.B. eine Exception werfen willst, wenn ein falscher Wert gesetzt werden soll. Und wie auch Norbert schon schrieb, hätte man ja durch eine Konstante letztlich auch nur wieder einen String oder Integer übergeben.

  4. Hallo,

    über dieses Thema habe ich mir auch schon lange den Kopf zerbrochen und eine Lösung gefunden, die ein Mix aus dem Vorgestellten und dem von @Robert sind.

    Ich habe das mal auf meinen Server hochgeladen:
    http://labor.corenergy.de/enum.zip
    –> Enthalten ist eine enum.php

    Für die Funktionalität müssen die Konstanten jedoch unbedingt vom Typ string sein. Dafür fühlt es sich fast wie ein echtes Enum an. (Meiner Meinung nach :) )

  5. es gibt eine pecl-implementierung zu datentypen u.a. auch Enum.
    –> http://php.net/manual/en/splenum.construct.php

    Das Standard-beispiel ist schlecht. Aber in den Comments findet man ein gutes Beispiel.

    Die extension muss aber manuell installiert werden (gehört also nicht zum Standard-Umfang)

  6. noch ein hinweis.
    Die extension ist z.Z. noch im alpha-stadium. d.h. es kann sich jederzeit was ändern.

  7. Zwei Beispiele, wie dies in verschiedenen Frameworks gelöst ist:
    http://stubbles.net/wiki/Docs/Enums
    http://docs.xp-framework.net/xml/doc?core/enum

    Beide Implementierungen funktionieren mit PHP 5.2 – würde man nur noch 5.3 voraussetzen ließen sich da noch zahlreiche Verbesserungen unter Nutzung der neuen 5.3-Features (Late Static Binding, Closures) erzielen.

  8. Also an sich sieht das cool aus.
    Aber ich würde es etwas umschreiben, dass man es so verwenden kann:

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

    Also einfach als public Variable.

  9. @Sven: Dafür müsste man das “magische” __get() etwas missbrauchen, würde dann aber funktionieren.

  10. Pingback: Enums in PHP 5.3 | Webentwicklung im Alltag

  11. Ich habe mit der selben Überlegung mal eine Typesafe Enum Klasse erstellt (auch noch unter PHP 5.2). Da Klassenkonstanten in PHP nur skalare Werte enthalten dürfen, habe ich dazu auf Klassenmethoden zurückgegriffen, was gegenüber deiner Lösung u.a. den Vorteil hat dass keine zusätzliche Instantiierung nötig ist. Die Idee, die Definition mittels Konstanten und Reflection zu machen finde ich aber schick, eventuell klaue ich mir die :-) Bisher habe ich mit mit Code-Generierung gearbeitet.

    Beispiel:

    Ohne Code-Generierung sähe die Definition so aus:


    final class Suit extends Enum
    {
    public static final function CLUBS() { return self::___get();}
    public static final function DIAMONDS() { return self::___get();}
    public static final function HEARTS() { return self::___get();}
    public static final function SPADES() { return self::___get();}
    }

    ___get ermittelt dabei die eindeutige Instanz aus der aufrufenden Klasse und Methode.

    Die Enum-Klasse ist unter BSD Lizenz und mit Dokumentation hier veröffentlicht:

    http://www.phpclasses.org/browse/package/6021.html

  12. Oh, wegen der php-Tags wurde mein Beispiel geschluckt. Nochmal:

    Enum::define('Suit', 'CLUBS', 'DIAMONTS', 'HEARTS', 'SPADES');

    testSuit(Suit::SPADES());

    function testSuit(Suit $suit)
    {
    echo $suit; // 'SPADES'
    var_dump($suit===Suit::SPADES()); // true
    }

  13. @Fabian: Mit statischen Werten könnte es zu evtl. Problemen kommen, wenn man an mehreren Stellen verschiedene Status verwendet. Mit Instanzen fühle ich mich sicherer. ;-)

  14. Ùnd wie würdest du das realisieren? Ob Du dich mit new UserStatus() oder UserStatus::ACTIVE() an die Status-Klasse bindest macht doch keinen Unterschied.
    Vielleicht verstehe ich auch einfach nicht, was du mit verschiedenen Stati an verschiedenen Stellen meinst. Ich finde, eine Enum-Klasse sollte eindeutig sein, auch wenn Vererbung theoretisch möglich ist, was wiederum mit beiden Lösungen realisierbar ist.

  15. Ich persönlich verwende die aus java 4 (ich glaube ab 5 hatten sie dann selbst schon enums) variante:

    final class MyEnum
    {
    private $value;
    private function __construct($value)
    {
    $this->value = $value;
    }

    public static function value1()
    {
    return new self(__METHOD__);
    }

    public static function value2()
    {
    return new self(__METHOD__);
    }

    public function getValue()
    {
    return $value;
    }
    public function __toString()
    {
    return (string) $value;
    }
    }

    die kann man dann sogar in einem switch case typsicher verwenden:

    $value = MyEnum::value1();

    switch($value)
    {
    case MyEnum::value1():
    echo ‘Enum rocks’;
    break;
    case MyEnum::value2():
    echo ‘Enum2 rocks!’;
    break;
    }

    oder mit method typehints.

    class MyClass
    {
    public function __construct(MyEnum $enum)
    {

    }
    }

    lg Manuel

    Ps.: mit etwas mehraufwand kann man die dinger sogar iterierbar machen ;)

  16. @Fabian: Es könnte/wird zu Problemen kommen, wenn der Wert in statischen Klassenvariablen gespeichert wird und man z.B. mehreren Benutzern unterschiedliche Status zuweisen will.

    In Manuels Beispiel erzeugt der Methodenaufruf jedesmal ein neues Objekt und man könnte unterschiedliche Werte an verschiedenen Stellen verwenden. Meintest du das so?

    Was aber immer bleibt, ist die Klassenabhängigkeit durch den statischen Aufruf.

  17. @Daniel: jetzt verstehe ich das missverständnis. Den wert speicher ich natürlich nicht in einer statischen variable, es gibt lediglich einen instanz-pool so dass für jeden wert nur ein objekt existiert, UserStatus::ACTIVE() also eine eindeutige instanz zurückgibt. Mir fällt kein anwendungsfall ein, wo dieses verhalten unpassend wäre, es entspricht übigens auch dem typesafe enum-pattern aus java bevor es dort den nativen enum-typ gab.

  18. @Daniel prinzipiell ja, es wird bei jedem aufruf ein neues objekt erzeugt, und somit kann ich mit dem objekt auch machen worauf ich immer lust habe (serialisieren, zerstören, usw).
    Die Klassenabhängigkeit hast du bei Enumerationen im normalfall immer, auch bei nativen Enums.

    Nachteilig ist das sich die “werte” nicht gegen ein interface binden können, weil statische methoden durch ein interface nicht deklarierbar sind.

    Den Overhead beim Erzeugen des Objekts lasse ich mal außen vor.

    Hach, native Enums wären halt schon was schönes in PHP, inklusive returntype-hinting und (was vermutlich niemals kommt) typehinting für native datentypen.

  19. Hallo liebe Leute,

    die Lösung von Manuel ist die einzige mit der ich vernünftig arbeiten kann.
    Const-Varianten haben das Problem dass man sich drum kümmern muss, dass die z.B. bei Methodenparametern auch verwendet werden, um Lesbarkeit zu erhalten. Auch ob der Wertebereich richtig ist kommt erst zur Laufzeit raus.

    Statt

    foo(MyEnum::value1)
    kann man eben auch

    foo(1)

    oder
    foo(“falscher wert”)
    schreiben, ohne dass eine schlaue IDE das als falsch anmeckern kann.

    Bei der Java4-Variante kann man das mit Type Hinting verhindern.

    in

    foo(MyEnum $value)

    kann ich richtigerweise nur ein MyEnum übergeben, dass ich mit einem der statischen Konstruktoren erzeugt habe.

    Daumen hoch! Dass das aus Java 4 stammt wusste ich noch gar nicht…

Schlagwörter: Adapter, Amazon, Animation, Annotations, Anonyme Klasse, Ant, Apache, API, Array, ArrayAccess, Attachment, AutoLoader, Bedienung, Bedingung, Benchmark, Bildbearbeitung, BOM, Bootstrap, Bot, Byte Order Mark, Callback, CamelCase, Canvas, Captcha, Cheatsheet, CLI, Closure, Cloud, CodeSniffer, Community, Comparator, Contest, Controller, Converter, CouchDB, Countable, Cronjob, CSV, CustomLibrary, Custom_Model, Data Mapper, Datei, Datenbank, Datenstruktur, Datentypen, Dating, Decorator, Dekorierer, Design Patterns, Dump, Duplikat, each, Eclipse, Entwicklung, Entwurfsmuster, Enum, Erweiterung, Eventhandling, Exception-Handling, Extension, Factory, Fehler, Flash, Foreach, Formatierung, Formular, Funktion, Futon, Header, HTML5, HTTP, IDE, If, Implementierung, InnoDB, Interceptor, Interface, isset, Iterator, Java, JavaScript, jQuery, Konfiguration, Konsole, Kontrollstruktur, kopieren, Late Static Binding, Layout, Linux, Listeners, Logging, Löschen, Magento, Magic Methods, Marketing, Methode, Model, MVC, MySQL, NetBeans, Objekt, Observable, Observer, OOP, Operator, Parameter, Partnersuche, Performance, PHP, phpMyAdmin, PHPUnit, Plugin, Proxy, Qualitätssicherung, Query, Reflection, Request, Response, Rest-API, Rockstar, Routing, S3, Samba, Scheifen, Schleife, Schutz, Secure Shell, Selbstreferenz, Shop, Sicherheit, Sicherung, Singleton Pattern, SOAP, Sortierung, Sourcecode, Spam, Speicherproblem, Spickzettel, SPL, SSH, Statement, Stellvertreter, Strategy Pattern, Stream, String, Sun VirtualBox, Support, Switch, Symfony, Symfony2, Symfony Live, Tag, Template, Template Method, Ternär Operator, Testing, Thumbnail, Tool, Tour, Twig, Type-Cast, Umwandlung, Underscore, unset, Vererbung, Verzweigung, Video, Videospiel, Virtualisierung, Visitor Pattern, Vorschaubild, walk, Webserver, Webservice, Weiterleitung, Wrapper, Youtube, Zeitsteuerung, Zend Framework, Zend_Cloud, Zend_CodeGenerator, Zend_Http_Client, Zend_Service, Zugriffsmethode