コメント

ブログは公開され、とても面白い記事をいくつか書いて Adminer で載せました。人々はブログを読んでいて、たいそう気に入ってくれています。毎日たくさんの称賛のメールが届きます。しかしその称賛が私たちのメールの中にしかなく、ほかの誰にも読めないなら、いったい何の役に立つでしょうか。読者が記事に直接コメントできて、私たちがどれだけ素晴らしいかを誰もが見られたほうがよくないでしょうか。

コメントを作りましょう。

新しいテーブルを作る

もう一度 Adminer を立ち上げて、次の列を持つ comments のテーブルを作りましょう。

  • id int。autoincrement(AI)にチェックを入れます
  • post_idposts テーブルを参照する外部キー
  • 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 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', 'メール:');

	$form->addTextArea('content', 'コメント:')
		->setRequired();

	$form->addSubmit('send', 'コメントを投稿');

	return $form;
}

手短に説明しましょう。1 行めは Form コンポーネントの新しいインスタンスを作ります。続くメソッドは、フォームの定義に HTML の入力を取り付けます。->addText<label>お名前:</label> の付いた <input type="text" name="name"> として描かれます。ご想像のとおり、->addTextArea<textarea> を、->addSubmit<input type="submit"> を足します。似たメソッドはたくさんありますが、今はこれで十分です。詳しくはドキュメントで学べます

フォームのコンポーネントをプレゼンターで定義したら、テンプレートでそれを描け(表示でき)ます。そのために、記事ひとつを表示するテンプレート Post/show.latte の終わりに {control} のタグを置きます。このコンポーネントの名前は(メソッド名 createComponentCommentForm から来て)commentForm なので、タグは次のようになります。

...
<h2>新しいコメントを投稿</h2>

{control commentForm}

これで記事の詳細のページを見ると、終わりに新しいコメントのフォームが見えます。

データベースに保存する

フォームを埋めて送ってみましたか。フォームがまだ実は何もしないことに気づいたかもしれません。送られたデータを保存するコールバックのメソッドを取り付ける必要があります。

commentForm コンポーネントのファクトリメソッドの return の文の前に、次の行を足します。

$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 のテーブルに入れます。

説明すべきメソッドの呼び出しがあと 2 つあります。redirect('this') メソッドは文字どおり今のページへリダイレクトし直します。フォームが正しく送信されてコールバックの処理が終わったら、毎回これをすべきです。フォームの送信後のリダイレクトは、ブラウザでおなじみの「送信したデータをもう一度送りますか」というメッセージも防ぎます。(一般に POST メソッドでフォームを送信したあとは、いつも GET のアクションへリダイレクトすべきです。)

flashMessage メソッドは、処理の結果を利用者に知らせるのに使います。リダイレクトするので、このメッセージをそのままテンプレートへ渡して描くことはできません。このメソッドはメッセージを保存し、次のページの読み込みのときに使えるようにします。フラッシュメッセージは主の layout のテンプレート app/Presentation/@layout.latte で、次のように描かれます。

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

すでに知っているとおり、フラッシュメッセージはテンプレートへ自動的に渡されるので、あまり考える必要はありません。ただ動きます。詳しくはドキュメントをご覧ください

コメントを描く

これはきっと大好きになる機能のひとつです。Nette Database には Explorerという素晴らしい機能があります。データベースのテーブルをわざと InnoDB のエンジンで作ったのを覚えていますか。Adminer はいわゆる外部キーを作り、それが多くの手間を省いてくれます。

Nette Database Explorer はこれらの外部キーを使って、テーブルどうしの関係を読み解きます。その関係を知っているので、データベースのクエリを自動的に作れるのです。

思い出してください。PostPresenter::renderShow() でテンプレートへ $post の変数を渡しました。今度は 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 のタグそのものにだけ当たります。おかげで、コメントした人がメールアドレスを書いた場合にだけ、その名前をリンクにできます。次の 2 行はまったく同じ結果を生みます。

<strong n:tag-if="$important"> Hello there! </strong>

{if $important}<strong>{/if} Hello there! {if $important}</strong>{/if}