Komentáře

Nahráli jsme blog na webserver a publikovali několik velmi zajímavých příspěvků pomocí Admineru. Lidé čtou náš blog a jsou z něho velmi nadšení. Dostáváme každý den mnoho e-mailů s pochvalami. Ale k čemu je všechna tato chvála, pokud ji máme pouze v e-mailu a nikdo si ji nemůže přečíst? Bylo by lepší, kdyby mohl čtenář článek přímo komentovat, takže by si mohl každý přečíst, jak jsme úžasní.

Pojďme tedy naprogramovat komentáře.

Tvorba nové tabulky

Nažhavíme Adminer a vytvoříme tabulku comments s těmito sloupci:

  • id int, zaškrtneme autoincrement (AI)
  • post_id, cizí klíč, který odkazuje na tabulku posts
  • name varchar, length 255
  • email varchar, length 255
  • content text
  • created_at timestamp

Tabulka by tedy měla vypadat nějak takto:

Nezapomeňte opět použít úložiště InnoDB.

CREATE TABLE `comments` (
	`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`post_id` int(11) NOT NULL,
	`name` varchar(250) NOT NULL,
	`email` varchar(250) NOT NULL,
	`content` text NOT NULL,
	`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
	FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)
) ENGINE=InnoDB CHARSET=utf8;

Formulář pro komentování

Prvně musíme vytvořit formulář, který umožní uživatelům příspěvky komentovat. Nette Framework má úžasnou podporu pro formuláře. Můžeme je nakonfigurovat v presenteru a vykreslit v šabloně.

Nette Framework využívá koncept komponent. Komponenta je znovupoužitelná třída, nebo část kódu, který může být přiložen k jiné komponentě. Dokonce i presenter je komponenta. Každá komponenta je vytvořena prostřednictvím továrny. Vytvoříme si tedy továrnu pro formulář na komentáře v presenteru PostPresenter.

protected function createComponentCommentForm(): Form
{
	$form = new Form; // means Nette\Application\UI\Form

	$form->addText('name', 'Jméno:')
		->setRequired();

	$form->addEmail('email', 'E-mail:');

	$form->addTextArea('content', 'Komentář:')
		->setRequired();

	$form->addSubmit('send', 'Publikovat komentář');

	return $form;
}

Pojďme si to zase trochu vysvětlit. První řádka vytvoří novou instanci komponenty Form. Následující metody připojují HTML inputy do definice tohoto formuláře. ->addText se vykreslí jako <input type="text" name="name"> s <label>Jméno:</label>. Jak již zřejmě správně odhadujete, tak ->addTextArea se vykreslí jako <textarea> a ->addSubmit jako <input type="submit">. Existuje daleko více obdobných metod, ale tyto zatím pro tento formulář stačí. Více se dočtete v dokumentaci.

Pokud je již formulář definován v presenteru, můžeme ho vykreslit (zobrazit) v šabloně. To uděláme umístěním značky {control} na konec šablony, která vykresluje jeden konkrétní příspěvek, do Post/show.latte. Protože se komponenta jmenuje commentForm (název je odvozen od názvu metody createComponentCommentForm), značka bude vypadat takto:

...
<h2>Vložte nový příspěvek</h2>

{control commentForm}

Když si nyní zobrazíte stránku s detailem příspěvku, na jeho konci uvidíte nový formulář pro komentáře.

Ukládání do databáze

Už jste zkoušeli formulář vyplnit a odeslat? Asi jste si všimli, že ten formulář vlastně nic nedělá. Musíme připojit callback metodu, která uloží odeslaná data.

Na řádek před return v továrničce pro komponentu commentForm umístíme následující řádek:

$form->onSuccess[] = $this->commentFormSucceeded(...);

Předchozí zápis znamená „po úspěšném odeslání formuláře zavolej metodu commentFormSucceeded ze současného presenteru“. Tato metoda však ještě neexistuje. Pojďme ji tedy vytvořit:

private function commentFormSucceeded(\stdClass $data): void
{
	$postId = $this->getParameter('postId');

	$this->database->table('comments')->insert([
		'post_id' => $postId,
		'name' => $data->name,
		'email' => $data->email,
		'content' => $data->content,
	]);

	$this->flashMessage('Děkuji za komentář', 'success');
	$this->redirect('this');
}

Tuto metodu umístíme přímo za továrnu formuláře commentForm.

Tato nová metoda má jeden argument, což je instance formuláře, který byl odeslán – vytvořen továrnou. Odeslané hodnoty získáme ve $data. A následně uložíme data do databázové tabulky comments.

Ještě jsou zde další dvě metody, které si zaslouží vysvětlit. Redirect metoda doslova přesměrovává zpět na aktuální stránku. Toto je vhodné udělat po každém odeslání formuláře, pokud obsahoval validní data a callback provedl operaci tak jak měl. A taky pokud přesměrujeme stránku po odeslání formuláře, neuvidíme dobře známou hlášku Chcete odeslat data z formuláře znovu?, kterou občas můžeme v prohlížeči spatřit. (Obecně platí, že po odeslání formuláře metodou POST by mělo následovat vždy přesměrování na GET akci.)

Metoda flashMessage je pro informování uživatele o výsledku nějaké operace. Protože přesměrováváme, zpráva nemůže být jednoduše předána šabloně a vykreslena. Proto je zde tato metoda, která si tuto zprávu uloží a zpřístupní ji při dalším načtení stránky. Flash zprávy jsou vykreslovány v hlavní šabloně app/Presenters/templates/@layout.latte a vypadá to takto:

<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">
	{$flash->message}
</div>

Jak již víme, tak flash zprávy jsou automaticky předávány do šablony, takže o tom nemusíme moc přemýšlet, zkrátka to funguje. Pro více informací navštivte dokumentaci.

Vykreslování komentářů

Toto je jedna z těch věcí, které si prostě zamilujete. Nette Database má skvělou funkci nazvanou Explorer. Pamatujete si ještě, že tabulky v databázi jsme schválně vytvářeli pomocí InnoDB úložiště? Adminer tak vytvořil něco, čemu se říká cizí klíče, které nám ušetří spoustu práce.

Nette Database Explorer používá cizí klíče pro vyřešení vzájemného vztahu mezi tabulkami a ze znalostí těchto vztahů umí automaticky vytvořit databázové dotazy.

Jak si jistě pamatujete, do šablony jsme předali proměnnou $post pomocí metody PostPresenter::renderShow() a nyní chceme iterovat přes všechny komentáře, které mají hodnotu sloupce post_id shodnou s $post->id. Toho můžeme docílit voláním $post->related('comments'). Ano, takhle jednoduše. Podívejme se na výsledný kód:

public function renderShow(int $postId): void
{
	...
	$this->template->post = $post;
	$this->template->comments = $post->related('comments')->order('created_at');
}

A šablonu:

...
<h2>Komentáře</h2>

<div class="comments">
	{foreach $comments as $comment}
		<p><b><a href="mailto:{$comment->email}" n:tag-if="$comment->email">
			{$comment->name}
		</a></b> napsal:</p>

		<div>{$comment->content}</div>
	{/foreach}
</div>
...

Všimněte si speciálního atributu n:tag-if. Již víte jak n:atributy fungují. Pokud k atributu připojíte předponu tag-, funkcionalita se aplikuje pouze na HTML tag, ne na jeho obsah. Toto nám umožňuje udělat ze jména komentátora odkaz pouze v případě, že poskytl svůj e-mail. Tyto dvě řádky jsou identické:

<strong n:tag-if="$important"> Dobrý den! </strong>

{if $important}<strong>{/if} Dobrý den! {if $important}</strong>{/if}