Комментарии

Блог был развернут, мы написали несколько очень хороших постов для блога и опубликовали их через Adminer. Люди читают блог, и они очень увлечены нашими идеями. Каждый день мы получаем множество писем с похвалами. Но к чему все эти похвалы, если мы получаем их только по электронной почте, и никто больше не может их прочитать? Разве не лучше было бы, если бы люди могли оставлять комментарии прямо в блоге, чтобы все остальные могли прочитать, какие мы замечательные?

Давайте сделаем все статьи комментируемыми.

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

Снова запустите Adminer и создайте новую таблицу с именем comments с этими столбцами:

  • id int, отметьте автоинкремент (AI)
  • post_id, внешний ключ, ссылающийся на таблицу posts.
  • name varchar, длина 255
  • email varchar, длина 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 имеет отличную поддержку форм. Они могут быть настроены в презентере и отображены в шаблоне.

В Nette есть понятие компоненты. Компонент — это многократно используемый класс или фрагмент кода, который может быть присоединен к другому компоненту. Даже презентер является компонентом. Каждый компонент создается с помощью фабрики компонентов. Итак, давайте определим фабрику формы комментариев в PostPresenter.

protected function createComponentCommentForm(): Form
{
	$form = new Form; // означает Nette\Application\UI\Form

	$form->addText('name', 'Ваше имя:')
		->setRequired();

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

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

	$form->addSubmit('send', 'Отправить');

	return $form;
}

Давайте немного объясним это. Первая строка создает новый экземпляр компонента Form. Методы, которые указаны ниже, размещают HTML-элементы input внутри формы. ->addText будет отображаться как <input type=text name=name>, с <label>Ваше имя:</label>. Как вы уже догадались, ->addTextArea прикрепляет <textarea>, а ->addSubmit добавляет <input type=submit>. Подобных методов больше, но это всё, что вам нужно знать прямо сейчас. Вы можете узнать больше в документации.

После того как компонент формы определен в презентере, мы можем отобразить его в шаблоне. Для этого поместите тег {control} в конец шаблона детализации поста в app/Presenters/templates/Post/show.latte. Поскольку имя компонента — commentForm (оно происходит от названия метода createComponentCommentForm), тег будет выглядеть следующим образом:

...
<h2>Оставить комментарий</h2>

{control commentForm}

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

Сохранение в базе данных

Пытались ли вы отправить какие-либо данные? Вы могли заметить, что форма не выполняет никаких действий. Она просто есть, выглядит круто и ничего не делает. Мы должны прикрепить к ней метод обратного вызова, который будет сохранять переданные данные.

Поместите следующую строку перед строкой return в фабрике компонентов для commentForm:

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

Это означает «после успешной отправки формы вызвать метод commentFormSucceeded текущего презентера». Этот метод ещё не существует, поэтому давайте создадим его.

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('Спасибо за комментарий!', 'success');
	$this->redirect('this');
}

Вы должны разместить его сразу после фабрики компонента commentForm.

Новый метод имеет один аргумент, которым является экземпляр отправляемой формы, созданный фабрикой компонентов. Мы получаем переданные значения в $data. Затем мы вставляем данные в таблицу базы данных comments.

Необходимо объяснить ещё два вызова метода. $this->redirect('this') буквально перенаправляет на текущую страницу. Вы должны делать это каждый раз, когда форма отправлена, действительна, и операция обратного вызова сделала то, что должна была сделать. Кроме того, когда вы перенаправляете страницу после отправки формы, вы не увидите хорошо известного сообщения Вы хотите отправить данные сообщения снова?, которое иногда можно увидеть в браузере. (В целом, после отправки формы методом POST, вы всегда должны перенаправлять пользователя на действие GET).

$this->flashMessage предназначен для информирования пользователя о результате некоторой операции. Поскольку мы перенаправляем, сообщение не может быть напрямую передано в шаблон и отображено. Поэтому существует метод, который сохранит его и сделает доступным при следующей загрузке страницы. Флэш-сообщения отображаются в стандартном файле app/Presenters/templates/@layout.latte, и выглядят они следующим образом:

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

Как мы уже знаем, они автоматически передаются в шаблон, поэтому вам не нужно много думать об этом, это просто работает. Для получения более подробной информации ознакомьтесь с документацией.

Отображение комментариев

Это одна из тех вещей, которые вам просто понравятся. Nette Database имеет классную функцию под названием Explorer. Помните ли вы, что мы создали таблицы как InnoDB? Adminer создал так называемые внешние ключи, которые сэкономят нам тонну работы.

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

Как вы помните, мы передали переменную $post шаблону в PostPresenter::renderShow() и теперь хотим перебрать все комментарии, у которых столбец post_id равен нашему $post->id. Вы можете сделать это, вызвав $post->related('comments'). Это так просто. Посмотрите на полученный код.

public function renderShow(int $postId): 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-, он будет обводить только теги, а не их содержимое. Это позволяет превратить имя комментатора в ссылку, если он указал свою электронную почту. Эти две строки идентичны по результатам:

<strong n:tag-if="$important"> Здравствуйте! </strong>

{if $important}<strong>{/if} Здравствуйте! {if $important}</strong>{/if}