Validazione dei form

Controlli obbligatori

I controlli si contrassegnano come obbligatori con il metodo setRequired(). Il suo argomento è il testo del messaggio di errore che verrà mostrato se l'utente non compila il controllo. Se non viene indicato alcun argomento, viene usato il messaggio di errore predefinito.

$form->addText('name', 'Nome:')
	->setRequired('Inserite il vostro nome.');

Regole

Aggiungiamo regole di validazione ai controlli con il metodo addRule(). Il primo parametro è la regola, il secondo è il messaggio di errore e il terzo è l'argomento della regola di validazione.

$form->addPassword('password', 'Password:')
	->addRule($form::MinLength, 'La password deve essere lunga almeno %d caratteri', 8);

Le regole di validazione vengono controllate solo se l'utente ha compilato il controllo.

Nette porta con sé diverse regole predefinite, i cui nomi sono costanti della classe Nette\Forms\Form. Possiamo applicare queste regole a tutti i controlli:

costante descrizione tipo dell'argomento
Required controllo obbligatorio, alias di setRequired()
Filled controllo obbligatorio, alias di setRequired()
Blank il controllo non deve essere compilato
Equal il valore deve essere uguale al parametro mixed
NotEqual il valore non deve essere uguale al parametro mixed
IsIn il valore deve essere uno degli elementi dell'array array
IsNotIn il valore non deve essere nessuno degli elementi dell'array array
Valid il controllo è compilato correttamente? (solo in addConditionOn())

Campi di testo

Ai controlli addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat() si possono applicare anche alcune delle regole seguenti:

MinLength lunghezza minima del testo int
MaxLength lunghezza massima del testo int
Length lunghezza in un intervallo o lunghezza esatta coppia [int, int] oppure int
Email indirizzo e-mail valido
URL URL assoluto
Pattern corrisponde a un'espressione regolare string
PatternInsensitive come Pattern, ma senza distinguere maiuscole e minuscole string
Integer valore intero
Numeric intero non negativo (solo cifre)
Float numero
Min valore minimo di un controllo numerico int|float
Max valore massimo di un controllo numerico int|float
Range valore in un intervallo coppia [int|float, int|float]

Le regole di validazione Integer e Float convertono automaticamente il valore rispettivamente in intero o in float. La regola URL accetta inoltre anche un indirizzo senza schema (per esempio nette.org) e ne completa lo schema (https://nette.org). L'espressione in Pattern e PatternInsensitive deve valere per l'intero valore, cioè come se fosse racchiusa tra i caratteri ^ e $.

Numero di elementi

Ai controlli addMultiUpload(), addCheckboxList(), addMultiSelect() potete applicare anche le regole seguenti, per limitare il numero di elementi selezionati o di file caricati:

MinLength numero minimo int
MaxLength numero massimo int
Length numero in un intervallo o numero esatto coppia [int, int] oppure int

Upload di file

Ai controlli addUpload(), addMultiUpload() si possono applicare anche le regole seguenti:

MaxFileSize dimensione massima del file in byte int
MimeType tipo MIME, sono ammessi i caratteri jolly ('video/*') string|string[]
Image immagine JPEG, PNG, GIF, WebP, AVIF
Pattern il nome del file corrisponde a un'espressione regolare string
PatternInsensitive come Pattern, ma senza distinguere maiuscole e minuscole string

MimeType e Image richiedono l'estensione PHP fileinfo. Se un file o un'immagine siano del tipo richiesto viene rilevato in base alla loro firma, e l'integrità dell'intero file non viene controllata. Potete stabilire se un'immagine è danneggiata, per esempio, provando a caricarla.

Messaggi di errore

Tutte le regole predefinite tranne Pattern e PatternInsensitive hanno un messaggio di errore predefinito, quindi si può omettere. Indicando e formulando tutti i messaggi personalizzati secondo le vostre esigenze, però, renderete il form più amichevole.

Potete cambiare i messaggi predefiniti nella configurazione, modificando i testi nell'array Nette\Forms\Validator::$messages, oppure usando un traduttore.

Nel testo dei messaggi di errore si possono usare i segnaposto seguenti:

%d sostituito in sequenza dagli argomenti della regola
%n$d sostituito dall'n-esimo argomento della regola
%label sostituito dall'etichetta del controllo (senza i due punti)
%name sostituito dal nome del controllo (per esempio name)
%value sostituito dal valore inserito dall'utente
$form->addText('name', 'Nome:')
	->setRequired('Compilate %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'almeno %d e al massimo %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'al massimo %2$d e almeno %1$d', [5, 10]);

Condizioni

Oltre alle regole si possono aggiungere anche delle condizioni. Si scrivono come le regole, ma al posto di addRule() usiamo il metodo addCondition() e naturalmente non indichiamo un messaggio di errore (la condizione si limita a chiedere):

$form->addPassword('password', 'Password:')
	// se la lunghezza della password non supera 8
	->addCondition($form::MaxLength, 8)
		// allora deve contenere una cifra
		->addRule($form::Pattern, 'Deve contenere una cifra', '.*[0-9].*');

La condizione si può legare a un controllo diverso da quello corrente con addConditionOn(). Il primo parametro è un riferimento al controllo. In questo esempio l'e-mail sarà obbligatoria solo se la checkbox è selezionata (cioè se il suo valore è true):

$form->addCheckbox('newsletters', 'Inviatemi le newsletter');

$form->addEmail('email', 'Email:')
	// se la checkbox è selezionata
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// allora richiedi l'e-mail
		->setRequired('Inserite il vostro indirizzo e-mail');

Le condizioni si possono comporre in strutture complesse con elseCondition() ed endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // se la prima condizione è soddisfatta
		->addConditionOn(/* ... */) // ed è soddisfatta anche la seconda condizione su un altro controllo
			->addRule(/* ... */) // richiedi questa regola
		->elseCondition() // se la seconda condizione non è soddisfatta
			->addRule(/* ... */) // richiedi queste regole
			->addRule(/* ... */)
		->endCondition() // torniamo alla prima condizione
		->addRule(/* ... */);

Il primo argomento di addCondition() può essere anche un valore booleano. È utile quando la decisione è già nota mentre il form viene costruito, per esempio per applicare una regola solo in certe circostanze:

$form->addText('nickname')
	->addCondition($isRequired) // un valore noto al momento della costruzione del form
		->setRequired();

In Nette è molto semplice reagire dal lato JavaScript al fatto che una condizione sia soddisfatta o meno, con il metodo toggle(), vedi JavaScript dinamico.

Riferimento a un altro controllo

Come argomento di una regola o di una condizione potete passare anche un altro controllo del form. La regola userà allora il valore che l'utente inserirà in seguito nel browser. Lo si può usare, per esempio, per validare dinamicamente che il controllo password contenga la stessa stringa del controllo password_confirm:

$form->addPassword('password', 'Password');
$form->addPassword('password_confirm', 'Conferma la password')
    ->addRule($form::Equal, 'Le password non coincidono', $form['password']);

Regole e condizioni personalizzate

A volte incontriamo situazioni in cui le regole di validazione integrate in Nette non bastano e dobbiamo validare i dati dell'utente a modo nostro. In Nette è semplicissimo!

Come primo parametro dei metodi addRule() o addCondition() potete passare qualsiasi callback. La callback accetta come primo parametro il controllo stesso e restituisce un valore booleano che indica se la validazione è riuscita. Aggiungendo una regola con addRule() si possono indicare argomenti aggiuntivi, che vengono poi passati come secondo parametro.

Un insieme personalizzato di validatori si può quindi creare come classe con metodi statici:

class MyValidators
{
	// verifica se il valore è divisibile per l'argomento
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// altri validatori
	}
}

L'uso è poi molto immediato:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'Il valore deve essere un multiplo di %d',
		8,
	);

Le regole di validazione personalizzate si possono aggiungere anche a JavaScript. La condizione è che la regola sia un metodo statico. Il suo nome per il validatore JavaScript si forma concatenando il nome della classe senza le barre rovesciate \, un trattino basso _ e il nome del metodo. Per esempio App\MyValidators::validateDivisibility si scrive come AppMyValidators_validateDivisibility e si aggiunge all'oggetto Nette.validators:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Evento onValidate

Dopo l'invio del form avviene la validazione, che controlla le singole regole aggiunte con addRule(), e viene poi scatenato l'evento onValidate. Il suo gestore si può usare per una validazione aggiuntiva, di norma per verificare la corretta combinazione di valori in più controlli del form.

Se viene rilevato un errore, lo si passa al form con il metodo addError(). Lo si può chiamare su un controllo specifico oppure direttamente sul form.

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

private function validateSignInForm(Form $form, \stdClass $data): void
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('Questa combinazione non è possibile.');
	}
}

Elaborare gli errori

In molti casi scopriamo un errore solo elaborando un form valido, per esempio scrivendo un nuovo record nel database e incontrando una chiave duplicata. In tal caso passiamo di nuovo l'errore al form con il metodo addError(). Lo si può chiamare su un controllo specifico oppure direttamente sul form:

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

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) {
		$form->addError('Password non valida.');
	}
}

Se possibile, consigliamo di aggiungere l'errore direttamente al controllo del form, perché con il renderer predefinito verrà mostrato accanto a esso.

$form['date']->addError('Spiacenti, questa data è già occupata.');

Potete chiamare addError() più volte per passare più messaggi di errore a un form o a un controllo. Potete ottenerli con getErrors().

Attenzione: $form->getErrors() restituisce il riepilogo di tutti i messaggi di errore, compresi quelli passati direttamente ai singoli controlli, non solo quelli passati direttamente al form. I messaggi di errore passati solo al form si ottengono con $form->getOwnErrors().

Modificare i valori inseriti

Con il metodo addFilter() possiamo modificare il valore inserito dall'utente. In questo esempio tolleriamo e rimuoviamo gli spazi nel codice postale:

$form->addText('zip', 'Codice postale:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // rimuove gli spazi dal codice postale
	})
	->addRule($form::Pattern, 'Il codice postale non è di cinque cifre', '\d{5}');

Il filtro si integra tra le regole di validazione e le condizioni, quindi l'ordine dei metodi conta: il filtro e la regola vengono chiamati nello stesso ordine in cui sono elencati i metodi addFilter() e addRule().

Validazione JavaScript

Il linguaggio per formulare condizioni e regole è molto potente. Tutti i costrutti funzionano sia sul lato server sia sul lato client, in JavaScript. Vengono trasferiti negli attributi HTML data-nette-rules come JSON. Della validazione vera e propria si occupa uno script che intercetta l'evento submit del form, scorre i singoli controlli ed esegue la validazione corrispondente.

Questo script è netteForms.js ed è disponibile da diverse fonti possibili:

Potete inserire lo script direttamente nella pagina HTML da una CDN:

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

Oppure copiarlo localmente nella cartella pubblica del vostro progetto (per esempio da vendor/nette/forms/src/assets/netteForms.min.js):

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

Oppure installarlo con npm:

npm install nette-forms

E poi caricarlo ed eseguirlo:

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

In alternativa potete caricarlo direttamente dalla cartella vendor:

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

Potete disattivare del tutto la validazione lato client aggiungendo al form l'attributo novalidate. Lo script netteForms.js salta allora la validazione all'invio, quindi la validazione avviene solo sul server:

$form->setHtmlAttribute('novalidate');

JavaScript dinamico

Volete mostrare i campi dell'indirizzo solo se l'utente sceglie di farsi spedire la merce per posta? Nessun problema. La chiave è la coppia di metodi addCondition() e toggle():

$form->addCheckbox('send_it')
	->addCondition($form::Equal, true)
		->toggle('#address-container');

Questo codice dice che, quando la condizione è soddisfatta (cioè quando la checkbox è selezionata), l'elemento HTML #address-container sarà visibile, e viceversa. Collochiamo quindi i controlli con l'indirizzo del destinatario in un container con questo ID, e si nasconderanno o si mostreranno quando la checkbox viene cliccata. Se ne occupa lo script netteForms.js.

Come argomento del metodo toggle() si può passare qualsiasi selettore. Per motivi storici, una stringa che inizia con una lettera, una cifra o un trattino basso e contiene solo lettere, cifre, trattini bassi, trattini, punti e due punti viene trattata come un ID di elemento, come se fosse preceduta dal carattere #. Il secondo parametro facoltativo permette di invertire il comportamento; se per esempio usassimo toggle('#address-container', false), l'elemento verrebbe mostrato solo se la checkbox non fosse selezionata.

L'implementazione JavaScript predefinita cambia la proprietà hidden degli elementi. Possiamo però cambiare facilmente il comportamento, per esempio aggiungendo un'animazione. Basta sovrascrivere in JavaScript il metodo Nette.toggle con una soluzione personalizzata:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// nasconde o mostra 'el' in base al valore di 'visible'
	});
};

Disattivare la validazione

A volte può essere utile disattivare la validazione. Se premere un pulsante di invio non deve eseguire la validazione (adatto ai pulsanti AnnullaAnteprima), la disattiviamo con il metodo $submit->setValidationScope([]). Se deve eseguire solo una validazione parziale, possiamo indicare quali campi o container del form vadano validati.

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

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

$form->addSubmit('send1'); // valida l'intero form
$form->addSubmit('send2')
	->setValidationScope([]); // non valida nulla
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // valida solo il controllo 'name'
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // valida solo il controllo 'age'
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // valida il container 'details'

setValidationScope non influisce sull'evento onValidate del form, che verrà sempre chiamato. L'evento onValidate di un container verrà scatenato solo se quel container è contrassegnato per la validazione parziale.

La validazione parziale influisce anche sui valori restituiti da getValues(): il risultato contiene solo i valori dei controlli che rientrano nell'ambito della validazione. I valori dei controlli fuori da questo ambito vengono omessi.

versione: 4.x