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. 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.
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. 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 {<<a href="@{email | mailto}">@{email | mailize}</a>>} </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!.
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. 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 callbacku u instance 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()
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()
(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 , a to konkrétně 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 {<<a href="@{email | mailto}">@{email | mailize}</a>>} </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. 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, nebo ZIP.
Demo je dostupné na http://bukaj.netuje.cz/play/guestbook/.