Forms in Presenters

Nette Forms make it dramatically easier to create and process web forms. In this chapter, you will learn how to use forms inside presenters.

If you are interested in using them completely standalone without the rest of the framework, there is a guide for standalone forms.

First Form

We will try to write a simple registration form. Its code will look like this:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];

and in the browser the result should look like this:

The form in the presenter is an object of the class Nette\Application\UI\Form, its predecessor Nette\Forms\Form is intended for standalone use. We added the fields name, password and sending button to it. Finally, the line with $form->onSuccess says that after submission and successful validation, the $this->formSucceeded() method should be called.

From the presenter's point of view, the form is a common component. Therefore, it is treated as a component and incorporated into the presenter using factory method. It will look like this:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// here we will process the data sent by the form
		// $data->name contains name
		// $data->password contains password
		$this->flashMessage('You have successfully signed up.');
		$this->redirect('Home:');
	}
}

And render in template is done using {control} tag:

<h1>Registration</h1>

{control registrationForm}

And that is all :-) We have a functional and perfectly secured form.

Now you're probably thinking that it was too fast, wondering how it's possible that the method formSucceeded() is called and what the parameters it gets. Sure, you're right, this deserves an explanation.

Nette comes up with a cool mechanism, which we call Hollywood style. Instead of having to constantly ask if something has happened (“was the form submitted?”, “was it validly submitted?” or “was it not forged?”), you say to the framework “when the form is validly completed, call this method” and leave further work on it. If you program in JavaScript, you are familiar with this style of programming. You write functions that are called when a certain event occurs. And the language passes the appropriate arguments to them.

This is how the above presenter code is built. Array $form->onSuccess represents the list of PHP callbacks that Nette will call when the form is submitted and filled in correctly. Within the presenter's life cycle it is a so-called signal, so they are called after the action* method and before the render* method. And it passes to each callback the form itself in the first parameter and the sent data as object ArrayHash in the second. You can omit the first parameter if you do not need the form object. The second parameter can be even more handy, but about that later.

The $data object contains the name and password properties with the data entered by the user. Usually we send the data directly for further processing, which can be, for example, insertion into the database. However, an error may occur during processing, for example, the username is already taken. In this case, we pass the error back to the form using addError() and let it redrawn, with an error message:

$form->addError('Sorry, username is already in use.');

In addition to onSuccess, there is also onSubmit: callbacks are always called after the form is submitted, even if it is not filled in correctly. And finally onError: callbacks are called only if the submission is not valid. They are even called if we invalidate the form in onSuccess or onSubmit using addError().

After processing the form, we will redirect to the next page. This prevents the form from being unintentionally resubmitted by clicking the refresh, back button, or moving the browser history.

Try adding more form controls.

Access to Controls

The form is a component of the presenter, in our case named registrationForm (after the name of the factory method createComponentRegistrationForm), so anywhere in the presenter you can get to the form using:

$form = $this->getComponent('registrationForm');
// alternative syntax: $form = $this['registrationForm'];

Also individual form controls are components, so you can access them in the same way:

$input = $form->getComponent('name'); // or $input = $form['name'];
$button = $form->getComponent('send'); // or $button = $form['send'];

Controls are removed using unset:

unset($form['name']);

Validation Rules

The word valid was used several times, but the form has no validation rules yet. Let's fix it.

The name will be mandatory, so we will mark it with the method setRequired(), whose argument is the text of the error message that will be displayed if the user does not fill it. If no argument is given, the default error message is used.

$form->addText('name', 'Name:')
	->setRequired('Please fill your name.');

Try to submit the form without the name filled in and you will see that an error message is displayed and the browser or server will reject it until you fill it.

At the same time, you will not be able cheat the system by typing only spaces in the input, for example. No way. Nette automatically trims left and right whitespace. Try it. It's something you should always do with every single-line input, but it's often forgotten. Nette does it automatically. (You can try to fool the forms and send a multiline string as the name. Even here, Nette won't be fooled and the line breaks will change to spaces.)

The form is always validated on the server side, but JavaScript validation is also generated, which is quick and the user knows of the error immediately, without having to send the form to the server. This is handled by the script netteForms.js. Insert it into the layout template:

<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></script>

If you look in the source code of the page with form, you may notice that Nette inserts the required fields into elements with a CSS class required. Try adding the following style to the template, and the “Name” label will be red. Elegantly, we mark the required fields for the users:

<style>
.required label { color: maroon }
</style>

Additional validation rules will be added by method addRule(). The first parameter is rule, the second is again the text of the error message, and the optional validation rule argument can follow. What does that mean?

The form will get another optional input age with the condition, that it has to be a number (addInteger()) and in certain boundaries ($form::Range). And here we will use the third argument of addRule(), the range itself:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);

If the user does not fill in the field, the validation rules will not be verified, because the field is optional.

Obviously room for a small refactoring is available. In the error message and in the third parameter, the numbers are listed in duplicate, which is not ideal. If we were creating a multilingual form and the message containing numbers would have to be translated into multiple languages, it would make it more difficult to change values. For this reason, substitute characters %d can be used:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Let's return to the password field, make it required, and verify the minimum password length ($form::MinLength), again using the substitute characters in the message:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);

We will add a field passwordVerify to the form, where the user enters the password again, for checking. Using validation rules, we check whether both passwords are the same ($form::Equal). And as an argument we give a reference to the first password using square brackets:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();

Using setOmitted(), we marked an element whose value we don't really care about and which exists only for validation. Its value is not passed to $data.

We have a fully functional form with validation in PHP and JavaScript. Nette's validation capabilities are much broader, you can create conditions, display and hide parts of a page according to them, etc. You can find out everything in the chapter on form validation.

Default Values

We often set default values for form controls:

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

It is often useful to set default values for all controls at once. For example, when the form is used to edit records. We read the record from the database and set it as default values:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Call setDefaults() after defining the controls.

Rendering the Form

By default, the form is rendered as a table. The individual controls follows basic web accessibility guidelines. All labels are generated as <label> elements and are associated with their inputs, clicking on the label moves the cursor on the input.

We can set any HTML attributes for each element. For example, add a placeholder:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

There are really a lot of ways to render a form, so it's dedicated chapter about rendering.

Mapping to Classes

Let's go back to the formSucceeded() method, which in the second parameter $data gets the sent data as an ArrayHash object. Because this is a generic class, something like stdClass, we will lack some convenience when working with it, such as code completition for properties in editors or static code analysis. This could be solved by having a specific class for each form, whose properties represent the individual controls. E.g.:

class RegistrationFormData
{
	public string $name;
	public int $age;
	public string $password;
}

As of PHP 8.0, you can use this elegant notation that uses a constructor:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public int $age,
		public string $password,
	) {
	}
}

How to tell Nette to return data as objects of this class? Easier than you think. All you have to do is specify the class as the type of the $data parameter in the handler:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name is instance of RegistrationFormData
	$name = $data->name;
	// ...
}

You can also specify array as the type and then it will pass the data as an array.

In a similar way, you can use the getValues() method, which we pass as a class name or object to hydrate as a parameter:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

If the forms consist of a multi-level structure composed of containers, create a separate class for each one:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public int $age;
	public string $password;
}

The mapping then knows from the $person property type that it should map the container to the PersonFormData class. If the property would contain an array of containers, provide the array type and pass the class to be mapped directly to the container:

$person->setMappedType(PersonFormData::class);

You can generate a proposal for the data class of a form using the method Nette\Forms\Blueprint::dataClass($form), which will print it out to the browser page. You can then simply click to select and copy the code into your project.

Multiple Submit Buttons

If the form has more than one button, we usually need to distinguish which one was pressed. We can create own function for each button. Set it as a handler for the onClick event:

$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

These handlers are also called only in the case form is valid, as in the case of the onSuccess event. The difference is that the first parameter can be the submit button object instead of the form, depending on the type you specify:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

When a form is submitted with the Enter key, it is treated as if it had been submitted with the first button.

Event onAnchor

When you build a form in a factory method (such as createComponentRegistrationForm), it doesn't yet know if it has been submitted or the data it was submitted with. But there are cases where we need to know the submitted values, perhaps it's depend on them what the form will look like, or they are used for dependent selectboxes, etc.

Therefore, you can have the code that builds the form called when it is anchored, i.e. it is already linked to the presenter and knows its submitted data. We will put such code into the $onAnchor array:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// this function will be called when the form knows data it was submitted with
	// so you can use the getValue() method
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Vulnerability Protection

Nette Framework puts a great effort to be safe and since forms are the most common user input, Nette forms are as good as impenetrable. All is maintained dynamically and transparently, nothing has to be set manually.

In addition to protecting the forms against attacks targeted at well-known vulnerabilities such as Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF), it does a lot of small security tasks that you no longer have to think about.

For example, it filters out all control characters from the inputs and checks the validity of the UTF-8 encoding, so that the data from the form will always be clean. For select boxes and radio lists, it verifies that the selected items were actually from the offered ones and there was no forgery. We've already mentioned that for single-line text input, it removes end-of-line characters that an attacker could send there. For multiline inputs, it normalizes the end-of-line characters. And so on.

Nette fixes security vulnerabilities for you that most programmers have no idea exist.

The mentioned CSRF attack is that an attacker lures the victim to visit a page that silently executes a request in the victim's browser to the server where the victim is currently logged in, and the server believes that the request was made by the victim at will. Therefore, Nette prevents the form from being submitted via POST from another domain. If for some reason you want to turn off protection and allow the form to be submitted from another domain, use:

$form->allowCrossOrigin(); // ATTENTION! Turns off protection!

This protection uses a SameSite cookie named _nss. SameSite cookie protection may not be 100% reliable, so it's a good idea to turn on token protection:

$form->addProtection();

It's strongly recommended to apply this protection to the forms in an administrative part of your application which changes sensitive data. The framework protects against a CSRF attack by generating and validating authentication token that is stored in a session (the argument is the error message shown if the token has expired). That's why it is necessary to have an session started before displaying the form. In the administration part of the website, the session is usually already started, due to the user's login. Otherwise, start the session with the method Nette\Http\Session::start().

Using One Form in Multiple Presenters

If you need to use one form in more than one presenter, we recommend that you create a factory for it, which you then pass on to the presenter. A suitable location for such a class is, for example, the directory app/Forms.

The factory class might look like this:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}

We ask the class to produce the form in the factory method for components in the presenter:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// we can change the form, here for example we change the label on the button
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // and add handler
	return $form;
}

The form processing handler can also be delivered from the factory:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		$form->onSuccess[] = function (Form $form, $data): void {
			// we process our submitted form here
		};
		return $form;
	}
}

So, we have a quick introduction to forms in Nette. Try looking in the examples directory in the distribution for more inspiration.

version: 4.0 3.x 2.x