× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Kniha návštěv s PHP a metatable

[ http://programujte.com/profil/10927-jakub-kulhan/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/14523-martin-simecek/ ]Google [ ?rel=author ]       5. 8. 2009       33 507×

V tomto článku si ukážeme, jak vytvořit jednoduchou knihu návštěv. Různé články se různí v pohledu na to, kdo by si na takovou knihu návštěv měl troufnout. Ti, kteří znají PHP, i ti, kteří ho neznají, uvidí, že až tak složité to není; rozhodně ne s těmi správnými nástroji.

Jestliže vás odpudily hrůzné kódy, které se válí různě po internetu, ať jsou pro jiné platformy (skriptovací či další), nebo pro PHP, anebo chcete pochytit pár triků, díky kterým v PHP můžete docílit zajímavých výsledků, pak je tento článek určen pro vás.

Databáze

„Databáze“, nad kterou bude tato kniha návštěv běžet, bude opravdu jednoduchá. Budou se v ní pouze ukládat příspěvky uživatelů, nic víc. U každého příspěvku by mělo stačit jméno přispěvatele, jeho e-mailová adresa, adresa jeho stránek a samotný text příspěvku.

Jako „databáze“, resp. úložiště dat, bude sloužit metatable [ http://jakubkulhan.github.com/metatable ]. Bylo by zbytečné tu popisovat práci s metatable, když je to již napsáno jinde. Buď na již odkazované stránce, nebo ve zdejším článku [ http://programujte.com/?akce=clanek&cl=2009053100-metatable-%96-ukladame-a-vybirame-data-v-php ].

Méně zdatnějším čtenářům tu radši napíšu, jak se vlastně ta „databáze“ vytvoří a jak se k ní připojíme:

${'@table'} = metatable::open(dirname(__FILE__) . '/db/guestbook');

Ano, to je celé.

Jak budou data uložena? Vzhledem k tomu, že bude potřeba příspěvky stránkovat podle data přidání, každý příspěvek v databázi vytvoří záznamy, které budou mít jako klíč (aka řádek, row) datum přidání a v jednotlivých sloupcích budou informace o příspěvku. Pro rychlejší vybírání se bude navíc ještě udržovat řádek, řekněme =, kde název každého sloupce bude klíč k příspěvku:

+---------------------+-------+-------------------+-------------+-------------------------------+
|                     | name  | email             | www         | text                          |
+---------------------+-------+-------------------+-------------+-------------------------------+
| 2009-07-30 00:00:00 | Jan   | jan@example.com   | http://.../ | Super guestbook!              |
+---------------------+-------+-------------------+-------------+-------------------------------+
                                            .
                                            .
                                            .
+---------------------+-------+-------------------+-------------+-------------------------------+
| 2009-07-30 06:06:06 | Jakub | jakub@example.com | http://.../ | Tak tohle se mi opravdu líbí. |
+---------------------+-------+-------------------+-------------+-------------------------------+

+---+---------------------+---------------------+
|   | 2009-07-30 00:00:00 | 2009-07-30 06:06:06 | . . .
+---+---------------------+---------------------+
| = | TRUE                | TRUE                | . . .
+---+---------------------+---------------------+

Zobrazování příspěvků

K zobrazování dat na stránce použijeme at [ http://github.com/jakubkulhan/at ]. Co to ten at je? Je to jazyk, který si můžete upravit podle sebe. Nejlepší bude asi odkázat na krátkou sérii článků, které at popisují.

Kód v at pro vypisování příspvěků bude takovýto:

@each {
    @{text | texy}
    <p class="right">
        @{date | date | escape} —
        @on  www   {<a href="@{www | escape}">@{name | escape}</a>}
        @on! www   {@{name | escape}}
        @on  email {&lt;<a href="@{email | mailto}">@{email | mailize}</a>&gt;}
    </p>
}

Stručně se jedná o to, že at prochází předaný text a makra nahrazuje za jejich obsah (který je výsledkem volání některé PHP funkce). Makra začínají symbolem zavináče, poté následuje nějaký text s názvem makra a jeho parametry a nakonec je tu blok ohraničený složenými závorkami.

Makra musíme spojit s funkcemi v PHP. Vytvoříme si tedy instanci at:

${'@at'} = new at;

A teď již nějaké to makro. Třeba to nejjednodušší – makro s prázdným názvem, které vypíše obsah proměnné (to je například to volání @{text | texy}). Aby to nebylo zas tak jednoduché, budeme chtít, abychom mohli proměnné prohnat přes nějaké filtry. Ve zmíněném příkladu je to např. filtr texy, který zavolá formátovač Texy! [ http://texy.info/ ].

Filtry se budou přidávat jednoduše – vytvoří se proměnná s názvem filtru prefixovaným řetězcem @filter::

${'@filter:texy'} = fn(array(new Texy, 'process'), array(fn::ph()));

Co je to fn() a fn::ph()? Způsob, jak v PHP provádět currying – viz článek PHP curry [ http://bukaj.netuje.cz/blog/php-curry ]. V tomto případě by tu ani fn() být nemuselo, ale je lepší to vysvětlit předem.

${'@at'}->fn('', fn('_echo', array(fn::ph())));

function _echo($block)
{
    $filters = preg_split('~\s*\|\s*~', $block[0]);
    $var = $GLOBALS['@' . array_shift($filters)];

    foreach ($filters as $filter)
        $var = call_user_func($GLOBALS['@filter:' . $filter], $var);

    return $var;
}

„Výroba“ makra spočívá v zaregistrování si nějakého callback [ http://php.net/callback ]u u in­stance at. To dělá metoda at::fn() (pozor, není to to samé jako funkce fn() použitá dříve!), která příjímá jako první argument název makra a jako druhý callback na PHP funkci (v tomto případě opět obalenou pomocí fn(), teď to je ta fn() použitá dříve).

Kód je jednoduchý – rozdělí text mezi složenými závorkami ($block[0]) podle rour pomocí funkce preg_split() [ http://php.net/preg_split ] a odebere první „filtr“, protože to je vlastně název proměnné. (Pozorování: proměnné nemohou mít v názvu znak |.) Navíc, aby se daly proměnné pro at a ostatní proměnné od sebe odlišit, ty pro at jsou prefixované zavináčem. Poté se na obsah proměnné aplikují požadované filtry. A nakonec výsledek vrátíme (což ho vlastně vypíše na výstup).

Ještě ukážu, jak jsou udělané dva filtry – a to date a escape. Ani pro jeden z nich nemusíme vytvářet vlastní funkci, protože pomocí curry fn() můžeme na základní funkce v PHP navázat výchozí parametry:

${'@filter:escape'} = fn('htmlspecialchars', array(fn::ph(), ENT_QUOTES));

${'@filter:date'} = fn('date', array('j. n. Y, H:i:s', fn::ph()));

V příkladu výše jsou použity další dva filtry – mailto a mailize. Jejich implementace v PHP nejsou podstatné; pouze zařídí trochu obfuskace e-mailové adresy proti snadnému získání hloupými roboty. Jejich kód je v archivu, který je k nalezení na konci článku.

Další makra jsou on a on!. Obě jsou implementována jedním callbackem s nějakými dosazenými argumenty:

${'@at'}->fn('on', fn('_on', array(FALSE, fn::ph(), fn::ph())));

${'@at'}->fn('on!', fn('_on', array(TRUE, fn::ph(), fn::ph())));

function _on($not, $cond, $block)
{
    if ($not === empty($GLOBALS['@' . trim($cond)]))
        return $GLOBALS['@at']->run($block);
}

on je něco jako konstrukt if v PHP, jen mnohem omezenější – zkontroluje, jestli je proměnná „prázdná“ funkcí empty() [ http://php.net/empty ] (jaké hodnoty jsou považované za „prázdné“ si můžete přečíst v dokumentaci) a není-li prázdná, vrátí výsledek bloku. První argument _on() – $not – udává, jestli se má výsledek empty() negovat.

Abychom mohli předat, jestli se má, nebo nemá negovat, je pro vytvoření callbacku na _on() použito fn(). Šlo by to udělat též pomocí názvu makra, pod kterým byl callback vyvolán (o tomto způsobu se můžete dočíst v odkazované sérii článků o at).

Poslední makro je each, které pro každý příspěvek spustí daný blok:

${'@all'} = array_reverse(
    array_keys(array_shift(${'@table'}->get('=', '*'))
);

${'@limit'} = 10;

${'@page'} = 1;
if (!empty($_SERVER['QUERY_STRING']))
    ${'@page'} = intval($_SERVER['QUERY_STRING']);

${'@at'}->fn('each', fn('_each', array(fn::ph())));

function _each($block)
{
    $ret = '';

    foreach (array_slice($GLOBALS['@all'], ($GLOBALS['@page'] - 1) * 
        $GLOBALS['@limit'], $GLOBALS['@limit']) as $i)
    {
        $values = $GLOBALS['@table']->get($i, '*');
        $values[$i]['date'] = strtotime($i);

        foreach ($values[$i] as $k => $v) $GLOBALS['@' . $k] = $v;

        $ret .= $GLOBALS['@at']->run($block);
    }

    return $ret;
}

Aby bylo vůbec možné nějaké příspěvky vypisovat, musíme je nejprve načíst. O to se starají první tři řádky. Získáme klíče (názvy řádků) všech příspěvků array_keys(array_shift(${'@table'}->get('=', '*')). Jelikož jsou data seřazena vzestupně (taková už metatable prostě je), obrátíme je. Aby bylo možné příspěvky stránkovat, nastavíme omezení v proměnné ${'@limit'}. Proměnná ${'@page'} obsahuje momentální stránku získanou z query stringu.

Makro each jako každé jiné zaregistrujeme. Opět je použita curry funkce fn(), i když by tu opět nemusela být. Kód _each() je jasný – vybereme potřebné klíče k příspěvkům, pro každý klíč získáme příspěvek podle klíče, nastavíme proměnné příspěvku a necháme instanci at, aby proběhla blok a rozvinula v něm makra.

on nám dalo možnost do kódu vnést podmínky, each zase iterovat nad sadou dat (v tomto případě předem danou, ale nemusí to tak být vždy). Podobně se v at dají implementovat další jazykové konstrukty.

Stránkování

Na stránkování je tu opět (jak jinak) makro:

@pages {
    @{<a href="?@{i}">@{i}</a> }
    @{<strong>@{i}</strong> }
}

pages má v bloku „podbloky“ (vlastně se jedná o makra; akorát že nejsou zavolána přímo, ale jsou z bloku vyfiltrována a at::run() je spouštěno až na jejich blocích), kde první z nich je vykonán, jedná-li se o nějakou neurčenou stránku, a druhý, jde-li o momentální stránku. Kód makra v PHP vypadá následovně:

${'@at'}->fn('pages', fn('_pages', array(fn::ph())));

function _pages($block)
{
    $ret = '';
    list($any, $current) = array_merge(array_filter($block, 'is_array'));

    for ($i = 1, $stop = ceil(count($GLOBALS['@all']) / 
        $GLOBALS['@limit']); $i <= $stop; ++$i)
    {
        $GLOBALS['@i'] = $i;
        if ($i === $GLOBALS['@page']) {
            $ret .= $GLOBALS['@at']->run($current[1]);
        } else {
            $ret .= $GLOBALS['@at']->run($any[1]);
        }
    }

    return $ret;
}

Je to už trochu magie (moudří si prohlédnou výstup var_dump(at::parse('@pages { ... }')); a hned uvidí, proč to tak je), vězte tedy, že list($any, $current) = array_merge(array_filter($block, 'is_array')) vybere dva podbloky a přiřadí je do proměnných. Poté poiterujeme nad jednotlivými stránkami a podle toho, o jakou stránku se jedná, vykonáme potřebný blok. V blocích je nastavena proměnná ${'@i'} na číslo stránky, na které se uživatel nachází.

Přidávací formulář

Programátor je člověk líný a měl by tedy využívat co nejvíce již napsaného kódu (stojí-li ten kód za to). A proto pro práci s formuláři zneužijeme část framworku Nette [ http://nettephp.com/ ] , a to konkrétně Nette\Forms [ http://nettephp.com/nette-forms ]:

// vytvoříme
${'@form'} = new Form;

// přidáme potřebné prvky a jejich validační pravidla
${'@form'}->addText('name', 'Jméno:')
    ->addRule(Form::FILLED, 'Anonymy tu nechceme.');

${'@form'}->addtext('email', 'E-mail:')
    ->setEmptyValue('@')
    ->addCondition(Form::FILLED)
        ->addRule(Form::EMAIL, 'Podivný e-mail.');

${'@form'}->addText('www', 'WWW:')
    ->setEmptyValue('http://');

${'@form'}->addTextarea('text', 'Text:')
    ->addRule(Form::FILLED, 'Žádný text?');
${'@form'}['text']->getControlPrototype()->rows(2);

// a zpracujeme
${'@form'}->addSubmit('ok', 'Přidat')
    ->onClick[] = '_add';

function _add()
{
    $now = date('Y-m-d H:i:s');

    foreach ($GLOBALS['@form']->getValues() as $k => $v)
        $GLOBALS['@table']->set($now, $k, $v);

    $GLOBALS['@table']->set('=', $now, TRUE);

    $GLOBALS['@table']->close();

    header('HTTP/1.1 303 See Other');
    $request = new HttpRequest;
    header('Location: ' . $request->getOriginalUri()->getAbsoluteUri());
    exit();
}

${'@form'}->isSubmitted();

O Nette\Forms se můžete dočíst více v odkazované dokumentaci. Zaměřme se na zpracování – po odeslání formuláře bude zavolána funkce _add(). Ta vytvoří klíč pro příspěvek z nynějšího času (času přidání) a zapíše jednotlivé hodnoty získané z formuláře do tabulky. Pak přidá ještě záznam do řádku s klíči. Nakonec uloží tabulku a přesměruje – aby uživatel obnovením stránky neodeslal příspěvek znovu.

Zobrazení formuláře na stránce je velmi velmi prosté:

@{form}

Na závěr

Celý zdrojový kód:

@{form}

@each {
    @{text | texy}
    <p class="right">
        @{date | date | escape} —
        @on  www   {<a href="@{www | escape}">@{name | escape}</a>}
        @on! www   {@{name | escape}}
        @on  email {&lt;<a href="@{email | mailto}">@{email | mailize}</a>&gt;}
    </p>
}

@pages {
    @{<a href="?@{i}">@{i}</a> }
    @{<strong>@{i}</strong> }
}
Asi mě nařknete, že tohle přeci není celý zdrojový kód. Ale kdyby existoval framework postavený na at (jsou jich stovky postavených na XML, tak proč by nemohl být nějaký na at!), takhle by výsledný kód vypadat mohl (dobře, ještě je potřeba do toho připočíst ten formulář).

Kompletní (a teď to myslím vážně) zdrojové kódy této knihy návštěv jsou dostupné v GitHubím repozitáři [ http://github.com/jakubkulhan/guestbook/ ]. Pro naklonování:

$ git clone git://github.com/jakubkulhan/guestbook.git

Nemáte-li Git či ho nechcete, pak je tu dostupný archiv s posledním commitem – buďto ve formátu TAR.GZ [ http://github.com/jakubkulhan/guestbook/tarball/master ], nebo ZIP [ http://github.com/jakubkulhan/guestbook/zipball/master ].

Demo je dostupné na http://bukaj.netuje.cz/play/guestbook/ [ http://bukaj.netuje.cz/play/guestbook/ ].


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2009073000-kniha-navstev-s-php-a-metatable/ ].