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 theposts
tablename
varchar, length 255email
varchar, length 255content
textcreated_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}