Výčtový typ

Školení, která pořádám

V MySQL používám výčtový typ s oblibou. Podle mně se ideálně hodí na označování speciálních hodnot. Příklad: v databázi máme jednotlivé webové stránky odkázané v navigaci. Některé z nich jsou ale speciální (např. podmínky provozu odkázané z registračního formuláře). Jak tuto speciálnost můžeme vyjádřit?

Pomocí číselného ID, kterému v programu můžeme přiřadit konstantu
Co když ale záznam se speciálním ID někdo omylem smaže? Dovoluje potom administrace vůbec ID ručně přiřadit? Jak má nebohý uživatel vědět, že zrovna trojka byly podmínky provozu?
Pomocí URL
Co když ale URL v budoucnu změníme např. na doporučení seologů? Odkaz přestane fungovat.
Pomocí výčtového typu
Speciální stránky si označíme řetězcem z výčtového typu, který používáme v aplikaci, v administraci ho můžeme přiřadit libovolné stránce, ale navenek nikde není vidět (takže ho ani není důvod měnit).

Třetí způsob se mi zdá zdaleka nejlepší, pracuje se s ním přitom velmi pohodlně: $notORM->page[array("special" => "conditions")]. Pokud potřebujeme přidat další typ speciální stránky, nutně to znamená i změnu aplikace, takže nevadí, že při tom musíme změnit i strukturu databáze.

Výčtový typ v PHP

V PHP potřebu používat výčtový typ v podstatě nemám. Třeba v uvedeném příkladě s klidem použiji řetězec "conditions". Problém je ale v tom, že pokud v řetězci uděláme překlep, nijak se o tom nedozvíme. Při ukládání nás na to může upozornit databázový server, ale při získávání dat zůstane potichu.

Některé programovací jazyky mají podporu pro výčtové typy zabudovanou, v PHP se to obvykle obchází pomocí konstant. Můžeme si vytvořit konstanty jako Page::CONDITIONS a předávat je v parametrech, problém je ale v tom, že nám nikdo nezabrání předat řetězec přímo (a udělat v něm překlep) nebo třeba předat Product::USED na místě, kde očekáváme typ stránky.

Tyto problémy dokáže vyřešit třída SplEnum z PECL extenze SPL Types:

<?php
class PageSpecial extends SplEnum {
    const CONDITIONS = 'conditions';
    const HOME = 'home';
}

function linkPage(PageSpecial $special) {
    echo "$special\n";
}

$special = new PageSpecial(PageSpecial::HOME);
$special = PageSpecial::CONDITIONS; // magie
linkPage($special);
?>

SplEnum oplývá stejně jako ostatní třídy z extenze SPL Types jistou magií: pokud se do proměnné obsahující SplType pokusíme přiřadit novou hodnotu, tak objekt proměnné zůstane zachován a změní se jen jeho vnitřek (po zkontrolování přípustnosti přiřazované hodnoty).

Poslední akademický problém zůstává u kolidujících hodnot. Pokud by hodnotami byla čísla, tak nám nic nezabrání zavolat new PageSpecial(Month::June) nebo přímo new PageSpecial(1). Jak tomu zabránit a jak se zároveň zbavit běžně nedostupné třídy SplEnum? Na řešení jsem natrefil v knize Dokonalý kód:

<?php
class PageSpecial {
    static private $cache = array();
    protected $value;
    private function __construct($value) {
        $this->value = $value;
    }
    function __toString() {
        return $this->value;
    }
    static protected function of($value) {
        if (!isset(self::$cache[$value])) {
            self::$cache[$value] = new self($value);
        }
        return self::$cache[$value];
    }
    static function conditions() {
        return self::of(__FUNCTION__);
    }
    static function home() {
        return self::of(__FUNCTION__);
    }
}

linkPage(PageSpecial::conditions());
?>

Jediný způsob, jak předat objekt požadovaného typu, je zavolat odpovídající statickou metodu.

Aby se nemusely vytvářet nové a nové objekty při každém přístupu ke konstantě a taky aby platilo PageSpecial::home() === PageSpecial::home(), tak se vytvořené objekty ukládají do keše. Alternativou by bylo statické metody zrušit, konstruktor zveřejnit a v případě předání neplatné hodnoty vyvolat výjimku.

Závěr

Osobně se mi zdají akademická řešení v PHP zbytečně komplikovaná a osobně zůstanu u pevných řetězců i za cenu rizika překlepu.

Jakub Vrána, Seznámení s oblastí, 6.7.2011, diskuse: 23 (nové: 0)

Diskuse

Martin:

Fascinuje mě, jak na těchto stránkách vždycky vyjde hypersložité a megakomplexní řešení k problému na jeden řádek :-)

ikona Jakub Vrána OpenID:

Mám ve zvyku ukazovat různá řešení stejného problému, popisovat jejich výhody a nevýhody a pak si z nich nějaké vybrat. Třeba u tohoto problému jsem zůstal u onoho jednořádkového, ale věřím, že pro čtenáře (a pro mě osobně) má článek vyšší hodnotu, než kdybych napsal: „V PHP použijte prostě "conditions".“

juzna.cz:

Pokud se pracuje v tymu, je dobre jeste vynutit coding standard ze se vzdy pouzivaji konstanty a nikdy primo rertezce. Pak k preklepu nedojde. Zameneni vice druhu konstant v ramci jedne tridy to vsak nezabrani. Ze bychom napsali RFC na novinku v php 5.5?

Opravdový odborník :-):

Tohle je vážně utrpení číst. Když už takhle bastlíte, tak se při tom prosím neohánějte relativně dobrou literaturou.

Předpokládám, že jste vycházel z příkladu v kapitole "Pokud váš jazyk nemá výčtové typy". Interpretoval jste ho ovšem zcela špatně. V knize se pro emulaci výčtových typů používá třída obsahující veřejné statické proměnné -- tedy konstanty -- nikoli metody/funkce, jako vaše PageSpecial.

Vada vašeho kódu je v tom, že při každém použití tohoto výčtového typu (např. linkPage(PageSpecial::conditions());) se vytvoří vždy nová instance dané třídy!

Právě proto se v té knize používají konstanty a neveřejný konstruktor (aby se předešlo vícenásobné instanciaci).

Dobrou literaturu si nestačí koupit, dát si ji do poličky a občas z ní citovat -- v první řadě je potřeba jí porozumět.

P.S. nevadí mi, že píšete jako prase, to dělá spousta lidí, smutné ovšem je, že svoje pomýlené nápady předkládáte ostatním jako vzor a "dobré rady".

Opravdový odborník :-):

P.S. nejen veřejné statické, ale i "final" (což je snad zřejmé z toho, že to jsou konstanty).

ikona Jakub Vrána OpenID:

Řešení jsem pochopitelně adaptoval pro PHP. Pokud máte nápad, jak ho adaptovat lépe, tak sem s ním. Prosil bych o ukázku kódu.

Zatím jste dokázal pouze to, že nerozumíte PHP a snažíte se urážet ostatní pomocí vulgarit.

Opravdový odborník :-):

Zkoušel jste nejdřív hledat, jak tento problém řešili v PHP jiní? Nebo jste jen střelil od boku a přepsal příklad z knížky (a zanesl do něj chyby)?

Ano, PHP neznám a nepoužívám, ale tohle je návrhový vzor, není třeba vymýšlet kolo. Chtělo by to více STFW a méně NIH.

Jen tak letmo jsem hodil dotaz do Googlu a řeší to lidé třeba na StackOverflow: http://stackoverflow.com/questions/254514/php-and-enums

V první řadě jde o to zbavit se volání těch metod v klientském kódu (vícenásobné instanciaci), to jsem psal už v předchozím příspěvku. Pokud mermomocí potřebujete ukázku kódu a nestačí vám to slovně (uložit si jednu instanci a tu pak používat, ne vytvářet pokaždé novou), koukněte na řešení, které tam poslal uživatel user667540. Akorát ten tam má také chybu – konstruktor by měl být neveřejný – ale jinak je to víceméně tak, jak bych si představoval.

Ovšem, je to složitější než v Javě (možná by to šlo optimalizovat přes dědičnost, abstraktní třídy…), ale PHP jste si vybral sám, za to já nemůžu :-)

P.S. také bych doporučoval kouknout na tohle: http://it.toolbox.com/blogs/macsploitation/…-implementation-25228 sice už je to trochu kouzelnictví (eval), ale pokud je to napsané správně, může to být celkem fajn – méně psaní než ten předchozí příklad.

ikona Jakub Vrána OpenID:

Je vidět, že PHP skutečně zbla nerozumíte. Jinak byste totiž pochopil, že pro onen „návrhový vzor“ PHP zkrátka nemá vyjadřovací schopnosti.

Kdybyste si můj článek přečetl pořádně, tak si všimnete, že popisuje více řešení a u každého uvádí jeho výhody a nevýhody. Řešení v odkazu na StackOverflow nejsou principiálně odlišná a také mají své výhody a nevýhody.

Např. řešení uživatele user667540 má tu nevýhodu, že se uživateli nedá zabránit v přepisu FruitsEnum::$APPLE. Za nevýhodu bych označil i to, že když chci přidat do výčtu další prvek, musím to udělat na třech místech. Jako další nevýhodu bych uvedl i to, že se třída bez ručního zavolání metody init() nedá použít.

A pokud tvrdíte, že v první řadě jde o to zbavit se volání metod, tak jste se právě stal obětí předčasné optimalizace. Nebo snad máte změřeno, že volání oněch metod každý program zpomalí tak a zabere tolik paměti, že se to stane úzkým hrdlem programu? Já samozřejmě znám řešení, které by zachovalo výhody v článku popsaného postupu a přitom snížilo paměťovou náročnost a obvykle i zrychlilo program, ale vzhledem k tomu, že to postup ještě více komplikuje, tak jsem neměl zapotřebí to uvádět (protože i tak je postup víc komplikovaný, než bych chtěl snášet, což je v článku také uvedeno).

Co se řešení s evalem týče, tak kdybyste PHP znal alespoň trochu lépe, tak byste věděl, že eval() nedokážou zpracovat akcelerátory, takže jakýkoliv kód, který eval() používá, je obvykle mnohem pomalejší než obdobný kód bez něj.

A opravdu není s podivem, že na blogu „PHP triky“ popisuji řešení pro PHP a ne pro Javu.

Než se do této diskuse vrátíte, tak si prosím doplňte znalosti PHP a získejte v něm dostatečnou praxi. Pak možná pochopíte, jak hloupě vaše vyjadřování působí.

Opravdový odborník :-):

Samozřejmě, všechno vám to věřím :-)

David Grudl:

Já zase věřím, že děláš 30 let v Javě :)

http://en.wikipedia.org/wiki/Java_%28programming_language%29

Opravdový odborník :-):

"Těžko říct, jestli je za tím stírání rozdílu mezi realitou a nadsázkou, nebo jestli jsou ti lidé prostě blbí."

http://www.misantrop.cz/561853-stredecni-strucne.php

Michal Till:

Nedá se toto celkem dobře řešit cacheováním?

<?php
return isset(self::$instances[__FUNCTION__]) ? self::$instances[__FUNCTION__] : (self::$instances[__FUNCTION__] = new self(__FUNCTION__))
?>

ikona Jakub Vrána OpenID:

Ano, přesně o tomhle řešení jsem v této diskusi mluvil, včera jsem to nahrál na http://pastebin.com/YyF8LZtL.

maryo:

Dost zajimavy.... A co takovouhle haluz? :)

<?php
abstract class AEnum
{
    private static $instances;

    private final function __construct()
    {}

    protected static function get($key, $value)
    {
        $args = func_get_args();
        $key = array_shift($args);
        $class = get_called_class();

        if (empty(self::$instances[$key])) {
            $o = new $class;
            if (!is_callable(array($o, 'init'))) {
                trigger_error(
                    "Class $class must implement method init.", E_USER_ERROR
               
);
            }
            call_user_func_array(array($o, 'init'), $args);
            self::$instances[$key] = $o;
        }
        return self::$instances[$key];
    }
}

class
PageSpecial extends AEnum
{
    private $value;

    protected function init($value)
    {
        $this->value = (string) $value;
    }

    function __toString()
    {
        return $this->value;
    }

    static final function CONDITIONS()
    {
        return self::get(__FUNCTION__, 'conditions');
    }
}

echo
get_class(PageSpecial::CONDITIONS()).' '.PageSpecial::CONDITIONS();

maryo:

Ta metoda init je tam hlavne proto, ze ne vzdycky staci jen jedna hodnota. Ta haluz je hlavne v tom, ze ne vzdycky staci jeden atribut instance... Takhle by se dalo zavolat neco jako

protected function init($manufacturer, $type, $cylinder){
    $this->manufacturer = $manufacturer;
    $this->type = $type;
    $this->cylinder = $cylinder;
}
static final function SLABA_SKODOVKA()
{
    return self::get(__FUNCTION__, 'Skoda', 'Felicia', 1300);
}

Milan Majer:

Taknějak mě pořád dráždí pohled na duplikující se těla statických metod. Pro PHP>5.3 bych navrhoval následující řešení:
<?php

class EnumException extends Exception {}

abstract class
Enum {

    protected $value;

    protected static $instances = array();
    protected static $values = array();
    protected static $useKeysAsValues = array();

    protected function __construct($value) {
        $this->value = $value;
    }

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

    public static function __callStatic($name, $arguments) {
        if (!empty($arguments)) {
            throw new EnumException('Agruments are not supported.');
        }
        $class = get_called_class();
        if (!isset(static::$values[$class])) {
            $values = static::values();
            $count = count($values);
            $values = array_flip(array_filter($values, 'is_scalar'));
            if ($count != count($values)) {
                throw new EnumException('Values are not unique or scalar.');
            }
            static::$values[$class] = $values;
        }
        if (!isset(static::$values[$class][$name])) {
            throw new EnumException('Invalid enum value.');
        }
        if (!isset(static::$instances[$class][$name])) {
            static::$instances[$class][$name] = new static(isset(static::$useKeysAsValues[$class]) && static::$useKeysAsValues[$class] ? static::$values[$class][$name] : $name);
        }

        return static::$instances[$class][$name];
    }

    protected static function useKeysAsValues() {
        static::$useKeysAsValues[get_called_class()] = TRUE;
    }

    abstract static function values();

}

class
PageSpecial extends Enum {
    static function values() {
        return array('conditions', 'home');
    }
}

class
WeekDays extends Enum {
    static function values() {
        static::useKeysAsValues();
        return array(1 => 'monday', 'tuesday', );
    }
}

class
Months extends Enum {
    static function values() {
        static::useKeysAsValues();
        return array(1 => 'january', 'february', );
    }
}

$objects = array();
$objects[] = PageSpecial::conditions();
$objects[] = Months::january();
$objects[] = PageSpecial::home();

foreach (
$objects as $object) {
    echo "$object\n";
}

$january = Months::january();
$monday = WeekDays::monday();
echo
"Months::january() == WeekDays::monday(): " . ($january == $monday ? 'TRUE' : 'FALSE') . "\n";
echo
'"Months::january()" == "WeekDays::monday()": ' . ("$january" == "$monday" ? 'TRUE' : 'FALSE') . "\n";
?>

maryo:

Pak jsem jeste koukal a prestalo se mi to moje libit, uz jen kvuli tomu ze jsem nepochopil ze to bude fungovat vlastne jen se stringama...

Milan Majer:
A tady se mi zas nelibi ta nutnost volat static::useKeysAsValues();

Tak jsem si to jeste trochu upravil :)...
http://pastebin.com/P3QSs2LL

maryo:

Milan Majer: Plus hlavne potom by ale ty vraceny hodnoty NEBYLY INSTANCI PageSpecial coz by pak trochu postradalo smysl proc to delat. Jako je v clanku ta funkce kde se to samo otestuje na typ.

<?php
function linkPage(PageSpecial $special) {
    echo "$special\n";
}
?>

V tom mym druhym reseni ktery je jen to upraveny v clanku misto toho pole a in_array v podmince by bylo lepsi jen $m->getDeclaringClass() == $c.

A jeste by slo zapojit dalsi nemagickou magii :) (tenhle zapis ale pouzitelnej az od php 5.4). S tim, ze se tam da vic omezit to pole co to vraci tak je to mensi prasarna nez by to bylo v php 5.3, ale porad je, nicmene je to pohodlnejsi.

<?php
if (!func_num_args()) {
    $value = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'];
} else {
    $value = (string) func_get_arg(0);
}
?>

a pak jen

<?php
   
static final function home()
    {
        return self::get();
    }
    static final function conditions()
    {
        return self::get();
    }
?>

maryo:

Ted jeste koukam :)) ze ne, budou to instance PageSpecial, jestli se na tom kodu nic nezmenilo tak nechapu proc jsem tvrdil ze ne... Je to asi trochu pohodlnejsi definovat to najednou, ale zase pres __callStatic nebudou editory napovidat...

Tak posledni finalni verze myho enumu kdyby na to nekdo chtel kliknout. Mozna(spis ne) to i jednou nekde pouziju, prijde mi to docela pohodlny. No nic nebudu to tu spamovat

http://pastebin.com/ausdvmaQ

Dr.Diesel:

Třetí způsob se mi zdá zdaleka nejlepší, pracuje se s ním přitom velmi pohodlně: $notORM->page[array("special" => "conditions")]. Pokud potřebujeme přidat další typ speciální stránky, nutně to znamená i změnu aplikace, takže nevadí, že při tom musíme změnit i strukturu databáze.

Já jsem zase poměrně silný odpůrce enumu a důvodem je přesně výše uvedené konstatování. Úprava skriptů nutná samozřejmě je, ale jejich nasazení je obzvlášť v případě PHP poměrně bezproblémové.
V případě databáze pokud mám tabulku se sedmi a více místným počtem záznamů, tak dělat alter kvůli enumu místo varchar/char sloupce je imho dost zvěrstvo. Osobně v něm oproti této brutální nevýhodě neshledávám konkrétní výhodu. Tím spíše pokud do DB ukládám hodnotu omezenou na úrovni kódu nějakou výše uvedenou metodou.

ikona Jakub Vrána OpenID:

Dělat kompromisy v čistotě návrhu kvůli výkonnosti je samozřejmě někdy nezbytné. A nebo ty výkonnostní problémy jde občas vyřešit jinak a čistotu návrhu zachovat. V tomto konkrétním případě např. pomocí http://www.mysqlperformanceblog.com/2007/…-certain-changes/, obecně pomocí http://www.facebook.com/notes/mysql-at-facebook/…/430801045932.

Konkrétní výhoda enumu je méně zabraného místa (ve srovnání s varcharem) a integrita dat (obdobně jako u cizích klíčů). Já prostě databázi rád navrhuji tak, aby ji aplikace pokud možno nemohla rozbít.

Když už bych měl enum oželet, asi bych raději ukládal čísla než řetězce.

Dr.Diesel:

Pozn. post z roku 2008, otazkou je aktuálnost výsledků nyní.

http://www.mysqlperformanceblog.com/2008/…-is-faster/

Jay Pipes says:
January 24, 2008 at 2:08 pm

Nice results. One thing to keep in mind that the criteria for using ENUM should be small and *static* lookups. Trying to alter the ENUM values on large tables requires a table rebuild, versus a lookup table needs just an INSERT into the lookup table...

peter says:
January 24, 2008 at 3:01 pm

Jay,

Thanks you’re right if you need to change number of values in ENUM it is nightmare. Sometimes you can hack around by avoiding costly ALTER TABLE (just replacing frm) but this is dangerous and not supported :)

DT:

Používám variantu poslední zmíněné třídy. Ovšem bez nutnosti dokola definovat stejné metody.
Jediná malinká nevýhoda je, že editory napoví výraz bez oněch prázdných závorek na konci.

<?php
abstract class enum {

    protected $value;

    protected function __construct($value) {
        $this->value = $value;
    }
    public function __toString() {
        return $this->value;
    }
    public static function __callStatic($name, $arguments) {
        if (defined("static::$name"))
            return new static(constant("static::$name"));

        throw new Exception("Invalid identifier $name.");
    }
}

class
language extends enum{
    const CS = 'cs';
    const EN = 'en';
    const FR = 'fr';
}
?>

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.