Rezervační formulář v Nette

V tomto článku si ukážeme jak jednoduše vytvořit rezervační formulář pomocí komponent Nette frameworku. Vzhledem k jednoduchosti zadání by šel celý formulář včetně aplikační logiky umístit do jednoho souboru, ale stejně tak by šla vytvořit plnohodnotná webová aplikace nad Nette frameworkem. Je těžké na začátku říct, jak se nám aplikace rozroste, takže se vydáme cestou postupného iterativního refaktoringu - začneme implementací do jednoho souboru a postupně budeme aplikaci rozdělovat na jednotlivé části když si to bude situace žádat. Postupně se tak z jednosouborové aplikace dostaneme k něčemu, co by mohlo připomínat plnohodnotnou aplikaci.

Inspirujeme se formulářem, přes který bylo možno se na Karlovarském filmovém festivalu zarezervovat u stánku LG, který půjčoval nové telefony LG G3. Na výběr bylo 5 festivalových dnů a pro každý den 3 časy. Každý čas v rámci měl volných pouze 10 míst. Vyplněné údaje budeme zapisovat do databáze a uživateli se pošle potvrzovací e-mail.

Celý postup budu postupně zaznamenávat pomocí Gitu, kde jsou vidět i jednotlivé dílčí úpravy a v textu se budu na ně odkazovat. Repozitář je zde: https://github.com/vojtasvoboda/registration-form-example

Pustíme se tedy do práce. Vytvoříme si složku dostupnou přes webový server (Instalace PHP a Apache na MacOS) a dále si připravíme Git repozitář a zavedeme si Composer. Pro aplikaci využijeme knihovnu tracy/tracy pro ladění, dibi/dibi pro ukládání dat do databáze a nette/forms pro vytvoření formuláře. Celá aplikace bude zatím v souboru index.php, takže ten si také vytvoříme.

mkdir Reservations
cd Reservations
git init
composer init
composer require tracy/tracy:'2.2.*'
composer require dibi/dibi:'2.2.*'
composer require nette/forms:'2.2.*'
touch index.php

Do složky vendor umístíme .htaccess, aby nebyla dostupná veřejně. Toto by šlo vyřešit přesunem aplikace do složky /app a nasměrováním DocumentRoot na složku /www kde by byl umístěn hlavní soubor index.php, ale pak bychom museli dál řešit ještě přesměrování, nebo nastavení virtuálního serveru (Nastavení serveru Apache). Uvidíme časem jak se nám aplikace rozroste.

Složka /vendor se pro Git ignoruje, takže přidáme soubor ručně:

git add -f vendor/.htaccess
git add .
git commit -m "Initial commit"

A následně vše dáme do gitu viz commit Initial commit.

Základ aplikace

Vytvoříme základní HTML5 stránku (např. z HtmlBasicTemplates) a odstraníme všechen obsah. Postupem času uvidíme, jestli kombinací HTML a PHP vznikne velký bordel a popřípadě vytvoříme šablonu zvlášť. Abychom otestovali, že vše funguje, v bloku <body> vytvoříme jednoduchý Nette formulář, který vypíšeme. Na začátku index.php musíme načíst autoloader z Composeru, abychom měli dostupné všechny knihovny stažené přes Composer. Otevřeme stránky v prohlížeči, například na adrese http://localhost/Reservations/

Vidíme formulář, který nemá validace a nikam svůj obsah zatím neukládá. Nicméně i tak byl celý postup velice rychlý a kus aplikace máme hotový. Aktuální stav viz commit Basic template, basic form.

Tvorba formuláře

Vytvoříme finální formulář dle zadání včetně validačních pravidel. V souboru index.php toho začíná být už docela dost, vyhodíme si proto formulář do samostatné továrničky která bude v souboru /forms/RegistrationForm.php. Do složky /forms musíme dát opět .htaccess soubor, aby nebyla přístupná zvenku. Tím máme formulář připravený, včetně validací. Soubor index.php je pořád ještě relativně přehledný a krátký. Viz commit Final registration form.

Vyzkoušíme připravené validace a vidíme, že JavaScript validace nefungují, protože webový server nemá přístup do složky /vendor, kde je soubor umístěn. Přesuneme ho tedy do složky /js a opravíme cestu. Nyní fungují validace na straně prohlížeče. Viz commit Fix JS validation file path.

Ukládání do databáze

Dále uděláme ukládání záznamů do databáze. Pro to využijeme již načtenou knihovnu dibi/dibi, která nám poskytuje rozhraní pro přístup k databázi. Vytvoříme si třídu models/Registrations.php, která nám bude reprezentovat práci s databázovou tabulkou registrací. Tato třída bude potřebovat připojení do databáze, ale abychom dle principu Dependency Injection ukázali, že třída Registrations je na tomto připojení závislá, předáme si ho tam zvenku pomocí konstruktoru. Protože nechci zadávat přístupové údaje k databázi v index.php a toto připojení možná využijeme i na více místech, vytvoříme si továrničku do souboru models/Connection.php. Akorát zde vyplníme databázové údaje a vrátíme objekt připojení. Pro jednoduchost zatím neděláme žádný konfigurační soubor, takže přístupové údaje jsou přímo v třídě připojení. Do nových složek opět nahrajeme soubor .htaccess.

Následovně si vytvoříme databázi (skript jsem přiložil do složky /sql). V index.php vytvoříme databázové připojení a repozitář rezervací kam toto připojení předáme. V repozitáři vytvoříme metodu create(), která nám zajistí uložení nové rezervace a dále si připravíme metody isExist() pro kontrolu obsazenosti termínu a checkEmail() pro kontrolu vícenásobné registrace. Vyzkoušíme vyplnit formulář a odeslat, jestli se záznam uloží do databáze a vypíše text "Saved!". Viz commit Saving to database.

Na začátku souboru index.php je vidět, že už se nám množí řádky s načítáním souborů přes require. Toto by šlo vyřešit autoloadingem pomocí knihovny nette/robot-loader, ale zatím s tím počkáme. Také nám chybí přesměrování při správném uložení záznamu kvůli tomu, abychom viděli všechny zachycené chyby.

Oddělení šablony

Většina nového kódu nám přibyla do souborů s modelem aplikace (Reservations.php, Connection.php), ale i tak se nám soubor index.php začíná zvětšovat a to tam ještě ani nejsou všechny validace. Navíc dochází k míchání HTML a PHP kódu dohromady. Proto si vytvoříme samostatnou šablonu do složky /templates, kam přesuneme všechen HTML kód a tuto šablonu načteme na konci stávajícího souboru index.php, který má nyní poloviční počet řádek, je přehlednější a obsahuje pouze čisté PHP.

Aplikaci otestujeme jestli vše funguje. Viz commit Move template separately.

V šabloně vykreslujeme pouze jednu proměnou $form, takže nasazení šablonovacího systému (Smarty, latte/latte) nemá zatím smysl.

Validace termínů

Nyní máme hotový formulář, který ukládá údaje do databáze a validuje vstupní hodnoty. Musíme ale dodělat omezení podle zadání, kdy na jedno datum a jeden čas lze provést maximálně 10 rezervací. Všechnu logiku umístíme do modelu aplikace, konkrétně do Reservations.php. Přidáme novou konstantu, která nám určuje, kolik lidí lze umístit na jeden čas v rámci dne. Pro testování nastavíme konstantu na číslo 2, takže na jeden termín budou maximálně pouze dvě rezervace. Tuto konstantu pak zohledním v metodě isFree(), která nám vrací, jestli je daný termín volný. Tyto připravené metody si pak jednoduše zavoláme při zpracování formuláře v souboru index.php.

Zkusíme vytvořit několik rezervací pro jeden termín a u třetí rezervace vidíme, že se nám vypsali chybové hlášky nad formulářem a rezervace se neuložila.

Aktuální stav viz commit Terms validation.

Zobrazení pouze volných termínů

Takto máme rezervační formulář hotový podle zadání. Lze vybrat termín, vyplnit údaje a uložit do databáze. Aplikace přitom hlídá omezení jedné rezervace na jeden e-mail a maxima deseti rezervací na jeden čas. Co je trošku nešikovné, že se nám zobrazují i termíny, které jsou již obsazené. Ideální by bylo vracet pouze datumy a časy, které jsou volné. Bude to pro uživatele přívětivější a snížíme počet volání na server s obsazenými termíny.

Bohužel máme strukturu voných datumů a časů uloženou na pevno ve formuláři. Přesuneme tedy termíny z formuláře do modelu aplikace, abychom to odtud mohli modifikovat a předávat upravené do formuláře. Dále do modelu doplníme metody getDates() a getTimes(), které budou vracet volné termíny a pošleme je do formuláře přes konstruktor. Tím se nám formulář dost zjednodušil a zobrazuje nám pouze hodnoty, které chceme. Viz commit Show only available dates and times.

Ale i toto řešení není opět ideální, protože i tak můžeme narazit na plně obsazenou kombinaci. Ideální by bylo, když by se select box pro výběr času modifikoval podle výběru datumu. To by vyžadovalo řešení pomocí asynchronního volání na server. Tuto vlastnost si dáme zatím na seznam možných rozšíření, protože ještě nemáme aplikaci hotovou - chybí nám plno věcí kolem, třeba nakódování CSS pro finální vzhled, přesměrování formuláře po odeslání a posílání potvrzovacího e-mailu.

Dokončení

Rezervační formulář nám nyní funguje včetně všech kontrol a ukládání do databáze. Doplníme podmínku, aby se formulář zobrazil pouze v případě, že je alespoň jeden termín volný. Jinak zobrazíme informaci o tom, že rezervace jsou ukončeny. Dále nastavíme přesměrování s kódem HTTP 303 po odeslání formuláře. Viz commit Show form when terms are available.

Protože musíme vyzkoušet formulář na testovacím serveru, nastavíme si tracy/tracy debugger, aby chyby logoval do složky /log a v případě chyby nám poslal e-mail. Vytvoříme složku /log s .htaccess a nastavíme náš e-mail. Viz commit Debugger settings.

Poslední věc na kterou jsme zapomněli je poslání notifikačního e-mailu uživateli který se zaregistroval. Vytvoříme tedy novou metodu sendConfirmationEmail() v Reservations.php, kterou zavoláme při uložení záznamu do databáze. Přidáme ještě jedno integritní omezení pro unikátnost e-mailu do SQL souboru. Viz commit Confirmation e-mail.

Nakonec ještě po odeslání formuláře zobrazíme, že registrace byla úspěšná. Viz commit Show success message.

Možnosti rozšíření

  1. Volné datumy a časy bychom mohli dát do databáze. Pak bychom akorát vytvořili novou modelovou třídu models/Terms.php, která by v konstruktoru přijímala instanci models/Connection.php stejně tak jako Reservations.php. Instanci Terms bychom předali konstruktorem do instance Reservations, abychom si zde mohli volat dostupné datumy a časy.
  2. Většina nastavení by se mohla přesunout do konfiguračního souboru. Týkalo by se to maximálního počtu rezervací na jeden termín (konstanta MAX_PERSON_PER_TERM), přístupové údaje do databáze (nyní v Connection.php), e-mailu odesílatele pro potvrzovací e-mail a předmět e-mailu.
  3. Přesunout celou aplikaci do složky /app. Vytvořit složku /www kam bychom umístili soubor index.php, který by načítal aktuální soubor index.php ze složky /app. Nasměrovali bychom pak DocumentRoot do složky /www a nemuseli mít všude .htaccess soubory zabraňující dostupnosti zvenku.
  4. Zaregistrovat autoloader nette/robot-loader, abychom se zbavili závislosti zapisovat každý nový soubor na začátek index.php. Přidáme nette/robot-loader buď ručně do souboru composer.json, nebo zavoláme composer require nette/robot-loader:"2.2.*". Vytvoříme složku /temp s právama pro zápis. V index.php robot-loader zavoláme, nastavíme které složky chceme sledovat a můžeme smazat všechny volání require. Viz commit Robot loader. Tato úprava je již čistě na vás, já jsem jí do finální aplikace nedal. Výhodou úpravy je, že se při dalším rozšiřování aplikace nemusíme starat o require všech potřebných souborů. Nevýhodou je, že společně s nette/robot-loader se načtou další dvě závislosti nette/caching a nette/finder. Aplikace tak roste a ještě se musíme starat o složku /temp která potřebuje zapisovat. Počet řádků v index.php se nám nijak nesížil.

Finální výsledek

Po doplnění stylů může vypadat výsledek takto:

Originální rezervační formulář byl zobrazen na stránce http://www.lg.com/cz/kviff, která byla určena pro návštěvníky 49. ročníku Karlovarského filmového festivalu. Návštěvníci si v daném termínu mohli zarezervovat mobilní telefon LG G3, kterým vytvářeli videa a fotografie. Celkem mělo telefon v ruce 170 návštěvníků, kteří vytvořili přes 2000 záznamů, ze kterých pak vzniklo video:

Deset lidí bylo odměněno novým telefonem LG G3 a dalších 10 dostalo sluchátka LG.