Комментарии
Блог развёрнут, мы написали несколько очень интересных записей и опубликовали их через Adminer. Люди читают наш блог и очень им воодушевлены. Каждый день мы получаем множество писем с похвалами. Но какой прок от всех этих похвал, если они только у нас в почте и никто другой их не прочтёт? Не лучше ли было бы, если бы читатели могли комментировать статьи прямо на сайте и все видели, какие мы замечательные?
Реализуем комментарии.
Создание новой таблицы
Снова запустим Adminer и создадим таблицу comments с такими
столбцами:
idint, отметьте autoincrement (AI)post_id, внешний ключ, ссылающийся на таблицуpostsnamevarchar, длина 255emailvarchar, длина 255contenttextcreated_attimestamp
Таблица должна выглядеть примерно так:

Не забудьте снова использовать движок хранения InnoDB.
CREATE TABLE `comments` (
`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
`post_id` int NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) 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 отличная поддержка форм. Мы можем настроить их в презентере и отрисовать в шаблоне.
Nette Framework использует понятие компонентов. Компонент – это
многократно используемый класс или кусок кода, который можно
присоединить к другому компоненту. Даже презентер является
компонентом. Каждый компонент создаётся фабричным методом. Создадим в
PostPresenter фабрику для формы комментария.
protected function createComponentCommentForm(): Form
{
$form = new Form; // то есть Nette\Application\UI\Form
$form->addText('name', 'Ваше имя:')
->setRequired();
$form->addEmail('email', '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">. Похожих методов много, но пока этих
достаточно. Подробнее вы узнаете в
документации.
Когда компонент формы определён в презентере, мы можем отрисовать
(показать) его в шаблоне. Для этого поместите тег {control} в конец
шаблона, который показывает одну запись, то есть в Post/show.latte.
Поскольку компонент называется commentForm (это выведено из имени
метода createComponentCommentForm), тег будет выглядеть так:
...
<h2>Оставить новый комментарий</h2>
{control commentForm}
Теперь, если вы посмотрите на страницу подробностей записи, в конце вы увидите новую форму комментария.
Сохранение в базу данных
Вы пробовали заполнить и отправить форму? Вы могли заметить, что форма пока ничего не делает. Нам нужно присоединить метод-callback, который сохранит отправленные данные.
Добавьте перед выражением return в фабричном методе компонента
commentForm следующую строку:
$form->onSuccess[] = $this->commentFormSucceeded(...);
Эта строка означает “после успешной отправки формы вызови метод
commentFormSucceeded текущего презентера”. Этого метода ещё не
существует, так что создадим его:
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('this')
буквально перенаправляет обратно на текущую страницу. Так стоит
делать каждый раз, когда форма успешно отправлена и операция 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"> Hello there! </strong>
{if $important}<strong>{/if} Hello there! {if $important}</strong>{/if}