PHP triky

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

Ukládání dat do cookie

Na mobilním telefonu se běžně stává, že vypadne připojení k Internetu, a při ukládání dat na server bychom s tím měli počítat. Pokud data ukládáme běžným formulářem obnovujícím stránku, tak by se o to měl postarat prohlížeč – zobrazí informaci o nedostupnosti připojení a při jejím obnovení se data pošlou znovu. Pokud ale data odesíláme AJAXem, tak se o to musíme postarat sami.

Možné řešení spočívá v tom, že si data na klientu uložíme do cookie, která se při dalším úspěšném požadavku automaticky pošle na server. S knihovnou js-cookie můžeme cookie nastavit pomocí Cookies.set('save', data). Po uložení dat v cookie ji na straně serveru smažeme: setcookie("save", ""). Do cookie lze typicky uložit jen 4 KB dat, takže pro větší objemy dat se toto řešení bohužel nehodí.

Pokud je uživatel k aplikaci přihlášen a chceme, aby se změna dat projevila na všech jeho zařízeních (případně jde o změnu, která ovlivňuje i ostatní uživatele), tak je vhodné na server ihned odeslat požadavek pro uložení dat v cookie. Jen pokud se nepovede, tak data v cookie zůstanou a uloží se při dalším požadavku na server. Uživatele je v tom případě vhodné informovat o tom, že data ještě nejsou na straně serveru uložena.

Pokud se uložení dat má projevit jen v současném prohlížeči uživatele a nikde jinde (např. košík nepřihlášeného uživatele), tak extra požadavek na server ani posílat nemusíme a stačí počkat na přenos dat při dalším normálním požadavku.

A pokud stránku sestavujeme až na klientu, tak data na server ani nemusíme posílat a místo do cookie je můžeme uložit do localStorage.

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

Převod římských číslic

Nedávno jsem četl zajímavý článek Make Everything The Same o poznatcích z převodu na římské číslice. Článek je v kostce o tom, že odečítací číslice (např. IX = 9) vyžadují jiné zacházení než normální číslice (např. X = 10). Článek hovoří o podmínce, která má tendence se do kódu vkrádat a popisuje způsob, jak se jí zbavit. Ten spočívá v tom, že místo jedné konverze do finálního tvaru uděláme dvě konverze – první do aditivní formy (nepoužívá odečítací číslice) a druhou z aditivní formy do finální podoby.

Oceňuji přístup se vším zacházet stejně a nepoužívat zbytečně podmínky, ale popsané řešení mi přijde zbytečně krkolomné. Tady je moje řešení:

<?php
class RomanNumerals {
    private static $romanToNumber = array(
        'M' => 1000, 'CM' => 900,
        'D' => 500, 'CD' => 400,
        'C' => 100, 'XC' => 90,
        'L' => 50, 'XL' => 40,
        'X' => 10, 'IX' => 9,
        'V' => 5, 'IV' => 4,
        'I' => 1,
    );

    /** Převod na římské číslice
    * @param int
    * @return string
    * @copyright Jakub Vrána, http://php.vrana.cz/
    */
    static function toRoman($n) {
        $return = '';
        foreach (self::$romanToNumber as $roman => $number) {
            $times = floor($n / $number);
            $return .= str_repeat($roman, $times);
            $n -= $number * $times;
        }
        return $return;
    }
}
?>

Vytvoříme si jen jednu mapu, ve které budou všechny převody a projdeme ji. Žádné podmínky v kódu nejsou. Mapa by se dala vytvořit i programově, ale to by bylo poměrně krkolomné.

Převod z římských číslic

Mnohem zajímavější je převod z římských číslic. Pro ten můžeme použít stejnou převodní mapu a následující kód:

<?php
class RomanNumerals {
    /** Převod z římských číslic
    * @param string
    * @return int
    * @copyright Jakub Vrána, http://php.vrana.cz/
    */
    static function fromRoman($s) {
        $return = 0;
        $pos = 0;
        foreach (self::$romanToNumber as $roman => $number) {
            while ($pos < strlen($s) && substr_compare($s, $roman, $pos, strlen($roman)) == 0) {
                $return += $number;
                $pos += strlen($roman);
            }
        }
        if ($pos != strlen($s)) {
            throw new Exception("Invalid roman number: $s");
        }
        return $return;
    }
}
?>

Kód kontroluje, jestli jsou číslice seřazené od nejvyšší po nejnižší a jestli řetězec neobsahuje nepovolený znak (což jsou nutné podmínky při zápisu římských číslic). Nekontroluje počet opakování jednotlivých číslic (např. IIIII = 5 nebo IXIX = 18 projde) ani kompaktnost zápisu (např. VIV = 9 projde, i když se dá zapsat jako IX = 9). Podle článku na Wikipedii jsou ale takové zápisy možné. Pokud bychom chtěli zkontrolovat, jestli je číslo v kanonické formě, tak by asi nejjednodušší bylo na výsledku zavolat toRoman a podívat se, jestli vrátilo náš vstup.

Viz též můj starší článek na stejné téma, který jsem objevil až po dopsání tohoto. A protože u minulého článku bylo nejvíc dotazů na to, jak se převede nějaké konkrétní číslo, tak ještě formulář:

Arabské nebo římské číslo:

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

Protobuf a NaN

Většina produktů Google komunikuje téměř výhradně pomocí Protocol Buffers. V tomto formátu se komunikuje s úložištěm, mezi servery a posílají se v něm data i do klienta a z klienta. Na serveru se protocol buffery přenáší v binárním formátu, na klienta se posílá řetězec obsahující JavaScriptový literál pro pole. Ve srovnání třeba s JSONem jde o formát mnohem úspornější, protože názvy polí se vůbec nepřenáší. Srovnejte:

{"id":1,"name":"Jakub"} // JSON
[1,"Jakub"] // Protobuf

Při buildu se vygenerují metody jako getName a setName, pomocí kterých se s protocol bufferem pracuje. To má zároveň tu výhodu, že když uděláte v názvu metody překlep, tak build spadne. U JSONu tato ochrana chybí, protože např. obj.anme je platná hodnota při čtení i zápisu. Closure Compiler vygenerované metody potom obvykle zase inlinuje, taže i výsledný JavaScriptový kód je malý.

Na klientu se řetězec obsahující protocol buffer dá přeložit do JavaScriptového pole v zásadě dvěma způsoby: eval nebo JSON.parse. Oba způsoby jsou v moderních prohlížečích velmi rychlé, JSON.parse o něco rychlejší. eval má zase tu výhodu, že nepoužitá pole lze vynechat: eval('[1,,,"Jakub"]') versus JSON.parse('[1,null,null,"Jakub"]'). Tahle výhoda ale není příliš zásadní, protože mnohokrát opakovaný fragment ,null lze dobře komprimovat. Při mnoha prázdných polích se pak použije formát {"1":1,"2000":"Jakub"}.

Většina produktů Google v současnosti pro zpracování protocol bufferů používá eval. Důvod je ten, že JSON nepodporuje speciální číselné hodnoty, které podporuje protobuf i JavaScript. Jde o NaN, Infinity a -Infinity. Nekonečno se dá simulovat pomocí obřího čísla (1e404 se zaokrouhlí na Infinity), ale s NaN je problém. Přenášená data jsou tak rozmanitá, že tu a tam se NaN bohužel někde vyskytne.

Rozhodli jsme se proto NaN přenášet jako řetězec "NaN" a při deserializaci ho přeložit zase zpátky na NaN. Opačnou změnu je samozřejmě potřeba udělat i při serializaci, protože JSON.stringify převádí NaN na null. Díky tomu budeme moci z kódu odstranit eval a z CSP potom vyhodit unsafe-eval.

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

Adminer 4.2.4

Nová verze Admineru opravuje závažnou bezpečnostní chybu: vzdálené spuštění. Může k němu dojít za těchto okolností:

Ke vzdálenému spuštění mohlo dojít použitím SQLite příkazu ATTACH, který dovoluje vytvořit novou databázi. Pokud tato databáze má název *.php a následně je do ní vložen PHP kód, tak šlo tento kód následně spustit. SQLite sice umožňuje příkaz ATTACH omezit pomocí SQLITE_LIMIT_ATTACHED, ale PHP tento limit nedovoluje nastavit. Adminer proto příkaz ATTACH zakazuje parsováním SQL dotazu.

U správně zabezpečeného serveru se tato chyba neprojeví:

  1. Používejte jen verzi Admineru s ovladači, které potřebujete (např. samotné MySQL).
  2. Přístup k Admineru zabezpečte buď pomocí rozšíření (metoda login) nebo externě (např. v Apache souborem .htaccess).
  3. Uživateli, pod kterým běží web, seberte práva zápisu. Pokud je zápis výjimečně někam potřeba povolit, tak jen do adresáře nedostupného z webu. Pokud tento adresář musí být dostupný z webu, tak v něm zakažte spouštění PHP skriptů např. pomocí engine Off.
  4. Adminer pokud možno provozujte na samostatném serveru, z kterého je přístup do databáze, ale nikam jinam.

Za objevení a nahlášení chyby děkuji uživateli 庞申杰(千霄) ze společnosti Alibaba.

Jakub Vrána, Adminer, 5.2.2016, diskuse: 8 (nové: 8)

Distributed Code Jam

Baví mě řešit programátorské úlohy, a proto jsem se účastnil i několika ročníků Google Code Jam. V roce 2008 jsem dokonce vydal seriál s řešením všech úloh. Letos jsem se do řešení zase pustil, tentokrát interně v Google – před veřejnými koly se konají ta stejná kola se stejnými příklady pro zaměstnance Google, aby se vychytaly případné mouchy. Letos nás čekala novinka – v jednom kole se úlohy spouštěly na propojených počítačích Googlu. Obvykle si řešení můžete naprogramovat v čem chcete a spouštíte ho na svém počítači (nebo ho klidně můžete udělat i na papíře). V Distributed Code Jamu řešení běží na předem daném počtu počítačů a můžete použít jen programovací jazyky, které podporuje běhové prostředí Googlu. Letos to bylo C, C++, Java, Python a Pascal. Z moderních jazyků chybí snad už jen Fortran.

Řešení Distributed Code Jamu bylo pro mě dost utrpení – běhové prostředí často nefungovalo; nástroj pro testování z příkazové řádky nepodporoval všechny jazyky; co fungovalo lokálně, nefungovalo distribuovaně (i když to běželo také jen na jednom počítači); informace o problémech s řešením byly extrémně skoupé (po dvou minutách testování programu jste se dozvěděli např. jen „RTE“). Ale proto ostatně existuje tohle interní kolo – víc problémů pocítí dobrovolníci Googlu a ne skuteční soutěžící. Do veřejného kola se doufám alespoň část těchto problémů podařilo vyřešit.

Když se ale na obrazovce objevilo zelené Correct, zažil jsem podobně opojný pocit, jako když jsem s programováním začínal. Tehdy mě totiž fascinovalo, že můžu počítači něco říct a on to pro mě udělá – třeba nakreslí kružnici nebo reaguje na klávesnici. V Distributed Code Jamu říkám počítači, co má říct dalším počítačům, aby pro něj udělaly. S propojenými počítači se samozřejmě běžně setkávám – replikace databáze, sharding, rozkládání zátěže. Přesto byl tohle výjimečný zážitek – počítače jsem totiž musel naučit, jak se spolu mají domluvit, což běžně není potřeba.

Při řešení úlohy se váš kód spustí paralelně na daném počtu strojů – třeba na 100. Na všech se spustí najednou ten stejný kód, který ale může zjistit své číslo stroje (např. od 0 do 99) a podle toho přizpůsobit své chování. Počítače si mezi sebou můžou posílat zprávy identifikované číslem stroje, na který se má zpráva poslat. Do daného časového limitu (např. 3 sekundy) musí všechny programy skončit a jeden z nich musí vypsat výsledek. Kromě toho je u každé úlohy omezena i paměť strojů, např. na 128 MB. Vstup nedostanete v textovém souboru, jak je běžně zvykem, ale v podobě API – sadě funkcí, které o problému vrací informace.

Shhhh

Zadání: Sedíte u obřího kulatého stolu a svému kamarádovi chcete poslat zprávu. Vaším úkolem je zjistit, zda bude kratší, když zprávu pošlete doleva nebo když ji pošlete doprava. Problém je, že znáte jen počet hostů a u každého hosta víte, kdo sedí bezprostředně vlevo vedle něj a vpravo vedle něj. Řešení

Sandwich

Zadání: Máte bagetu, která má různé části po své délce různě dobré nebo špatné. Např. začíná šunkou, pak je špenát, pak sýr, pak zvratky a končí slaninou. Vaším cílem je sníst co nejvíc dobrého s tím, že můžete jíst jenom z obou konců – nemůžete začít uprostřed nebo něco přeskočit. Každá část je obodovaná (např. 4, -1, 3, -9, 2) a vaším cílem je maximalizovat počet bodů – v našem případě by to bylo tedy (4 + -1 + 3 ujedeno zleva + 2 ujedeno zprava). Řešení

Majority

Zadání: Probíhají volby a máme opravdu hodně kandidátů i voličů. U každého voliče vím, pro kterého kandidáta hlasoval. Cílem je zjistit, jestli některý z kandidátů dostal nadpoloviční většinu hlasů. Řešení

Jakub Vrána, Výuka, 14.10.2015, diskuse: 0 (nové: 0)

Starší články naleznete v archivu.

avatar © 2005-2016 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.