Forms Used Standalone

Nette Forms make it dramatically easier to create and process web forms. You can use them in your applications completely on their own without the rest of the framework, which we'll demonstrate in this chapter.

However, if you use Nette Application and presenters, there is a guide for you: forms in presenters.

First Form

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

use Nette\Forms\Form;

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

And let's render it:

$form->render();

and the result should look like this:

The form is an object of the Nette\Forms\Form class (the Nette\Application\UI\Form class is used in presenters). We added the controls name, password and sending button to it.

Now we will revive the form. By asking $form->isSuccess(), we will find out whether the form was submitted and whether it was filled in validly. If so, we will dump the data. After the definition of the form we will add:

if ($form->isSuccess()) {
	echo 'The form has been filled in and submitted correctly';
	$data = $form->getValues();
	// $data->name contains name
	// $data->password contains password
	var_dump($data);
}

Method getValues() returns the sent data in the form of an object ArrayHash. We will show how to change this later. The variable $data contains keys name and password with 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.');

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.

By default, the form is sent using the POST method to the same page. Both can be changed:

$form->setAction('/submit.php');
$form->setMethod('GET');

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

Try adding more form controls.

Access to Controls

The form and its individual controls are called components. They create a component tree, where the root is the form. You can access the individual controls as follows:

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

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

Controls are removed using unset:

unset($form['name']);

Validation Rules

The word valid was used here, 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 enter a 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. Add it to the page:

<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 form data processing. Method getValues() returned the submitted 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 to us as objects of this class? Easier than you think. All you have to do is specify the class name or object to hydrate as a parameter:

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

An 'array' can also be specified as a parameter, and then the data returns as an array.

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. The method isSubmittedBy() of the button returns this information to us:

$form->addSubmit('save', 'Save');
$form->addSubmit('delete', 'Delete');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

Do not omit the $form->isSuccess() to verify the validity of the data.

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

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.

In addition to protecting the forms against attack 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. Therefore, create a form before flushing the first output so that the cookie can be sent.

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().

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