Skip to main content

Forms for Data Input

User input is an important part of applications. For user input we need forms. In n2n, form models are responsible for creating and processing forms.

The Form Model​

Form data is managed by form models in n2n.

Create the form model app\qs\model\BlogCommentForm.php with the following content:

<?php
namespace qs\model;

use n2n\web\dispatch\Dispatchable;
use qs\bo\BlogArticle;
use n2n\web\dispatch\map\bind\BindingDefinition;
use n2n\impl\web\dispatch\map\val\ValNotEmpty;
use n2n\impl\web\dispatch\map\val\ValEmail;
use n2n\impl\web\dispatch\map\val\ValImageFile;
use n2n\io\managed\File;
use n2n\web\dispatch\map\MappingResult;

class BlogCommentForm implements Dispatchable {
public BlogArticle $blogArticle;

public string $email;
public File $image = null;
public string $content;

public function __construct(BlogArticle $blogArticle) {
$this->blogArticle = $blogArticle;
}

private function _mapping(MappingResult $mr) {
$mr->setLabel('email', 'E-Mail');
$mr->setLabel('content', 'Your comment');
$mr->setLabel('image', 'Your image');
}

private function _validation(BindingDefinition $bd) {
$bd->val(array('content', 'email'), new ValNotEmpty());
$bd->val('email', new ValEmail());
$bd->val('image', new ValImageFile(true));
}

public function save(BlogDao $blogDao) {
$blogDao->saveComment($this->blogArticle, $this->email, $this->content, $this->image);
}

}

A form model implements the interface Dispatchable. Form fields must be marked either as protected or public. If they are protected, getter and setter methods are strictly necessary, so that the fields can be read out or overwritten respectively.

Since a comment always belongs to a BlogArticle, we create a constructor that necessarily expects the associated BlogArticle to be passed. We store $blogArticle in the corresponding property.

Form Mapping​

A form model can contain the private method _mapping(). In the mapping labels can be assigned to the properties ($mr->setLabel()). These labels are then used in the form view or in error messages.

Form Validation​

A form model can contain the private method _validation(). This is processed before the dispatch method. Only if the validation does not fail is the dispatch method executed. The BindingDefinition is passed to the _validation() method via dependency injection. The required validators are then registered on it:

  1. ValNotEmpty: ensures that the fields (content, email) are not empty.
  2. ValEmail: checks whether the field is a valid e-mail address.
  3. ValImageFile: checks whether the file is a valid image.

It is possible to pass your own error messages to the validators. But for the validators defined in n2n, error messages are already stored. Note that the error messages appear in the language stored in the app.ini.

Attention

With user generated content (UGC) it is extremely important that all input is validated precisely. Validation is especially important when files can be uploaded.

Dispatch Method​

In our case save() is the dispatch method. Here we pass the BlogDao and save the new comment by falling back on the properties of the form model (i.e. the entered form values).

Integration in the Controller​

Now we build the form model into our controller. To do so we create a new action method in BlogController, which provides a comment form:

public function doComment(int $blogId) {
// fetch the article by ID
$blogArticle = $this->blogDao->getBlogArticleById($blogId);
// check whether the article was found
if ($blogArticle === null) {
throw new PageNotFoundException('invalid id: ' . $blogId);
}

$this->beginTransaction();
$commentForm = new BlogCommentForm($blogArticle);
if ($this->dispatch($commentForm, 'save')) {
$this->commit();
$this->redirectToController(array('thanks', $blogId));
return;
}
$this->commit();

$this->forward('..\view\comment.html', array('commentForm' => $commentForm));
}

As parameter we expect the $blogId of the article for which the comment is created. First the method checks whether a $blogArticle is found under the given ID.

If this is the case, we start a transaction ($this->beginTransaction()) and create the form model (BlogCommentForm). The controller tries to dispatch it ($this->dispatch()). As parameters we pass the dispatch method the form model ($commentForm) and the name of the dispatch method ('save'). The dispatch is successful if form data is present at all and the validation does not fail. In the success case the transaction is completed ($this->commit()), whereby the new comment is saved. After that the controller redirects to the thank-you page ($this->redirectToController()).

Question

How would $this->redirectToController() have to look so that it redirects directly to the detail page of the article?

If the dispatch is not successful, we can still commit the transaction. In this case $this->commit() would not be strictly necessary, because at the end of a controller method open transactions are completed automatically. After that we call the comment view, to which we pass our form model ($commentForm).

So that we can forward to the comment view at all, we of course have to create it.

Form View​

Create the view app\qs\view\comment.html.php with the following content for our form:

<?php
use n2n\impl\web\ui\view\html\HtmlView;
use qs\model\BlogCommentForm;

$view = HtmlView::view($this);
$formHtml = HtmlView::formHtml($view);

$commentForm = $view->getParam('commentForm');
$view->assert($commentForm instanceof BlogCommentForm);

$view->useTemplate('boilerplate.html', array('title' => 'Your comment'));
?>
<h1>Comment</h1>

<?php $formHtml->open($commentForm) ?>
<?php $formHtml->messageList() ?>
<div>
<?php $formHtml->label('email') ?><br />
<?php $formHtml->input('email', array('maxlength' => 120)) ?>
</div>
<div>
<?php $formHtml->label('content') ?><br />
<?php $formHtml->textarea('content', array('rows' => 5, 'cols' => 30)) ?>
</div>
<div>
<?php $formHtml->label('image')?><br />
<?php $formHtml->inputFileWithLabel('image') ?>
</div>

<?php $formHtml->buttonSubmit('save', 'Save comment')?>
<?php $formHtml->close() ?>

Every HTML view also provides a FormHtmlBuilder $formHtml. We need it to create the form tags. Additionally we load the $commentForm that was passed via the parameters of the view.

A form must be opened ($form->open()) and closed ($form->close()). When opening we always have to pass the form model ($commentForm). This way the FormHtmlBuilder knows which properties ($content, $email, $image) are available.

With $form->messageList() all error messages that the validation produced are output.

After that the different fields and their labels are output. $commentForm already knows the correct labels, because we defined them under _mapping(). But we could also overwrite the labels by passing the second parameter $label to $html->label().

For the submit it is relevant that the dispatch method (in our case 'save') is specified.

Final Work​

At the moment the view just created is not yet linked with the rest of our module. You can change this by appending the following code in app\qs\view\detail.html.php:

<h2>Comments</h2>
<p><?php $html->linkToController(array('comment', $blogArticle->getId()), 'Write a comment') ?></p>

<?php foreach ($blogComments as $blogComment): ?>
<h3><?php $html->linkEmail($blogComment->email) ?></h3>
<p><?php $html->escBr($blogComment->content) ?></p>
<p><strong>Date:</strong> <?php $html->l10nDateTime($blogComment->getCreated(), DateTimeFormat::STYLE_LONG) ?></p>
<?php if (null !== ($image = $blogComment->image)): ?>
<?php $html->image($image, ThSt::prop(200, 300))?>
<?php endif ?>
<hr />
<?php endforeach ?>

First we added the link to the comment form ($html->linkToController()). Note that here, as provided for in the controller, we pass the ID of the article. This way our form view is linked. Now you can output the comments. To do so you can iterate over all comments with a foreach.

Special Functions of the HtmlBuilder​

For the output of e-mail links the HtmlBuilder offers a special method ($html->linkEmail()).

So that possibly existing line breaks in the comments are output as such, we use the method $html->escBr() instead of $html->out() when outputting the property content. This method replaces line breaks with <br />.

Another peculiarity is the output of the creation date. The HtmlBuilder offers various localization methods for this (they all start with l10n). In our case we use $html->l10nDateTime() and pass the creation date as well as the desired time format. All localization methods take the current locale into account. Because we do not set any locales in our module, the default locale from the app.ini is taken.

Displaying Images​

Before we output an image, we check with if (null !== $image = $blogComment->getImage()) whether one is present at all. Images too can easily be output via the HtmlBuilder ($html->image()). Note that we can pass a ThumbStrategy to the image. A ThumbStrategy determines how the present image should be reduced/enlarged/cropped to the passed format (width, height). Currently ThSt knows the following methods:

  • prop: proportional enlarging/reducing
  • crop: centered, exact-format cropping
  • cropTop: exact-format cropping, aligned to the top

The width, the height and the indication whether the image may be enlarged are passed to each of these methods (enlarging leads to a loss of quality of the image).

Confirmation View​

In the BlogController we still have to adapt the call of the thank-you view:

public function doThanks(int $blogId) {
// fetch the article by ID
$blogArticle = $this->blogDao->getBlogArticleById($blogId);
// check whether the article was found
if ($blogArticle === null) {
throw new PageNotFoundException('invalid id: ' . $blogId);
}

$this->forward('\qs\view\thanks.html', array('blogArticle' => $blogArticle));
}

Now we also expect a $blogId in this method. With it we load $blogArticle and pass it to the view, so that we can link back to the relevant article there.

We also have to adapt our view (app\qs\view\thanks.html.php) a bit:

<?php
use n2n\impl\web\ui\view\html\HtmlView;
use n2n\web\ui\view\View;
use qs\bo\BlogArticle;

$view = HtmlView::view($this);
$html = HtmlView::html($view);

$blogArticle = $view->getParam('blogArticle');
$view->assert($blogArticle instanceof BlogArticle);

$view->useTemplate('boilerplate.html', array('title' => 'Thanks'));
?>
<h1>Comment saved</h1>
<p>Thank you for your comment</p>
<?php $html->linkToController($blogArticle->getUrlPart(), 'back to the article') ?>

But there is nothing here that you do not already know.

Now it is time to really test the module and enter a few comments! We wish you a lot of fun!

The Module for Download​

Here is the finished code of the module. Please do not forget that the module also needs the app.ini entries to be runnable.

Conclusion​

With this the Quickstart is completed. We have demonstrated the most important functions of n2n to you. We hope that we could convince you that n2n is a powerful and flexible framework. It is suitable for simple as well as complex internet projects. But you have not seen the best part yet. It is the seamless integration of Rocket, our CMS.

The next logical step is therefore to continue with the Quickstart of Rocket!