# Nette Forms v2.2
Nette Forms is a comprehensive PHP library for creating, validating, and rendering secure web forms. It provides an object-oriented API for building forms with built-in protection against common web vulnerabilities including Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF). The library handles both server-side validation in PHP and client-side validation in JavaScript, eliminating the need to write validation logic twice.
The framework follows a component-based architecture where forms are containers that hold controls (input fields, buttons, etc.), with support for nested containers, validation rules, conditional logic, and flexible rendering. It integrates seamlessly with the Nette ecosystem including Latte templating and dependency injection, while also working standalone. All form inputs are automatically checked for UTF-8 validity, and multiple-choice controls are verified against forged values during validation.
## Creating a Basic Form
Create a simple registration form with text inputs, validation, and submission handling.
```php
addText('name', 'Name:')
->setRequired('Please fill your name.');
// Add email input with validation rule
$form->addText('email', 'Email:')
->setRequired('Please enter your email.')
->addRule($form::EMAIL, 'Invalid email address');
// Add password input with length validation
$form->addPassword('password', 'Password:')
->setRequired('Choose your password')
->addRule($form::MIN_LENGTH, 'The password is too short: it must be at least %d characters', 8);
// Add submit button
$form->addSubmit('send', 'Register');
// Process form submission
if ($form->isSuccess()) {
$values = $form->getValues();
// $values->name, $values->email, $values->password
// Process registration...
echo "Registration successful!";
}
// Render the form
?>
```
## Adding Form Controls
Add various input types including text, numbers, dates, selections, and file uploads.
```php
addText('username', 'Username:');
$form->addPassword('password', 'Password:');
$form->addTextArea('bio', 'Biography:');
$form->addText('email', 'Email:')
->addRule($form::EMAIL, 'Invalid email address');
$form->addText('age', 'Age:')
->addRule($form::INTEGER, 'Age must be a number');
$form->addHidden('userid');
// Checkbox and radio buttons
$form->addCheckbox('agree', 'I agree to terms');
$form->addCheckboxList('colors', 'Favorite colors:', [
'r' => 'red',
'g' => 'green',
'b' => 'blue',
]);
$form->addRadioList('gender', 'Gender:', [
'm' => 'male',
'f' => 'female',
]);
// Select dropdown with prompt
$countries = [
'us' => 'United States',
'ca' => 'Canada',
'uk' => 'United Kingdom',
];
$form->addSelect('country', 'Country:', $countries)
->setPrompt('Select your country');
// Multi-select dropdown
$form->addMultiSelect('interests', 'Interests:', [
'sports' => 'Sports',
'music' => 'Music',
'tech' => 'Technology',
]);
// File upload with validation
$form->addUpload('avatar', 'Profile Picture:')
->addRule($form::IMAGE, 'Uploaded file must be an image')
->addRule($form::MAX_FILE_SIZE, 'Maximum file size is 2 MB', 2 * 1024 * 1024);
// Multiple file upload
$form->addMultiUpload('photos', 'Photos:');
// Buttons
$form->addSubmit('submit', 'Submit');
$form->addButton('cancel', 'Cancel');
// Set default values
$form->setDefaults([
'username' => 'john_doe',
'userid' => 231,
'agree' => true,
]);
if ($form->isSuccess()) {
$values = $form->getValues();
var_dump($values);
}
echo $form;
```
## Validation Rules
Apply server-side and client-side validation rules with error messages.
```php
addText('name', 'Name:')
->setRequired('Name is required');
// Email validation
$form->addText('email', 'Email:')
->addRule($form::EMAIL, 'Invalid email address');
// String length validation
$form->addText('username', 'Username:')
->addRule($form::MIN_LENGTH, 'Username must be at least %d characters', 3)
->addRule($form::MAX_LENGTH, 'Username must be at most %d characters', 20);
// Range validation for length
$form->addText('code', 'Code:')
->addRule($form::LENGTH, 'Code must be between %d and %d characters', [5, 15]);
// Numeric validation
$form->addText('age', 'Age:')
->addRule($form::INTEGER, 'Age must be a number')
->addRule($form::RANGE, 'Age must be between %d and %d', [18, 100]);
$form->addText('price', 'Price:')
->addRule($form::FLOAT, 'Price must be a decimal number')
->addRule($form::MIN, 'Price must be at least %d', 0)
->addRule($form::MAX, 'Price must be at most %d', 999999);
// Pattern matching (regex)
$form->addText('phone', 'Phone:')
->addRule($form::PATTERN, 'Phone must be in format XXX-XXX-XXXX', '[0-9]{3}-[0-9]{3}-[0-9]{4}');
// URL validation
$form->addText('website', 'Website:')
->addRule($form::URL, 'Invalid URL');
// Password confirmation
$password = $form->addPassword('password', 'Password:')
->setRequired('Choose your password')
->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8);
$form->addPassword('password2', 'Confirm Password:')
->setRequired('Reenter your password')
->addRule($form::EQUAL, 'Passwords do not match', $password);
// File upload validation
$form->addUpload('document', 'Document:')
->addRule($form::MAX_FILE_SIZE, 'Maximum file size is 5 MB', 5 * 1024 * 1024)
->addRule($form::MIME_TYPE, 'Document must be PDF or Word', ['application/pdf', 'application/msword']);
// Multiple file upload validation
$form->addMultiUpload('photos', 'Photos:')
->addRule($form::COUNT, 'Upload between %d and %d photos', [2, 5])
->addRule($form::IMAGE, 'All files must be images');
$form->addSubmit('send', 'Submit');
if ($form->isSuccess()) {
$values = $form->getValues();
echo "Form validated successfully!";
} else {
// Display all errors
foreach ($form->getErrors() as $error) {
echo "
$error
";
}
}
echo $form;
```
## Conditional Validation
Apply validation rules conditionally based on other field values.
```php
addCheckbox('send', 'Ship to address')
->addCondition($form::FILLED) // if checkbox is checked
->toggle('shipping-box'); // toggle element with id="shipping-box"
// Conditional validation: required if checkbox is checked
$form->addText('street', 'Street:')
->addConditionOn($form['send'], $form::FILLED)
->setRequired('Enter your shipping address');
$form->addText('city', 'City:')
->addConditionOn($form['send'], $form::FILLED)
->setRequired('Enter your city');
// Conditional with specific value
$form->addSelect('payment', 'Payment Method:', [
'card' => 'Credit Card',
'paypal' => 'PayPal',
'bank' => 'Bank Transfer',
]);
$form->addText('card_number', 'Card Number:')
->addConditionOn($form['payment'], $form::EQUAL, 'card')
->setRequired('Enter card number')
->addRule($form::PATTERN, 'Invalid card number', '[0-9]{16}');
$form->addText('paypal_email', 'PayPal Email:')
->addConditionOn($form['payment'], $form::EQUAL, 'paypal')
->setRequired('Enter PayPal email')
->addRule($form::EMAIL, 'Invalid email address');
// Chained conditions: if field is filled AND meets criteria
$form->addText('discount_code', 'Discount Code:')
->addCondition($form::FILLED) // if filled
->addRule($form::MIN_LENGTH, 'Code must be at least %d characters', 5);
// Complex condition: if one field is filled, another is required
$form->addText('phone', 'Phone:');
$form->addText('email', 'Email:');
$form->addCondition($form::BLANK, $form['phone'])
->addCondition($form::BLANK, $form['email'])
->addRule(function() { return false; }, 'Fill either phone or email');
$form->addSubmit('submit', 'Submit');
if ($form->isSuccess()) {
$values = $form->getValues();
var_dump($values);
}
?>
Shipping information will appear here
```
## Form Groups and Organization
Organize form controls into logical groups with fieldsets.
```php
addGroup('Personal Information')
->setOption('description', 'Please provide your personal details.');
$form->addText('name', 'Full Name:')
->setRequired('Enter your name');
$form->addText('birthdate', 'Birth Date:');
$form->addRadioList('gender', 'Gender:', [
'm' => 'Male',
'f' => 'Female',
]);
// Create shipping address group
$form->addGroup('Shipping Address')
->setOption('embedNext', true); // embed next group inside this one
$form->addCheckbox('send', 'Ship to address')
->addCondition($form::FILLED)
->toggle('shipping-fields');
// Subgroup with custom container
$form->addGroup()
->setOption('container', Html::el('div')->id('shipping-fields'));
$form->addText('street', 'Street:');
$form->addText('city', 'City:')
->addConditionOn($form['send'], $form::FILLED)
->setRequired('Enter city');
$form->addSelect('country', 'Country:', [
'us' => 'United States',
'ca' => 'Canada',
'uk' => 'United Kingdom',
])->setPrompt('Select country');
// Account settings group
$form->addGroup('Account Settings');
$form->addPassword('password', 'Password:')
->setRequired('Choose password')
->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8);
$form->addPassword('password2', 'Confirm Password:')
->setRequired('Reenter password')
->addRule($form::EQUAL, 'Passwords do not match', $form['password']);
$form->addTextArea('bio', 'Biography:')
->setOption('description', 'Tell us about yourself (optional)');
// Group for buttons (no label)
$form->addGroup();
$form->addSubmit('submit', 'Create Account');
if ($form->isSuccess()) {
$values = $form->getValues();
echo "
Account created successfully!
";
var_dump($values);
}
echo $form;
```
## Nested Containers
Use containers to organize form data hierarchically and create reusable form sections.
```php
addContainer('billing');
$billing->addText('name', 'Name:')->setRequired();
$billing->addText('street', 'Street:')->setRequired();
$billing->addText('city', 'City:')->setRequired();
$billing->addText('zip', 'ZIP:')->setRequired();
// Create container for shipping address
$shipping = $form->addContainer('shipping');
$shipping->addText('name', 'Name:')->setRequired();
$shipping->addText('street', 'Street:')->setRequired();
$shipping->addText('city', 'City:')->setRequired();
$shipping->addText('zip', 'ZIP:')->setRequired();
// Container for contact information
$contact = $form->addContainer('contact');
$contact->addText('email', 'Email:')
->setRequired()
->addRule($form::EMAIL, 'Invalid email address');
$contact->addText('phone', 'Phone:');
$form->addSubmit('submit', 'Submit Order');
// Set default values for containers
$form->setDefaults([
'billing' => [
'name' => 'John Doe',
'city' => 'New York',
],
'contact' => [
'email' => 'john@example.com',
],
]);
if ($form->isSuccess()) {
$values = $form->getValues();
// Access nested values
echo "Billing Name: " . $values->billing->name . "\n";
echo "Billing City: " . $values->billing->city . "\n";
echo "Shipping Name: " . $values->shipping->name . "\n";
echo "Contact Email: " . $values->contact->email . "\n";
// Values are organized hierarchically
var_dump($values);
/*
object(stdClass) {
billing => object(stdClass) {
name => "John Doe"
street => "123 Main St"
city => "New York"
zip => "10001"
}
shipping => object(stdClass) {
name => "Jane Doe"
street => "456 Oak Ave"
city => "Boston"
zip => "02101"
}
contact => object(stdClass) {
email => "john@example.com"
phone => "555-1234"
}
}
*/
}
echo $form;
```
## Custom Validators
Create custom validation functions for specialized validation logic.
```php
getValue() % $divisor === 0;
}
public static function uniqueUsername(BaseControl $control)
{
$username = $control->getValue();
// Check database for existing username
// This is a simplified example
$existingUsers = ['admin', 'test', 'user'];
return !in_array($username, $existingUsers);
}
public static function strongPassword(BaseControl $control)
{
$password = $control->getValue();
// Check for uppercase, lowercase, number, and special character
return preg_match('/[A-Z]/', $password) &&
preg_match('/[a-z]/', $password) &&
preg_match('/[0-9]/', $password) &&
preg_match('/[^A-Za-z0-9]/', $password);
}
}
$form = new Form;
// Use custom validator with parameter
$form->addText('number', 'Multiple of 8:')
->setRequired()
->addRule('MyValidators::divisibilityValidator', 'Number must be divisible by %d', 8);
// Use custom validator without parameter
$form->addText('username', 'Username:')
->setRequired()
->addRule('MyValidators::uniqueUsername', 'Username is already taken');
// Use custom validator for complex password rules
$form->addPassword('password', 'Password:')
->setRequired()
->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8)
->addRule('MyValidators::strongPassword', 'Password must contain uppercase, lowercase, number, and special character');
// Custom validator using closure
$form->addText('promo_code', 'Promo Code:')
->addCondition($form::FILLED)
->addRule(function(BaseControl $control) {
$validCodes = ['SAVE10', 'SAVE20', 'FREESHIP'];
return in_array(strtoupper($control->getValue()), $validCodes);
}, 'Invalid promo code');
$form->addSubmit('submit', 'Submit');
if ($form->isSuccess()) {
$values = $form->getValues();
echo "Validation passed!";
var_dump($values);
}
?>
```
## Manual Form Rendering
Manually render form elements with full control over HTML structure.
```php
addText('name', 'Name:')->setRequired('Enter your name');
$form->addText('age', 'Age:')
->setRequired('Enter your age')
->addRule($form::INTEGER, 'Age must be a number');
$form->addRadioList('gender', 'Gender:', ['m' => 'male', 'f' => 'female']);
$form->addText('email', 'Email:')
->addRule($form::EMAIL, 'Invalid email address');
$form->addSubmit('submit', 'Send');
if ($form->isSuccess()) {
echo '
';
var_dump($form->getValues());
exit;
}
?>
Bootstrap 3 Form
Bootstrap 3 Form
render() ?>
```
## CSRF Protection
Add Cross-Site Request Forgery protection to forms with automatic token generation.
```php
addProtection('Security token has expired, please submit the form again');
// Add form fields
$form->addText('username', 'Username:')
->setRequired();
$form->addPassword('password', 'Password:')
->setRequired();
$form->addSubmit('login', 'Login');
if ($form->isSuccess()) {
$values = $form->getValues();
// Token was validated automatically
echo "Login successful! Token validated.";
// Process login...
} elseif ($form->isSubmitted()) {
// Form submitted but validation failed (possibly invalid token)
echo "Validation failed. Please check for errors.";
}
// The form will automatically include a hidden CSRF token field
echo $form;
/*
Rendered HTML will include:
The token is automatically validated on form submission.
If the token is missing, expired, or invalid, the form will not pass validation.
*/
```
## Event Handlers
Attach callbacks to form events for processing successful submissions and handling errors.
```php
addText('email', 'Email:')
->setRequired()
->addRule($form::EMAIL, 'Invalid email address');
$form->addPassword('password', 'Password:')
->setRequired();
$form->addSubmit('login', 'Login');
// onSuccess: Called when form is submitted and validated successfully
$form->onSuccess[] = function(Form $form) {
$values = $form->getValues();
// Process valid form data
echo "Login successful!\n";
echo "Email: {$values->email}\n";
// Multiple handlers can be attached
// They are called in the order they were added
};
// onSuccess can have multiple handlers
$form->onSuccess[] = function(Form $form) {
$values = $form->getValues();
// Log the successful login
error_log("User {$values->email} logged in");
// Redirect after successful login
// header('Location: /dashboard');
// exit;
};
// onError: Called when form is submitted but validation fails
$form->onError[] = function(Form $form) {
echo "Form has errors:\n";
foreach ($form->getErrors() as $error) {
echo "- $error\n";
}
};
// onSubmit: Called on every form submission (before validation)
$form->onSubmit[] = function(Form $form) {
echo "Form was submitted\n";
// Useful for logging or analytics
};
// Button-specific handlers
$loginButton = $form['login'];
$loginButton->onClick[] = function($button) {
$form = $button->getForm();
$values = $form->getValues();
echo "Login button clicked\n";
};
// Add a second submit button with different handler
$form->addSubmit('register', 'Register Instead');
$form['register']->onClick[] = function($button) {
echo "Register button clicked\n";
// Redirect to registration
// header('Location: /register');
// exit;
};
// Process the form
if ($form->isSubmitted() && $form->isValid()) {
// Handlers are called automatically
// No need to manually call them
}
echo $form;
```
## Working with Form Data
Set default values, retrieve submitted data, and reset forms.
```php
addText('name', 'Name:');
$form->addText('email', 'Email:')
->addRule($form::EMAIL, 'Invalid email address');
$form->addText('age', 'Age:')
->addRule($form::INTEGER, 'Age must be a number');
$form->addCheckbox('subscribe', 'Subscribe to newsletter');
$billing = $form->addContainer('billing');
$billing->addText('street', 'Street:');
$billing->addText('city', 'City:');
$form->addSubmit('submit', 'Submit');
// Set default values for the entire form
$form->setDefaults([
'name' => 'John Doe',
'email' => 'john@example.com',
'age' => 30,
'subscribe' => true,
'billing' => [
'street' => '123 Main St',
'city' => 'New York',
],
]);
// Set values from database or other source
$userData = [
'name' => 'Jane Smith',
'email' => 'jane@example.com',
'age' => 25,
];
$form->setValues($userData, true); // second param: erase fields not in $userData
// Get submitted values (after validation)
if ($form->isSuccess()) {
// Get values as ArrayHash object
$values = $form->getValues();
echo $values->name; // 'Jane Smith'
echo $values->email; // 'jane@example.com'
echo $values->age; // 25
echo $values->subscribe; // true/false
echo $values->billing->city; // 'New York'
// Get values as associative array
$valuesArray = $form->getValues(true);
echo $valuesArray['name']; // 'Jane Smith'
echo $valuesArray['billing']['city']; // 'New York'
// Access individual control values
$name = $form['name']->getValue();
$email = $form['email']->getValue();
// Modify values programmatically
$form['name']->setValue('Modified Name');
}
// Check if form was submitted
if ($form->isSubmitted()) {
echo "Form was submitted\n";
// Check if validation passed
if ($form->isValid()) {
echo "Form is valid\n";
} else {
echo "Form has errors\n";
}
}
// Reset form to defaults
$form->reset();
// Clear all values
$form->setValues([], true);
// Check form errors
if (!$form->isValid()) {
$errors = $form->getErrors();
foreach ($errors as $error) {
echo "Error: $error\n";
}
// Get errors for specific control
$nameErrors = $form['name']->getErrors();
}
// Add custom error to form
if ($form->isSuccess()) {
$values = $form->getValues();
// Check business logic
$existingEmails = ['existing@example.com', 'test@example.com'];
if (in_array($values->email, $existingEmails)) {
$form['email']->addError('Email already registered');
}
if (!$form->hasErrors()) {
// Process form...
}
}
echo $form;
```
## Latte Template Integration
Render forms using Latte templating engine for maximum flexibility.
```php
addGroup('Personal Information');
$form->addText('name', 'Name:')->setRequired();
$form->addText('email', 'Email:')
->setRequired()
->addRule($form::EMAIL, 'Invalid email address');
$form->addGroup('Address');
$form->addText('street', 'Street:');
$form->addText('city', 'City:');
$form->addGroup();
$form->addSubmit('submit', 'Submit');
if ($form->isSuccess()) {
$values = $form->getValues();
echo "Form submitted successfully!";
}
// Render using Latte template
$latte = new Engine;
$latte->render('form-template.latte', ['myForm' => $form]);
```
```latte
{* form-template.latte *}
Form with Latte
Registration Form
{* Render entire form automatically *}
{form myForm}
{* Iterate through groups *}
{foreach $myForm->getGroups() as $group}
{if $group->getOption('label')}
{/if}
{/foreach}
{/form}
{* Alternative: Manual rendering with Latte *}
{form myForm}
{label name /}
{input name}
{inputError name}
{label email /}
{input email}
{inputError email}
{label street /}
{input street}
{label city /}
{input city}
{input submit}
{/form}
{* Render specific parts *}
{form myForm}
{* Form errors *}
{if $myForm->hasErrors()}
{foreach $myForm->getErrors() as $error}
{$error}
{/foreach}
{/if}
{* Individual controls *}
{label name}Full Name:{/label}
{input name, class => 'form-control', placeholder => 'Enter your name'}
{inputError name}
{input submit, class => 'btn btn-primary'}
{/form}
```
## Client-Side JavaScript Validation
Enable automatic client-side validation using the Nette Forms JavaScript library.
```html
Client-Side Validation
addText('username', 'Username:')
->setRequired('Username is required')
->addRule($form::MIN_LENGTH, 'Username must be at least %d characters', 3)
->addRule($form::MAX_LENGTH, 'Username must be at most %d characters', 20);
$form->addText('email', 'Email:')
->setRequired('Email is required')
->addRule($form::EMAIL, 'Invalid email format');
$form->addText('age', 'Age:')
->setRequired()
->addRule($form::INTEGER, 'Age must be a number')
->addRule($form::RANGE, 'Age must be between %d and %d', [18, 100]);
$form->addPassword('password', 'Password:')
->setRequired()
->addRule($form::MIN_LENGTH, 'Password must be at least %d characters', 8)
->addRule($form::PATTERN, 'Password must contain letters and numbers', '.*[0-9].*[a-zA-Z]|.*[a-zA-Z].*[0-9].*');
// Conditional validation also works on client-side
$form->addCheckbox('shipping', 'Ship to different address')
->addCondition($form::FILLED)
->toggle('shipping-address'); // Shows/hides element with id="shipping-address"
$form->addText('shipping_address', 'Shipping Address:')
->addConditionOn($form['shipping'], $form::FILLED)
->setRequired('Enter shipping address');
$form->addSubmit('submit', 'Submit');
// The form will be validated on submit before being sent to server
// Invalid fields will be highlighted and error messages displayed
echo $form;
?>
Shipping address field appears here
```
---
## Summary
Nette Forms v2.2 provides a complete solution for web form handling in PHP applications, combining security, usability, and flexibility. The library is commonly used in Nette Framework applications but works standalone with any PHP project. Primary use cases include user registration and authentication forms, data collection and surveys, e-commerce checkout processes, admin panels and content management interfaces, and any scenario requiring secure form validation. The dual validation system ensures data integrity while providing immediate user feedback through client-side validation.
Integration patterns include standalone usage by simply instantiating the Form class and rendering it, Nette Application integration through presenters with automatic request handling, Latte template integration for advanced rendering control with template macros, and Bootstrap/CSS framework integration through renderer customization. The library's architecture allows for extending controls with custom input types, creating reusable form factories, building dynamic forms based on database schemas, and implementing multi-step forms with session persistence. With built-in CSRF protection, XSS prevention, and comprehensive validation, Nette Forms handles security concerns automatically while remaining highly customizable for specific application needs. Version 2.2 requires PHP 5.3.1+ and uses `addText()` with validation rules for specialized inputs like email and integers, rather than the convenience methods introduced in later versions.