Comments

The blog has been deployed, we’ve written some very good blog posts and published them via Adminer. People are reading the blog and they’re very passionate about our ideas. We’re receiving many emails with praise each day. But what is all the praise for when we’ve got it only in the email, so no one else can read it? Wouldn’t it be better if people could comment directly on the blog so that everyone else could read how awesome we are?

Let’s make all the articles commentable.

Creating a New Table

Fire up Adminer again and create a new table named comments with these columns:

  • id int, check autoincrement (AI)
  • post_id, a foreign key that references the posts table
  • name varchar, length 255
  • email varchar, length 255
  • content text
  • created_at timestamp

It should look like this:

Don’t forget to use InnoDB table storage and hit Save.

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;

Form for Commenting

First, we need to create a form, that will allow the users to comment on our page. Nette Framework has awesome support for forms. They can be configured in a presenter and rendered in a template.

Nette Framework has a concept of components. A component is a reusable class or piece of code, that can be attached to another component. Even a presenter is a component. Each component is created using the component factory. So let’s define the comments form factory in PostPresenter.

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

	$form->addText('name', 'Your name:')
		->setRequired();

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

	$form->addTextArea('content', 'Comment:')
		->setRequired();

	$form->addSubmit('send', 'Publish comment');

	return $form;
}

Let’s explain it a little bit. The first line creates a new instance of Form component. The following methods are attaching HTML inputs into the form definition. ->addText will render as <input type=text name=name>, with <label>Your name:</label>. As you might have already guessed right now, the ->addTextArea attaches a <textarea> and ->addSubmit adds an <input type=submit>. There are more methods like that, but this is all you have to know right now. You can learn more in the documentation.

Once the form component is defined in a presenter, we can render (display) it in a template. To do so, place the tag {control} at the end of the post detail template, in Post/show.latte. Because the component's name is commentForm (it's derived from the name of the method createComponentCommentForm), the tag will look like this

...
<h2>Post new comment</h2>

{control commentForm}

Now if you check out the detail of some post, there will be a new form for posting comments.

Saving to Database

Have you tried to submit some data? You might have noticed, that the form is not performing any action. It’s just there, looking cool and doing nothing. We have to attach a callback method to it, that will save the submitted data.

Place the following line before the return line in the component factory for the commentForm:

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

It means “after the form is successfully submitted, call the method commentFormSucceeded of the current presenter”. This method does not exist yet, so let’s create it.

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('Thank you for your comment', 'success');
	$this->redirect('this');
}

You should place it right after the commentForm component factory.

The new method has one argument which is the instance of the form being submitted, created by the component factory. We receive submitted values in $data. And then we insert the data into the database table comments.

There are two more method calls to explain. The redirect literally redirects to the current page. You should do that every time when the form is submitted, valid, and the callback operation did what it should have done. Also, when you redirect the page after submitting the form, you won’t see the well known Would you like to submit the post data again? message that you can sometimes see in the browser. (In general, after submitting a form by POST method, you should always redirect the user to a GET action.)

The flashMessage is for informing the user about the result of some operation. Because we’re redirecting, the message cannot be directly passed to the template and rendered. So there is this method, that will store it and make it available on the next page load. The flash messages are rendered in the default app/UI/@layout.latte file, and it looks like this:

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

As we already know, they are passed automatically to the template, so you don’t have to think about it too much, it just works. For more details check the documentation.

Rendering the Comments

This is one of the things you will just love. Nette Database has this cool feature named Explorer. Do you remember that we’ve created the tables as InnoDB? Adminer has created the so-called foreign keys that will save us a ton of work.

Nette Database Explorer uses the foreign keys to resolve relations between tables, and knowing the relations, it can automatically create queries for you.

As you may remember, we’ve passed the $post variable to the template in PostPresenter::renderShow() and now we want to iterate through all the comments that have the column post_id equal to our $post->id. You can do it by calling $post->related('comments'). It’s that simple. Look at the resulting code.

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

And the template:

...
<h2>Comments</h2>

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

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

Notice the special n:tag-if attribute. You already know how n: attributes work. Well, If you prepend the attribute with tag-, it will only wrap around the tags, not their content. This allows you to make the commenter's name into a link if they provided their email. These two lines are identical in results:

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

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