Form Validation

Rules

We add validation rules to fields with the addRule() method. The first parameter is the rule, the second is the error message, and the third is the validation rule argument.

$form->addPassword('password', 'Password:')
	->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8)

Nette comes with a number of built-in rules whose names are constants of the Nette\Forms\Form class:

We can use the following rules for all fields:

constant description arguments
REQUIRED alias of setRequired()
FILLED alias of setRequired()
BLANK must not be filled
EQUAL value is equal to parameter mixed
NOT_EQUAL value is not be equal to parameter mixed
IS_IN value is equal to some element in the array array
IS_NOT_IN value does not equal any element in the array array
VALID input passes validation (for conditions)

For fields addText(), addPassword(), addTextArea(), addEmail(), addInteger() the following rules can also be used:

MIN_LENGTH minimal string length int
MAX_LENGTH maximal string length int
LENGTH length in range or exact length pair [int, int] or int
EMAIL valid email address
URL valid URL
PATTERN matches regular pattern string
PATTERN_ICASE like PATTERN, but case-insensitive; since nette/forms v2.4.9 string
INTEGER integer
NUMERIC alias of INTEGER
FLOAT integer or floating point number
MIN minimum of the integer value int|float
MAX maximum of the integer value int|float
RANGE value in the range pair [int|float, int|float]

The INTEGER, NUMERIC a FLOAT rules automatically convert the value to integer (or float respectively). Furthermore, the URL rule also accepts an address without a schema (eg nette.org) and completes the schema (http://nette.org). The expressions in PATTERN and PATTERN_ICASE must be valid for the whole value, ie as if it were wrapped in the characters ^ and $.

For fields addUpload(), addMultiUpload() the following rules can also be used:

MAX_FILE_SIZE maximal file size int
MIME_TYPE MIME type, accepts wildcards ('video/*') string|string[]
IMAGE uploaded file is JPEG, PNG, GIF, WebP
PATTERN file name matches regular expression; since nette/forms v2.4.9 string
PATTERN_ICASE like PATTERN, but case-insensitive; since nette/forms v2.4.9 string

The MIME_TYPE and IMAGE require PHP extension fileinfo.

For fields addMultiUpload(), addCheckboxList(), addMultiSelect() the following rules can also be used:

MIN_LENGTH minimal files count int
MAX_LENGTH maximal files count int
LENGTH count of uploaded files in range or exact count pair [int, int] or int

All rules can be negated with ~ (a tilde), i.e. addRule(~$form::NUMBER, ...) passes validation if field is not filled, which is deprecated since Nette 3, so you need to use another corresponding rule (for example opposite of FILLED is BLANK).

Error Messages

All predefined rules except PATTERN and PATTERN_ICASE have a default error message, so they can be omitted for the addRule() and setRequired() methods. However, by passing and formulating all customized messages, you will make the form more user-friendly.

You can change the default messages in configuration, by modifing the texts in the Nette\Forms\Validator::$messages array or by using translator.

The following wildcards can be used in the text of error messages:

%d gradually replaces the rules after the arguments
n$%d replaces with the nth rule argument
%label replaces with field label (without colon)
%name replaces with field name (eg name)
%value replaces with value entered by the user
$form->addText('name', 'Jméno:')
	->setRequired('Vyplňte prosím %label');

Conditions

Besides validation rules, conditions can be set. They are set much like rules, yet we use addRule() instead of addCondition() and of course, we leave it without an error message (the condition just asks):

$form->addPassword('password', 'Password:')
	// if password is not longer than 8 characters ...
	->addCondition($form::MAX_LENGTH, 8)
		// ... then it must contain a number
		->addRule($form::PATTERN, 'Must contain number', '.*[0-9].*');

Condition can be linked to a different element than the current one using addConditionOn(). The first parameter is a reference to the field. In the following case, the email will only be required if the checkbox is checked (ie. its value is true):

$form->addCheckbox('newsletters', 'send me newsletters');

$form->addEmail('email', 'Email:')
	// if checkbox is checked ...
	->addConditionOn($form['newsletters'], $form::EQUAL, true)
		// ... require email
		->setRequired('Fill your email address');

Conditions can be grouped into complex structures with elseCondition() and endCondition() methods.

$form->addText(...)
	->addCondition(...) // if the first condition is met
		->addConditionOn(...) // and the second condition on another element too
			->addRule(...) // require this rule
		->elseCondition() // if the second condition is not met
			->addRule(...) // require these rules
			->addRule(...)
		->endCondition() // we return to the first condition
		->addRule(...)

All conditions can be negated with ~ (a tilde), i.e. addCondition(~$form::NUMBER, ...) passes validation if field is not filled, which is deprecated since Nette 3, so you need to use else-condition.

References Between Fields

The rule or condition argument can be a reference to another element. For example, you can dynamically validate that the text has as many characters as the value of the length field is:

$form->addInteger('length');
$form->addText('text')
	->addRule($form::LENGTH, null, $form['length']);

Custom Rules

We can create our own rules or conditions. As a rule, we pass any PHP callback.

class MyValidators
{
	// tests whether the value is divisible by an argument
	public static function divisibilityValidator(BaseControl $input, $arg)
	{
		return $input->getValue() % $arg === 0;
	}
}

$input->addRule(
	'MyValidator::divisibilityValidator',
	'Number must be multiple of %d',
	8
);

Modifying Input Values

Using the addFilter() method, we can modify the value entered by the user. In this example, we will tolerate and remove spaces in the zip code:

$form->addText('zip', 'Postcode:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // remove spaces from the postcode
	})
	->addRule($form::PATTERN, 'The postal code is not five digits', '\d{5}');

The filter is performed within the validation rules, so it depends on the order of the individual methods.

JavaScript

The language of validation rules and conditions is powerful. Even though all constructions work both server-side and client-side, in JavaScript. Rules are transferred in HTML attributes data-nette-rules as JSON. The validation itself is handled by another script, which hooks all submit events, iterates over all inputs and runs respective validations.

This script is netteForms.js, which is available from several possible sources:

Via npm installation:

npm install nette-forms

And usage:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternatively, you can load it directly from the vendor folder:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

You can also embed the script directly into the HTML page and load it from the CDN:

<script src="https://nette.github.io/resources/js/2/netteForms.min.js"></script>

Or copy it locally to the public folder of the project (ie from vendor/nette/forms/src/assets/netteForms.min.js):

<script src="/path/to/netteForms.min.js"></script>

Custom Rules and Conditions in JS

Custom validation rules can also be added to JavaScript. The condition is that the callback of the custom rule on the PHP side is a function or a static method. Then we add it to the JS object Nette.validators:

<script>
Nette.validators.divisibilityValidator = function(elem, args, val) {
	return val % args === 0;
};
</script>

If PHP validation callback is a static method, we derive the name for JavaScript by removing all backslashes \ and by replacing the double colon by single underscore _, e.g. App\MyValidator::divisibilityValidator is converted to AppMyValidator_divisibilityValidator.

Whole Form Validation

If you need custom validation functionality, you can define your own validation functions for the whole form and bind them to onValidate event. Typically, this is used to validate a combination of values:

protected function createComponentSignInForm()
{
	$form = new Form;
	...
	$form->onValidate[] = [$this, 'validateSignInForm'];
	return $form;
}

public function validateSignInForm($form)
{
	$values = $form->getValues();

	if (...) { // validation logic
		$form->addError('Password does not match.');
	}
}

You can bind multiple functions to the event. The function is considered successful if it doesn't add any error using $form->addError().

Disabling Validation

In certain cases, you need to disable validation. If a submit button isn't supposed to run validation after submitting (for example Cancel or Preview button), you can disable the validation by calling $submit->setValidationScope([]). You can also validate the form partially by specifying items to be validated.

$form->addText('name')->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')->setRequired('age');
$details->addInteger('age2')->setRequired('age2');

$form->addSubmit('send1'); // Validates the whole form
$form->addSubmit('send2')->setValidationScope(false); // Validates nothing
$form->addSubmit('send3')->setValidationScope([$form['name']]); // Validates only 'name' input
$form->addSubmit('send4')->setValidationScope([$form['details']['age']]); // Validates only 'age' input
$form->addSubmit('send5')->setValidationScope([$form['details']]); // Validates 'details' container

onValidate event on the form is always invoked and is not affected by the setValidationScope. onValidate event on the container is invoked only when this container is specified for partial validation.

Postprocessing Errors

We often find that the user's input is wrong even though each of the form's elements is technically valid. For example, stumbling upon a duplicate key when inserting a new database row. Or invalid login credentials. In that case, we can add the error manually with addError() method. It can be called either on a specific input or on a form itself:

try {
	$values = $form->getValues();
	$this->user->login($values->username, $values->password);
	$this->redirect('Homepage:');

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\IAuthenticator::INVALID_CREDENTIAL) {
		$form->addError('Invalid password.');
	}
}

It's recommended to link the error directly to a form element because then the error will be displayed next to it when using default renderer.

$form['date']->addError("We apologize but this date has been already taken.");

Related blog posts