Коментарі

Ми завантажили блог на веб-сервер і опублікували кілька дуже цікавих дописів за допомогою 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; // означає Nette\Application\UI\Form

	$form->addText('name', 'Ім\'я:')
		->setRequired();

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

	$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}