PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

Adminer 3.7.0

Asi největší novinkou v Admineru 3.7.0 je možnost v SQL příkazu importovat více souborů najednou. Využívá se k tomu <input multiple>. Další změny jsou drobnější:

  1. U provedených dotazů se nově zobrazuje čas zpracování. Umístil jsem ho do SQL komentáře příkazu, takže se přenese třeba i do historie. Nicméně je možné, že mi na tomto místě časem začne vadit a přesunu ho někam jinam.
  2. Počínaje předchozí verzí se Adminer pokusí obnovit původní pohled, trigger a uloženou proceduru, pokud vytvoření nové selže. Pokud se ovšem nepodaří ani obnova, tak objekt zmizí. Nová verze se proto nejprve pokouší pohled a uloženou proceduru vytvořit pod novým názvem. Když to selže, tak by nejspíš selhalo i vytvoření pod správným názvem (práva jdou sice nastavit i na jednotlivé objekty, ale moc často se to nevyužívá). Když to projde, tak se smaže dočasný a původní objekt a nový se vytvoří pod správným názvem. U triggerů tenhle postup není možný, protože MySQL dovoluje mít jen jeden trigger pro danou událost. Pokud je to možné, tak se použije ALTER VIEW, ale ALTER PROCEDURE je bohužel k ničemu.
  3. Vizuální maličkostí (vyžadující JavaScript) je zvýrazňování výchozího formulářového tlačítka. Tato funkce odpovídá na otázku: „Co se stane, když zmáčknu Enter?“ V Admineru je na stránce často několik formulářů a nemusí být úplně jasné, které tlačítko je pro daný formulářový prvek výchozí. Např. u možností exportu pod tabulkou bych čekal, že výchozí bude tlačítko pro export. Ale protože tyto formulářové prvky jsou součástí velkého formuláře skoro přes celou stránku, pomocí kterého si můžu vybrat záznamy k exportu, tak to tak není a výchozí je tlačítko pro uložení dat. Časem této funkce možná využiji k tomu, že výchozí tlačítko bude určeno nikoliv podle pravidel prohlížeče, ale podle kontextu.
  4. Do přihlašovacího formuláře jsem přidal placeholder pro název serveru. Tento atribut se často používá špatně a uvádí se v něm popis políčka. Správně v něm má být ukázková hodnota.
  5. Výsledky výpisu dat už nejde exportovat do SQL, pokud se ve výběru sloupců použily funkce.
  6. V rozšířeních lze nyní pro překlady používat funkci lang(). Při kompilaci se totiž všechny identifikátory překladu (což je obvykle anglická verze) z důvody úspory místa nahradí číslem. Rozšíření se ale nekompilují, takže z nich chodí původní textový identifikátor. Nová verze to řeší tak, že když zkompilované překladové funkci přijde text, tak pohledem do anglického překladu zjistí jeho číslo a pro překlad použije to.
  7. Z importu a exportu jsem odstranil podporu komprese Bzip2. Zjistil jsem, že jakékoliv exporty větší než 1 MB byly oříznuté. Vzhledem k tomu, že to nikoho příliš netrápilo, tak tato možnost asi nebyla moc využívaná.
  8. Při exportu více tabulek do CSV se vytvoří archiv TAR. V minulosti se to dělalo tak, že se každý soubor CSV nejprve umístil do paměti a z ní se potom vypsal do archivu, což žralo hodně paměti. Nová verze proto CSV soubory ukládá do dočasného souboru na disku. V PHP 5.2 by k tomu šlo využít php://temp, Adminer ale podporuje i starší verze, takže je to potřeba udělat ručně.
  9. Pokud jeden pohled závisí na druhém, tak je musíme vyexportovat ve správném pořadí. To je dost pracné určit, takže nová verze místo toho nejprve vytvoří tabulku pro každý pohled a před vytvořením skutečného pohledu ji zase smaže.
  10. Předchozí verze při vymazání vyhledávacího políčka vymazala i sloupec, ve kterém se hledá. Bohužel to nefungovalo správně, protože jsem pojmenoval dvě různé funkce stejně – taková hloupá chyba.
  11. Při úpravě záznamu se používá klauzule LIMIT 1. Ta je jako pojistka pro případ editace záznamů v tabulce bez primárního klíče. Bohužel to způsobuje varování při statement-based replikaci, že tato klauzule způsobuje nejednoznačné výsledky. Varování ničemu nevadí, ale „zasírá logy“. Nová verze proto kontroluje, jestli se záznam edituje podle primárního klíče a klauzuli LIMIT 1 přidá jen pokud ne.
  12. Nová verze také opravuje velmi ošklivou chybu, která je v Admineru nejspíš už od jeho vzniku. Ta se projevuje tehdy, pokud vyfiltruji záznamy, vyberu jen některé sloupce (bez primárního klíče), označím nějaké řádky a dám je upravit. Předchozí verze upravily všechny záznamy, jejichž hodnoty odpovídaly zobrazeným údajům. Žádoucí chování je, aby se upravily jen ty záznamy, které odpovídají výsledkům vyhledávání a zobrazeným údajům. Přepsal jsem si takhle nějaká data a doufám, že to nepostihlo moc dalších uživatelů – jde přeci jen o poměrně pokročilou funkci a většina uživatelů pro takovéto změny Adminer asi nepoužívá.
  13. Nová verze také aplikaci dále vylepšuje v mobilních prohlížečích. Změna spočívá v přesunu navigace až pod hlavní obsah. Zvažoval jsem dokonce, že bych navigaci udělal na desktopu schovávací, protože se mi špatně pracuje s výpisem širokých tabulek. Ale vzhledem k tomu, že seznam tabulek používám skoro stejně často jako hlavní obsah, tak jsem tuto změnu neudělal.
  14. Počet řádků v tabulkách na stránce exportu se nově načítá asynchronně, stejně jako na stránce se seznamem tabulek.
  15. V MySQL je z nějakého důvodu příkaz SHOW TABLE STATUS u velkých databází hrozně pomalý. Adminer ho potřebuje pro zjištění komentářů a typů tabulek. Nová verze si proto tyto informace vytahuje z information_schema, což je rychlejší. Projeví se to na stránce pro vytvoření tabulky a v Adminer Editoru.
  16. Datový typ bit se nově zobrazuje jako číslo v dvojkové soustavě. Hodnoty tohoto typu se vrací odlišně v MySQLnd a libmysql a jde o jedinou mně známou nekompatibilitu mezi těmito nízkoúrovňovými knihovnami. Adminer chování sjednocuje.
  17. Pro export binárních dat se nyní používají konverzní funkce.
  18. S datovým typem point se v některých případech pracovalo jako s číslem, protože obsahuje podřetězec int, ha ha.
  19. Při výpisu nezobrazitelných dat se používá dotaz ve tvaru SELECT *, HEX(binary) AS binary. To vedlo k dvojitému uvedení těchto dat v exportu. Nová verze to ošetřuje. Možná si říkáte, proč se prostě nevyjmenuje seznam všech sloupců. I když by se přeneslo míň dat, tak by dotaz byl podstatně míň přehledný, proto se používá verze s hvězdičkou.
  20. Předchozí verze rozbila EXPLAIN v MySQL < 5.1, nová verze to opravuje.
  21. Export SQLite nově obsahuje i pohledy.
  22. PostgreSQL vrací pravdivostní hodnoty jako znaky t a f. Jeden uživatel mi ale nahlásil, že mu je PostgreSQL vrací jako 1 a 0, což způsobovalo problémy při detekci NOT NULL sloupců a primarních klíčů. Nepodařilo se mi zjistit, čím je to způsobené, jestli nějakým nastavením PostgreSQL nebo nestandardním ovladačem, každopádně jsem Adminer upravil tak, aby přijímal oba formáty.
Jakub Vrána, Adminer, 20.5.2013, diskuse: 19 (nové: 19)

Dočasně obnovuji prodej své knihy

Knihu 1001 tipů a triků pro PHP si můžete koupit přímo od autora včetně podpisu a věnování. Tento prodej jsem přerušil po odstěhování do USA, v červnu ale budu v ČR a knihu budu dočasně opět prodávat.

Pokud knihu ještě nemáte a nejste si jisti, jestli za to stojí, přečtěte si ohlasy a recenze jejich čtenářů.

Knihu si můžete objednat už teď, nejzazší termín pro objednání je 15. 6. 2013. Knihu lze koupit v papírové podobě, elektronická verze bohužel není k dispozici – já na její vydání nemám práva a nakladatel ji nechystá.

Jakub Vrána, Osobní, 17.5.2013, diskuse: 6 (nové: 6)

Pozvánka na červnová školení

Po přestěhování do USA jsem svá školení PHP (Úvod, PHP 5, Bezpečnost a Výkonnost) předal Michalu Špačkovi a MySQL (Základ, Pokročilé) Tondovi Faltýnkovi a musím říct, že jsem s nimi stejně jako účastníci školení velmi spokojen.

Další termíny všech těchto školení jsou vypsána na červen tohoto roku a já bych vás na ně rád pozval. Tou dobou zrovna budu v Praze, takže se alespoň na některá školení přijdu podívat, i když výklad nechám na Michalovi a Tondovi. Přihlásit se můžete na stránkách jednotlivých školení, v případě jakýchkoliv dotazů můžete použít diskusi u tohoto článku nebo e-mail. Rád vás v červnu uvidím.

Jakub Vrána, Školení, 1.5.2013, diskuse: 0 (nové: 0)

Adminer 3.6.4

Adminer 3.6.4 přináší několik drobných vylepšení a oprav:

  1. Asi největší změna (i když spočívá jen ve změně stylu) je zafixování umístění stránkování. Řada uživatelů mě žádala, ať stránkování dám i nad výpis tabulky. Problém ale je, že zjištění celkového počtu řádků může trvat dlouho a je nesmysl na něj čekat před zobrazením dat. Dalo by se to vyřešit JavaScriptem, i když poněkud krkolomně. Fixní umístění stránkování považuji za lepší řešení – je vždy na jednom místě, nepotřebuje JavaScript a navíc je vidět i v případě posunutí stránky doprava. Tehdy se mi hodí především odkaz pro nahrání dalších dat AJAXem.
  2. S předchozí změnou souvisí i zvýšení výchozího počtu zobrazených záznamů ze 30 na 50. Při fixním umístění stránkování vypadá trochu hloupě, když je stránkování dole v okně osamoceno. Na poslední stránce to vypadá pořád trochu divně, ale než se na ni uživatelé dostanou, tak si na umístění snad zvyknou.
  3. Laboroval jsem i se změnou výchozího řazení záznamů (sestupně podle auto_increment sloupce), to jsem ale nakonec neudělal, protože zrušení tohoto výchozího řazení by bylo poněkud krkolomné.
  4. Odkaz pro úpravu SQL dotazu na stránce výpisu jsem původně změnil na inline formulář, pak jsem změnu ale vrátil, protože někdy je na úpravu potřeba víc místa a navíc je trochu matoucí, že po úpravě dotazu se dostanu na stránku s obecným SQL dotazem. Inline formulář jsem nakonec zachoval alespoň po Ctrl+kliknutí na dotaz.
  5. Historie SQL příkazů se nyní zobrazuje od nejnovějších.
  6. Pokud při změně pohledu, triggeru nebo procedury došlo k chybě, tak se starý objekt smazal. Data pro nový objekt zůstala vyplněná ve formuláři, takže po opravě chyby se objekt v pořádku vytvořil, ale pokud po chybě uživatel formulář zavřel, tak přišel jak o starý, tak o nový objekt. Adminer se nyní v případě chyby pokusí vytvořit starý objekt.
  7. Pokud při vytváření nebo změně databázového uživatele zadáte nehašované heslo, tak se nově zahašuje v odděleném dotazu, který se neukládá do historie.
  8. U sloupců typu timestamp se nyní zobrazuje výběr pro možnost ON UPDATE CURRENT_TIMESTAMP, který dříve byl v políčku pro výchozí hodnotu, což bylo matoucí.
  9. Při používání vývojové verze jsem byl mnohokrát vděčný za změnu, která otevře databázi do nového okna, pokud na ni uživatel Ctrl+klikne. Adminer tohle dělá se všemi tlačítky, ale formulář pro výběr databáze tlačítko nemá, takže bylo potřeba trochu krkolomného JavaScriptu.
  10. Formulářová políčka od nedávna mají typ vstupu uzpůsoben pro mobilní prohlížeče. Nová verze toho využívá tak, že při smazání hledaného textu se smaže i sloupec, ve kterém se hledalo. Funguje to i v Chrome, Firefox tuto akci bohužel nenabízí.
  11. U ručně položených SQL dotazů se nově zobrazuje výsledek EXPLAIN včetně informací o oddílech.
  12. Při nahrání další stránky dat došlo k vymazání hodnot v inline editaci. Bylo to proto, že se pro připojení další stránky používala vlastnost innerHTML. Nově se proto používá čistý DOM.
  13. Po smazání záznamu Adminer přešel na první stránku výsledků. Bylo to proto, aby po smazání všech záznamů na poslední stránce nedošlo k zobrazení prázdné stránky. Ve většině případů se ale spíš hodí zůstat na stejné stránce, proto jsem toto chování změnil.
  14. Při exportu jediné tabulky se nově použije její název pro název souboru, při exportu více tabulek se použije název databáze. Dřív byl název souboru odvozen od toho, ze které stránky uživatel na export přišel, což nemuselo vždy odpovídat tomu, co skutečně exportoval.
  15. Adminer už dlouho bere na vědomí existenci rozšíření Suhosin a upozorní uživatele, pokud odešle formulář, který toto rozšíření ořízlo. Nově se děje totéž i pro konfigurační direktivu PHP max_input_vars a děje se tak u všech formulářů.
  16. Po odhlášení se nezrušil záznam o trvalém přihlášení, šlo o chybu z verze 3.6.0.
  17. Drobná změna užitečná pro mobilní prohlížeče spočívá v nepoužívání velkých písmen na začátku identifikátorů, protože běžná konvence je začínat identifikátory malými písmeny.
  18. Adminer nyní podporuje nové vlastnosti MySQL 5.6, konkrétně změny u časových typů a fulltextové indexy v InnoDB.
  19. Alter export jsem přesunul do rozšíření, protože už ho sám nepoužívám a mám za to, že ho nepoužívá ani moc dalších uživatelů.
  20. Při exportu se nastavovalo časové pásmo podle proměnné time_zone. Není to ale tak jednoduché, protože časové pásmo nastavené na jednom serveru nemusí vůbec existovat na jiném serveru. Nejběžnější hodnota této proměnné je navíc SYSTEM znamenající, že se má použít časové pásmo daného serveru. Nově se proto časové pásmo uvádí v hodinách, což je přenosné všude.
  21. Ze záhlaví výpisu procesů nově vede odkaz do dokumentace.
  22. Export SQLite nyní obsahuje i indexy, které dříve chyběly.
Jakub Vrána, Adminer, 26.4.2013, diskuse: 21 (nové: 21)

Vězňovo dilema

Martin Malý zveřejnil sérii svých starších článků o tzv. Vězňově dilematu. Rád jsem si tuto úlohu připomněl. O co jde?

Představte si, že jste uvězněn spolu s vaším komplicem, jste držen každý zvlášť a jste zvlášť vyslýcháni. Máte možnost vypovídat proti tomu druhému nebo mlčet. Policie na vás skoro nic nemá a pokud se oba svorně rozhodnete mlčet, dostanete oba tak akorát podmínku. Pokud však pomůžete vašeho kolegu usvědčit, půjde on sedět na deset let a vy vyváznete bez trestu. Totéž platí i v opačném případě. A pokud se rozhodnete oba vypovídat proti tomu druhému, půjdete sedět na pět let oba. Jak se rozhodnete?

já / protihráčpodrazspolupráce
podraz110
spolupráce-106

Hra na toto téma spočívá v tom, že se proti sobě postaví dva algoritmy na předem neznámý počet kol a mají hrát tak, aby maximalizovaly svůj zisk při nějakém bodovém ohodnocení jednotlivých tahů.

Martin problém popisuje ve vztahu ke komunitám, do hry vnesl šum a mutování algoritmů a celé jeho dílo určitě stojí za přečtení. Mně problém přijde zajímavý z jiného pohledu.

Původní algoritmy

Martin použil v simulacích tyto algoritmy:

  • Kavka. Algoritmus, který nikdy nepodrazí, jedině když dojde k nějakému šumu, tak je jeho krok vnímán jako podraz. (Beránčí algoritmus)
  • Podrazák. Vždy podrazí.
  • Rozmar. Algoritmus, který se mezi spoluprací a podrazem rozhoduje zcela náhodně.
  • Dobrý. Algoritmus se rozhoduje náhodně. Spolupracuje, ale v jednom případu ze čtyř podrazí.
  • Špatný. Rozhoduje se náhodně, většinou podrazí, v jednom ze čtyř případů spolupracuje.
  • TFT. Algoritmus začne spoluprací a pak opakuje poslední krok soupeře – podrazí za podraz, spolupracuje při spolupráci. (Čistící algoritmus)
  • TF2T. Algoritmus, který začíná spoluprací a podrazí jen tehdy, když byl dvakrát za sebou podražen.
  • Velký pes. Algoritmus, který začne spoluprací. Pokud v minulé hře oba spolupracovali, spolupracuje dál, pokud byl podražen, podrazí. Pokud v minulé hře podrazil a podraz se mu vyplatil (=soupeř spolupracoval), bude dál podrážet. Pokud podráží oba, nabídne spolupráci. (Pragmatický algoritmus)
  • Malý pes. Chová se stejně jako velký pes, ale začíná podrazem.

Naprogramovat by se daly takhle:

<?php
// true je spolupráce, false je podraz

function kavka($history) {
    return true;
}

function podrazak($history) {
    return false;
}

function rozmar($history) {
    return (mt_rand(0, 1) == 0);
}

function dobry($history) {
    return (mt_rand(0, 3) != 0);
}

function spatny($history) {
    return (mt_rand(0, 3) == 0);
}

function tft($history) {
    return (!$history || array_pop($history));
}

function tf2t($history) {
    return (count($history) < 2 || array_pop($history) || array_pop($history));
}

function velky_pes($history) {
    $return = true;
    foreach ($history as $play) {
        if (!$play) {
            $return = !$return;
        }
    }
    return $return;
}

function maly_pes($history) {
    $return = false;
    foreach ($history as $play) {
        if (!$play) {
            $return = !$return;
        }
    }
    return $return;
}
?>

Optimální algoritmus

Všechny algoritmy jsou dost primitivní – do historie se buď vůbec nedívají nebo se dívají jen jeden, maximálně dva kroky zpět. Pro Martinovy účely to úplně stačí a závěry z toho vyvodil zajímavé. Ale já jsem si řekl – jak by vypadal algoritmus, který by chtěl maximalizovat svůj zisk s využitím všech dostupných znalostí? Pokud by věděl, kdo proti němu stojí, tak má situaci docela jednoduchou:

<?php
function optimalni($history, $opponent) {
    switch ($opponent) {
        case 'kavka': return false;
        case 'podrazak': return false;
        case 'rozmar': return false;
        case 'dobry': return false;
        case 'spatny': return false;
        case 'tft': return true;
        case 'tf2t': return (count($history) % 2 != 0);
        case 'velky_pes': return true;
        case 'maly_pes': return (bool)$history;
    }
    return true;
}
?>

Netriviální protihráči:

Pokud by se hra hrála na předem známý počet kol, tak by se také vyplatilo v posledním kole každého podrazit.

Učící se algoritmus

A teď řekněme, že nevíme, kdo proti nám stojí – jak z předešlých tahů odvodit, kdo to je? Kvůli náhodným algoritmům je to ještě o něco těžší – nikdy nemůžeme mít jistotu. Vyplatí se zariskovat a v jednom tahu nabídnout spolupráci jen proto, abych zjistil, kdo proti mně stojí v příštím kole? Vyplatí se na to vůbec sestavovat pravidla nebo by bylo lepší vyzkoušet všechny možnosti a optimální strategii vypočítat? Já jsem sestavil pravidla začínající podrazem a spoluprací, náhodné algoritmy jsem nezohlednil:

<?php
function ucici($history) {
    $opening = array(false, true); // v prvních dvou kolech vyzkoušíme reakce na podraz a spolupráci
    if (count($history) < count($opening)) {
        return $opening[count($history)];
    }
    
    if (!$history[0]) { // podraz v prvním kole: Podrazák nebo Malý pes
        return $history[1]; // když v druhém kole spolupracoval, je to Malý pes a vyplatí se spolupracovat, jinak je to Podrazák
    }
    
    if (!$history[1]) { // podraz v druhém kole: TFT nebo Velký pes
        if (count($history) < 3) {
            return false; // ve třetím kole musíme Velkého psa naučit, že se mu nevyplatí podrážet
        }
        return true;
    }
    
    // TF2T nebo Kavka
    if (count($history) < 4) {
        return false; // ve třetím kole se vyplatí podrazit, ve čtvrtém vyzkoušíme, jestli to je TF2T
    }
    if (count($history) < 5) {
        return true; // v pátém kole TF2T naznačíme, že s ním můžeme spolupracovat, zisk z Kavky oželíme
    }
    if (!$history[4]) { // když v pátém kole podrazil, je to TF2T
        return (count($history) % 2 == 0); // v lichých kolech se vyplatí spolupracovat
    }
    return false; // jinak to je Kavka
}
?>

Při jiném zahájení by pravidla samozřejmě vypadala jinak. Algoritmus není tak dobrý jako optimální (který ovšem podvádí) a v některých případech ani jako některé primitivní algoritmy, s velkým počtem kol se jim ale přibližuje. V součtu proti všem drtivě vyhrává:

ziskKavkaPodrazákTFTTF2TVelký pesMalý pescelkem
Kavka1200-2000120012001200-2000800
Podrazák2000200218236110011004854
TFT1200178120012001200465024
TF2T1200156120012001200-4504506
Velký pes1200-90012001200120011585058
Malý pes2000-90086550119811904124
Optimální200020012001600120011907390
Učící198417811521576116611907246

Neznámý protihráč

A co kdybych nevěděl, kdo proti mně stojí, ale ani bych nevěděl, jaké algoritmy existují? Co kdybych vypustil stádo divokých algoritmů do arény a nechal přežít ten nejlepší? Jak by takový algoritmus vypadal?

Pokud s tím někdo chce experimentovat, zde je program, který jsem použil pro testování algoritmů:

<?php
function veznovo_dilema($algos, $prices) {
    $profits = array_fill_keys($algos, array_fill_keys($algos, 0));
    foreach ($algos as $home) {
        foreach ($algos as $away) {
            $home_history = array();
            $away_history = array();
            for ($i = 0; $i < 100; $i++) {
                $home_play = $home($away_history, $away);
                $away_play = $away($home_history, $home);
                $home_history[] = $home_play;
                $away_history[] = $away_play;
                $profits[$home][$away] += $prices[$away_play][$home_play];
                $profits[$away][$home] += $prices[$home_play][$away_play];
            }
        }
    }
    return $profits;
}

$algos = array('kavka', 'podrazak', 'tft', 'tf2t', 'velky_pes', 'maly_pes', 'optimalni', 'ucici');
$prices = array(
    false => array(false => 1, true => -10),
    true => array(false => 10, true => 6),
);

foreach (veznovo_dilema($algos, $prices) as $algo => $val) {
    echo "$algo\t" . implode("\t", $val) . "\t" . array_sum($val) . "\n";
}
?>

Závěr

Ve skutečném světě taky pracujeme s delší historií, pamatujeme si křivdy z minula a při svém rozhodování je chtě nechtě zohledňujeme. Co se algoritmizuje těžko, je první dojem – pokud se nám někdo nezdá, tak se k němu chováme jinak, než pokud je nám sympatický.

Jakub Vrána, Řešení problému, 8.2.2013, diskuse: 8 (nové: 8)

Starší články naleznete v archivu.

© 2005-2013 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.