Коментари

Качихме блога на уеб сървъра и публикувахме няколко много интересни публикации с помощта на Adminer. Хората четат блога ни и са много ентусиазирани от него. Всеки ден получаваме много имейли с похвали. Но каква е ползата от всички тези похвали, ако ги имаме само в имейла и никой не може да ги прочете? Би било по-добре, ако читателят можеше директно да коментира статията, така че всеки да може да прочете колко сме страхотни.

Така че нека програмираме коментарите.

Създаване на нова таблица

Ще стартираме Adminer и ще създадем таблица comments със следните колони:

  • id int, отметнете autoincrement (AI)
  • post_id, външен ключ, който препраща към таблицата posts
  • name varchar, length 255
  • email varchar, length 255
  • content text
  • created_at timestamp

Така таблицата трябва да изглежда по следния начин:

Не забравяйте отново да използвате хранилище 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;

Форма за коментиране

Първо трябва да създадем форма, която ще позволи на потребителите да коментират публикациите. Nette Framework има страхотна поддръжка за форми. Можем да ги конфигурираме в presenter-а и да ги рендираме в шаблона.

Nette Framework използва концепцията за компоненти. Компонент е клас за многократна употреба или част от код, който може да бъде прикрепен към друг компонент. Дори presenter-ът е компонент. Всеки компонент се създава чрез фабрика. Така че ще създадем фабрика за формата за коментари в presenter-а PostPresenter.

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

	$form->addText('name', 'Име:')
		->setRequired();

	$form->addEmail('email', 'Имейл:');

	$form->addTextArea('content', 'Коментар:')
		->setRequired();

	$form->addSubmit('send', 'Публикувай коментар');

	return $form;
}

Нека отново да обясним малко. Първият ред създава нов екземпляр на компонента Form. Следващите методи прикачват HTML входове към дефиницията на тази форма. ->addText ще се рендира като <input type="text" name="name"> с <label>Име:</label>. Както вероятно вече правилно предполагате, ->addTextArea ще се рендира като <textarea>, а ->addSubmit като <input type="submit">. Има много повече подобни методи, но тези засега са достатъчни за тази форма. Можете да прочетете повече в документацията.

Ако формата вече е дефинирана в presenter-а, можем да я рендираме (покажем) в шаблона. Това се прави чрез поставяне на тага {control} в края на шаблона, който рендира една конкретна публикация, в Post/show.latte. Тъй като компонентът се нарича commentForm (името произлиза от името на метода createComponentCommentForm), тагът ще изглежда така:

...
<h2>Вложете нова публикация</h2>

{control commentForm}

Сега, когато видите страницата с детайли на публикацията, в края й ще видите нова форма за коментари.

Запазване в базата данни

Опитахте ли вече да попълните и изпратите формата? Вероятно сте забелязали, че формата всъщност не прави нищо. Трябва да прикачим callback метод, който ще запази изпратените данни.

На реда преди return във фабриката за компонента commentForm поставяме следния ред:

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

Предишният запис означава “след успешно изпращане на формата извикай метода commentFormSucceeded от текущия presenter”. Този метод обаче все още не съществува. Нека го създадем:

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

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

	$this->flashMessage('Благодаря за коментара', 'success');
	$this->redirect('this');
}

Поставяме този метод непосредствено след фабриката на формата commentForm.

Този нов метод има един аргумент, който е екземпляр на формата, която е била изпратена – създадена от фабриката. Получаваме изпратените стойности в $data. И след това запазваме данните в таблицата comments в базата данни.

Има още два метода, които заслужават обяснение. Методът Redirect буквално пренасочва обратно към текущата страница. Това е препоръчително да се направи след всяко изпращане на форма, ако тя съдържа валидни данни и callback-ът е извършил операцията както трябва. Също така, ако пренасочим страницата след изпращане на формата, няма да видим добре познатото съобщение Искате ли да изпратите данните от формата отново?, което понякога можем да видим в браузъра. (Общоприето е, че след изпращане на форма с метода POST винаги трябва да следва пренасочване към GET действие.)

Методът flashMessage е за информиране на потребителя за резултата от някаква операция. Тъй като пренасочваме, съобщението не може просто да бъде предадено на шаблона и рендирано. Затова съществува този метод, който запазва това съобщение и го прави достъпно при следващото зареждане на страницата. Flash съобщенията се рендират в главния шаблон app/Presentation/@layout.latte и изглеждат така:

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

Както вече знаем, flash съобщенията се предават автоматично на шаблона, така че не трябва да мислим много за това, просто работи. За повече информация посетете документацията.

Рендиране на коментари

Това е едно от нещата, в които просто ще се влюбите. Nette Database има страхотна функция, наречена Explorer. Спомняте ли си още, че нарочно създадохме таблиците в базата данни с помощта на хранилище InnoDB? Adminer така създаде нещо, което се нарича външни ключове, които ще ни спестят много работа.

Nette Database Explorer използва външни ключове, за да разреши взаимната връзка между таблиците и въз основа на знанието за тези връзки може автоматично да създава заявки към базата данни.

Както със сигурност си спомняте, предадохме променливата $post на шаблона с помощта на метода PostPresenter::renderShow() и сега искаме да итерираме през всички коментари, които имат стойност на колоната post_id, съвпадаща с $post->id. Можем да постигнем това чрез извикване на $post->related('comments'). Да, толкова е просто. Нека да разгледаме получения код:

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

И шаблона:

...
<h2>Коментари</h2>

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

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

Забележете специалния атрибут n:tag-if. Вече знаете как работят n:атрибутите. Ако добавите префикс tag- към атрибута, функционалността се прилага само към HTML тага, а не към съдържанието му. Това ни позволява да направим името на коментатора връзка само в случай, че е предоставил своя имейл. Тези два реда са идентични:

<strong n:tag-if="$important"> Добър ден! </strong>

{if $important}<strong>{/if} Добър ден! {if $important}</strong>{/if}