Přejít k navigační liště

Zdroják » PHP » Datový typ ENUM v PHP

Datový typ ENUM v PHP

Články PHP, Různé

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.

Komentáře

Subscribe
Upozornit na
guest
13 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
isnotgood

class SomeEnum {

const TYPE_A = 1;
const TYPE_B = 2;
const TYPE_C = 3;

private function __construct() {}

public static function enum() {
$rf = new ReflectionClas­s(__CLASS__);
return $rf->getConstants();
}
}

pepca

Není to typově zabezpečitelné, ale je to asi nejlepší z uvedených řešení.

n/a
aTan

nema byt v

17.public static function getLowLevel() {
18.if (!isset(self::$low)) {
19.self::$zero = new self(‘low’);
20.}
21.return self::$low;
22.}
23.
24.public static function getHighLevel() {
25.if (!isset(self::$hig­h)) {
26.self::$zero = new self(‘high’);
27.}
28.return self::$high;
29.}

misto self::$zero = new… spis self::$low nebo high podle metody? nejak mi to jinak nedava smysl. vracime neco cemu nikde nebylo prirazena hodnota.

Jan Prachař

Já to dělam nějak takhle:

class Enum
{
   private static $items;
   private static $frozen;

   public static function add($key, Enum $item)
   {
       $class = get_called_class();
       if (!self:$frozen[$class]) {
           self::$items[$class][$key] = $value;
       } else {
           throw new Exception;
       }

   }

   public static function get($key)
   {
       $class = get_called_class();
       return self::$items[$class][$key];
   }

   public static function freeze()
   {
       static::$frozen = TRUE;
   }
}

class VatLevel extends Enum
{
   const LOW = 1;
   const HIGH = 2;

   public $level;
   public $name;
   public $rate;

   public __construct($level, $name, $rate)
   {
       $this->level = $level;
       $this->name = $name;
       $this->rate = $rate;
   }
}


VatLevel::add(VatLevel::LOW, new VatLevel(VatLevel::LOW, 'Low', 14));
VatLevel::add(VatLevel::HIGH, new VatLevel(VatLevel::HIGH, 'High', 20));
VatLevel::freeze();

K jednotlivým hodnotám se pak dostanu:

VatLevel::get(VatLevel::LOW); //returns VatLevel(VatLevel::LOW, 'Low', 14)
Gwyn

Celé řešení mi přijde nedokončené. Za prvé je na první pohled vidět, že řešení obsahuje chyby v metodách getLowLevel, a getHighLevel

Dále třída nikde neumožnuje nastavit atribut $tax.
Autor zmiňuje, že dále ukáže možnost uložení hodnot z databáze, namísto toho se věnuje hanění jiných řešení.

Oldisy3

to mi dost nesedi. v c proste tam kde potrebujete deklarujete promenou typu enum, a vycet mate definovanej nekde pred tim. Enum je proste jenom pro to aby sme v kodu nemuseli cist porovnani s konstantama ktere nemaji zadnou vypovidaci hodnotu, nenesou pro programatora informaci o tom, ceho se tykaji. V php mi prijde dostatecne reseni s tridou a constantami. Ze to neni typove bezpecne, to je holt vlastnost php, vymyslet proti tomu ochranu je jako boj s vetrnymi mlyny. Vymyslet prekomplikovane reseni abych si usetril jedno rovnitko navic…
Dale pak priklad s davonou skupinou mi prijde jako krajne nevhodny, toto je polo abstraktni reseni michajici staticky a dynamicky pristup, nemuzu se spolehnout na to ze jsou momentalne danove skupiny dve nebo tri, a ze jich nebude brzo napriklad pet. Na danovou skupinu se hodi spise kontejner a nebo mapa, tedy par klic => hodnota.
Pekny priklad na enum je napriklad karetni hra a respektive typ karty, kde je mnozstvi a typy hodnot dane a nemenne.

beny

Dovolím si trošku nesouhlasit s ukládáním hodnoty DPH, resp. ukládáním reference. Pokud děláte např. nějaký skladový systém, musíte ukládat i nákupní ceny a nákupní sazbu DPH. A ta se v čase mění. Např. výrobek XY byl loni nakoupen s DPH 10%, no a letos byl zakoupen se 14%. Vzhledem k účetnictví ale nemůžete ve skladu přepočítat nákupny, pouze prodejky. Takže buď si ukládáte hodnotu DPH nákupky, anebo referenci na správnou hodnotu DPH, ale platnou v době naskladnění zboží -> číselník DPH s platností a časem. Není to taková trivka, jak to na první pohled vypadá.

JakubTesarek

Dobrý večer,
v první řadě bych vám chtěl poděkovat za přínosné komentáře.
Pokusím se reagovat na vaše dotazy a připomínky.

Příklad s DPH jsem vybral proto, abych ukázal, že díky objektové implementaci je možné prvky výčtu využít i praktickým způsobem a nejedná se jen o nějaké vymýšlení složitostí. Pracuji jako programátor v mezinárodní (a jedné z největších českých) společností se specializací na výrobu e-shopů. Samozřejmě že problém s DPH je trochu složitější. Dokonce je tam i problém s DPH, kdy se udělá objednávka před změnou, ale faktura se vystavuje až po změně DPH. Pokud máte skladový systém zavedený pomocí příjemek a výdejek, tak tento problém se dá velmi elegantně moji třídou řešit. Daňové zařazení položky se totiž (narozdíl od výše daně) mění pouze minimálně. A já přiznávám, že jsem toto řešení napsal až po našem vstupu do EU. To už jsem, ale myslím trochu OT. Rád si o tom popovídám, ale sem se to nehodí.

Děkuji za postřeh, že článek vypadá nedokončeně. Původně měl skutečně jinou koncepci. Jedná se o první článek, který jsem pro zdroják psal a tak jsme se s redakcí dohodli, že bych mu dal trochu jinou podobu. Ze stejného důvodu jsem nakonec do článku umístil i komentáře k implementacím, na kterých jsme se shodli, že by bylo dobré na ně reagovat. Mým cílem rozhodně nebylo očerňovat jiná řešení. Spíše jsem chtěl ukázat, že nejsem první, kdo podobný problém řeší. Poukázal jsem na důvody, proč bych já nemohl tato řešení použít a jak jsem se je snažil vyřešit ve svém kódu.

Za přiložené kódy děkuji, určitě se na ně podívám. Chyby zreviduji, pokud tam jsou, pak se velmi omlouvám. Pokud budu psát další článek, slibuji, že si dám větší pozor.

Gekon

Celé to řešení mi přijde totálně nešťastné …
Osobně mám tabulku vat, kde se definuje koeficient – 0.2,0.05 … (V reálu je to složitější, je to vázané na zemi. toto pro jednoduchost stačí)

Druhá tabulka se zbožím, kde se odkazuje na pk z VAT tabulky (ať žijí cizí klíče)

Tím není potřebná (prakticky) žádná obslužná logika, protože si to cizí klíč ohlídá. Sice se při vypisování zboží musí joinovat tabulka VAT, ale co si budeme povídat, stejně se jich joinuje hodně (sklady,tagy,o­blíbené produkty …)

Vystačím si s klasickým selectem z VAT tabulky a s update,insertem do tabulky se zbožím, kde si jen hlídám chyby fk.

Osobně mi tento článek připomíná laborku na zítřek, na kterou jsem si vzpoměl ve tři ráno …

Oldisy3

Vazane na zemi a jeste se v case meni zarazeni do danove skupiny a velikost dane.

paranoiq

moje implementace Enum: https://gist.github.com/1753178

Jakub Vrána

Viz též můj starší článek na stejné téma: Výčtový typ.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.