Datový typ ENUM v PHP

Enum, enumerated nebo česky výčtový typ je datový typ, jehož použití na správném místě nám může pomoci zjednodušit návrh aplikace a učinit ho elegantnějším. Výčtové typy slouží k definici skupin předem známých hodnot a umožnění následné typové kontroly (Rudolf Pecinovský – Návrhové vzory). Výhody výčtového typu můžeme využívat i v návrhu PHP aplikace, pokud překonáme jisté obtíže s implementací.

Možná jsou mezi čtenáři tací, co slyší o enum typu poprvé. Podívejme se tedy nejdříve na modelový příklad použití: Předpokládejme, že vytváříte e-shop a u každé položky e-shopu ukládáte informaci, v jaké daňové sazbě se nachází (zvýšená, snížená, nulová). Není možné ukládat přímo daňovou sazbu (20%, 10%, 0%) protože daňové sazby se mění. Po změně sazby byste tak měli u všech položek špatně uvedenou daň. Místo toho je třeba ukládat úroveň sazby. Již nyní jistě cítíte, že budete potřebovat minimálně dvě informace, kterou jsou spolu silně provázané. Bude třeba mít k dispozici jak informaci o hladině DPH (zvýšená, snížená …), tak její aktuální hodnotu (20%, 10% …)

Do jakého datového typu informaci o daňové sazbě uložit? Určitě není vhodný nějaký primitivní datový typ, jako například integer nebo string. V každé metodě, které budeme předávat daňovou hladinu, bychom museli kontrolovat, jestli je předaná sazba skutečně platná. S daňovou sazbou také budeme zcela určitě dále pracovat. Jak již bylo řečeno, budeme zjišťovat její aktuální výši, ale můžeme chtít třeba nechat rovnou spočítat cenu s DPH z ceny bez DPH.

Protože je výčet sazeb DPH předem známý a dostatečně malý, navrhneme sazbu DPH jako datový typ Enum. Bohužel nám však v PHP chybí přímá podpora pro tento typ, jako je tomu například v Pascalu nebo Javě. Nemůžeme použít ani implementaci ve stylu Javy 1.4 a nižší:

        final class VatLevel {
          private string name;
          private double tax;

          public static final State
              ZERO = new VatLevel(‘zero’, 0.0),
              LOW = new VatLevel(‘low’, 0.1),
              HIGH = new VatLevel(‘high’, 0.2);

          private VatLevel(string name, double tax) {
              this.name = name;
              this.tax = tax;
          }

          //Some other methods if needed
        }

Protože PHP podporuje pouze konstanty skalárních typů, musíme konstanty nahradit přístupovými metodami. Na druhou stranu, díky tomu můžeme využít líné inicializace, která se nám bude hodit hlavně v případě, kdy seznam možných hodnot budeme mít uložený mimo samotný kód, například v databázi, jak si ukážeme dále.

        final class VatLevel {
            private static $zero, $low , $high;
            private $name;
            private $tax;

            private function __construct($name) {
                $this->name = $name;
            }

            public static function getZeroLevel() {
                if (!isset(self::$zero)) {
                    self::$zero = new self(‘zero’);
                }
                return self::$zero;
            }

            public static function getLowLevel() {
                if (!isset(self::$low)) {
                    self::$zero = new self(‘low’);
                }
                return self::$low;
            }

             public static function getHighLevel() {
                if (!isset(self::$high)) {
                    self::$zero = new self(‘high’);
                }
                return self::$high;
            }
        }

Takto vypadá první primitivní implementace. Vidíme, že můžeme získat pouze instance platných daňových sazeb, a proto již jejich platnost nemusíme ověřovat v metodách, kterým objekt daňové sazby předáváme, stačí použít type hinting. Důležitý je privátní konstruktor, který nám zajistí plnou kontrolu nad tím, jaké objekty je možné vytvořit. Třídu také musíme definovat jako finální.

Tomuto příkladu je možné vytknout to, že míchá data a kód. Přece jen, to, jaké sazby existují, máme uloženo spolu s kódem samotné třídy. To nemusí být vždy špatně, ale protože již máme mimo třídu uložené informace o procentuální výši jednotlivých sazeb, dá se předpokládat, že budeme chtít tento zdroj použít i pro získání sazeb samotných. V tomto případě by sice případná změna znamenala úpravu pouze jednoho souboru, ale v jiných případech tomu tak být nemusí. V takovém případě bychom mohli místo jednotlivých metod pro získávání sazeb vytvořit jednu, která bude jako parametr přijímat název požadované sazby. Již vytvořené sazby bychom ukládali buď do statického pole anebo do centrálního úložiště instancí, pokud raději pracujete tímto způsobem.

Ještě lépe než u business objektů lze využít enum pro definici různých servisních objektů. Můžeme například vytvořit enum pro definici použitelných protokolů, stavů jiných objektů, signálů a podobně.

Já například používám enum pro definování výčtu typů souborů, do kterých je schopen exportovat data. Každá z možných hodnot třídy zastupující typ souboru mi pak umožňuje s její pomocí získat Exporter, díky kterému mohu takový soubor zapsat, nebo Reader, díky kterému jsem schopen takový soubor číst. 

Je zřejmé, že budeme chtít mít k dispozici každý typ souboru pouze jednou. CSV je prostě CSV a konstruktor tedy nebude třeba; můžeme vytváření instancí typů souborů svěřit tovární metodě nebo několika továrním metodám uvnitř samotné třídy.

Možné hodnoty typu Enum pak reprezentují množinu typů souborů, se kterými můj program umí pracovat. Toho můžeme využít například v rozhraní, kde z tohoto seznamu vygenerujeme formulář, v němž si uživatel vybere formát souboru, do kterého chce exportovat data.

Na podobném principu by bylo možné navrhnout vlastnost DOM elementů – nodeType. Tato vlastnost je nyní reprezentována kladným integerem, ke kterému je definována příslušná konstanta. Pokud se elementu zeptáme na jeho typ, obdržíme pouze nic neříkající číslo. V případě strojového zpracování proto musíme provést řadu porovnání, abychom zjistili, o jaký typ se jedná, a také kontrol, zda je předané číslo platným typem nodu. V případě, že dojde k chybě a my procházíme chybové hlášení, máme k dispozici právě jenom čísla. Nevím jak vy, ale já si čísla jednotlivých typů nepamatuji. Navíc, už z podstaty, typ elementu není číslo, je to hodnota z předem známé množiny hodnot – výčtu. Ze stejného důvodu je třeba implementovat Enum jako immutable. Pokud představuje množinu předem známých hodnot, je pochopitelné, že se tyto hodnoty nemohou za běhu programu změnit.

Alternativy

Při hledání optimálního řešení jsem narazil na několik zajímavých implementací, které mi ovšem z několika důvodů nevyhovovaly. Jistě bude přínosné se s vámi o ně podělit.

První implementace pochází z webu: http://www.je­remyjohnstone­.com/. Autor v úvodu článku uvádí, že se nechal inspirovat postupem uvedeným na http://it.tool­box.com/, kterému se budu také věnovat.

Celá implementace vychází z třídy, která nám reprezentuje obecný Enum. Pro každý enum, který chceme použít, pak dědíme novou třídu.

Zde je zjednodušená implementace, převzatá z webu http://mirin.cz/blog/e­num-v-php, používající zcela stejný postup, jen je pro potřeby zveřejnění na webu přehlednější.

        abstract class Enum {
          protected static $instances = array();
          final private function __construct() {}

          final public function __toString() {
            return get_class($this);
          }

          final public static function get($name) {
            if(is_subclass_of($name, "Enum")) {
              if(array_key_exists($name, self::$instances)) {
                return self::$instances[$name];
              } else {
                return self::$instances[$name] = new $name();
              }
            } else {
              throw Exception();
            }
          }

          final public static function __callStatic($name, $args) {
            return self::get($name);
          }
        }

Při prozkoumání kódu si jistě všimnete, že pro každou hodnotu musíme vytvořit novou třídu, která bude dědit od Enum, resp. některého z jejích potomků, jinak Enum nemůžeme použít.

Tento postup má několik zásadních problémů, kvůli kterým pro mě bylo toto řešení nepoužitelné.

Za prvé, pro každou hodnotu námi požadovaného výčtu musíme vytvořit novou třídu. V příkladu s DPH, který jsem uváděl dříve, by to znamenalo vytvořit třídu VatLevel, která dědí od třídy Enum, a pak další 3 třídy pro vysokou, nízkou a nulovou sazbu. Potomci třídy enum tedy reprezentují nejen jednotlivé výčtové typy, ale i jejich hodnoty.

Další nevýhodou je to, že hodnoty enumu nám mohou kolidovat s názvy jiných tříd. Také není možné vytvořit dvě různé množiny obsahující hodnoty stejného názvu. Například hodnota HIGH znamená v kontextu DPH daňovou hladinu. V kontextu hodnocení článku může reprezentovat nejlepší možné hodnocení. Pokud bychom použili implementaci uvedenou výše, byly by obě hodnoty zastupovány stejným objektem, i když každá z nich znamená něco jiného.

Abychom se vyhnuli tomuto nežádoucímu chování, museli bychom použít buď namespace, což se nám nemusí vždy hodit (pokud například vytváříme několik enum v rámci jednoho balíku), anebo prefixy. Já osobně jsem zásadně proti tomu, aby se názvům tříd dávaly prefixy čistě kvůli potřebám implementace.

A nakonec, uvažte následující kód:

        abstract class Enum {
          protected static $instances = array();
            final private function __construct() {}

          final public function __toString() {
            return get_class($this);
          }

          final public static function get($name) {
            if(is_subclass_of($name, "Enum")) {
              if(array_key_exists($name, self::$instances)) {
                return self::$instances[$name];
              } else {
                return self::$instances[$name] = new $name();
              }
            } else {
              throw Exception();
            }
          }

          final public static function __callStatic($name, $args) {
            return self::get($name);
          }
        }

        class VatLevel extends Enum {}
        class HighVat extends VatLevel {}

        class Color extends Enum {}
        class Green extends Color {}

        $vat = VatLevel::GREEN();

V tomto případě nám enum VatLevel vrátil hodnotu úplně jiného výčtu. Jakoukoli hodnotu výčtu totiž můžeme získat z kteréhokoli Enumu.

Problém by nevyřešilo ani přepsání kontroly, zda je třída potomkem Enumu:

        if(is_subclass_of($name, "Enum")) { .. }
        //Bychom vyměnili za:
        if(is_subclass_of($name, _CLASS_)) { .. }

Na důvod, proč tato kontrola nestačí, již určitě přijdete sami.

Jak jsem již zmínil, toto řešení vychází z kódu Jonathana Hohla na toolbox.com/. Obě řešení jsou prakticky stejná, nebudu jej zde tedy znovu rozebírat. Jediným rozdílem je, že Jonathan Hohle řeší problém s potřebou psát velké množství tříd pomocí funkce eval(). Použití této funkce ve zdravém objektově orientovaném kódu považuji za naprosto nevhodné. Pokud píšete software podobný Unitestům, pak má tato funkce své opodstatnění, jinak ale doporučuji na tuto funkci zapomenout. Dokonce si myslím, že eval() by měla být na produkčním serveru zakázaná. Jinak se tohoto řešení týkají i všechny již popsané komplikace.

Další implementace pochází přímo z webu php.net. Těsně před vydáním článku byl však komentář obsahující ukázku této implementace odstraněn. Na kód se můžete podívat v archivu a pro jistotu uvedu nezkrácenou ukázku i zde:

        class Enum {
          protected $self = array();
          public function __construct( /*...*/ ) {
              $args = func_get_args();
              for( $i=0, $n=count($args); $i<$n; $i++ )
                  $this->add($args[$i]);
          }

          public function __get( /*string*/ $name = null ) {
              return $this->self[$name];
          }

          public function add( /*string*/ $name = null, /*int*/ $enum = null ) {
              if( isset($enum) )
                  $this->self[$name] = $enum;
              else
                  $this->self[$name] = end($this->self) + 1;
          }
        }

        class DefinedEnum extends Enum {
            public function __construct( /*array*/ $itms ) {
                foreach( $itms as $name => $enum )
                    $this->add($name, $enum);
            }
        }

        class FlagsEnum extends Enum {
            public function __construct( /*...*/ ) {
                $args = func_get_args();
                for( $i=0, $n=count($args), $f=0x1; $i<$n; $i++, $f *= 0x2 )
                    $this->add($args[$i], $f);
            }
        }

        //Example usage:

        $eFruits = new Enum("APPLE", "ORANGE", "PEACH");
        echo $eFruits->APPLE . ",";
        echo $eFruits->ORANGE . ",";
        echo $eFruits->PEACH . "n";

        $eBeers = new DefinedEnum("GUINESS" => 25, "MIRROR_POND" => 49);
        echo $eBeers->GUINESS . ",";
        echo $eBeers->MIRROR_POND . "n";

        $eFlags = new FlagsEnum("HAS_ADMIN", "HAS_SUPER", "HAS_POWER", "HAS_GUEST");
        echo $eFlags->HAS_ADMIN . ",";
        echo $eFlags->HAS_SUPER . ",";
        echo $eFlags->HAS_POWER . ",";
        echo $eFlags->HAS_GUEST . "n";

        /*
          Will output:
          1, 2, 3
          25, 49
          1,2,4,8 (or 1, 10, 100, 1000 in binary)
        */

Tento kód je velmi zajímavý a skutečně dělá to, co je předvedeno v ukázce použití. Bohužel se domnívám, že se nejedná o datový typ Enum. Autor se pokouší o implementaci typu enum ve stylu jazyka C (nejsem v jazyce C příliš zběhlý, pokud se tedy mýlím, dejte mi vědět).

Třída Enum výše reprezentuje spíše pole s neměnnými prvky, hashovou mapu, či jinou podobnou strukturu. O hlavní důvod, proč v objektových jazycích implementujeme datový typ enum, tedy typovou kontrolu a definici předem známých a konečných množin, jsme ochuzeni.

Závěr

Implementace Enum v PHP je na první pohled složitější, než by se mohlo zdát. Vzhledem k tempu, s jakým se PHP svým rozhraním přibližuje k Javě, je možné, že vývojáři v PHP časem dostanou k dispozici přímou podporu pro Enum či třídní konstanty složených typů. Do té doby se při použití kterékoli implementace budeme potýkat s jistými obtížemi.

Jakub Tesárek je programátor který se rozhodl, že už bylo dost špagetového kódu a že to tak nenechá. Přednáší, školí, natáčí videa a jak vidíte, tak i píše o čistém kódu a co dělat, aby čistý byl.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 13

Přehled komentářů

isnotgood a nestaci proste neco takovyho?
pepca Re: a nestaci proste neco takovyho?
n/a spl enum
aTan asi preklep
Jan Prachař Alternativa
Gwyn nedokončené řešení
Oldisy3 to srovnani s c
beny Ukládání sazeb DPH
JakubTesarek reakce
Gekon Re: reakce
Oldisy3 Re: reakce
paranoiq Enum
Jakub Vrána PHP triky
Zdroj: https://www.zdrojak.cz/?p=3588