Form Usati Autonomamente

Nette Forms semplifica enormemente la creazione e l'elaborazione dei form web. Potete usarli nelle vostre applicazioni in modo completamente autonomo, senza il resto del framework, come mostreremo in questo capitolo.

Tuttavia, se utilizzate Nette Application e i presenter, la guida per l'utilizzo nei presenter è quella che fa per voi.

Primo Form

Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà il seguente (codice completo):

use Nette\Forms\Form;

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

Possiamo renderizzarlo molto facilmente:

$form->render();

e nel browser apparirà così:

Il form è un oggetto della classe Nette\Forms\Form (la classe Nette\Application\UI\Form viene utilizzata nei presenter). Abbiamo aggiunto ad esso gli elementi chiamati nome, password e il pulsante di invio.

Ora diamo vita al form. Interrogando $form->isSuccess() scopriremo se il form è stato inviato e se è stato compilato validamente. Se sì, stamperemo i dati. Quindi, dopo la definizione del form, aggiungiamo:

if ($form->isSuccess()) {
	echo 'Il form è stato compilato correttamente e inviato';
	$data = $form->getValues();
	// $data->name contiene il nome
	// $data->password contiene la password
	var_dump($data);
}

Il metodo getValues() restituisce i dati inviati sotto forma di oggetto ArrayHash. Vedremo più tardi come cambiare questo comportamento. L'oggetto $data contiene le chiavi name e password con i dati inseriti dall'utente.

Di solito, inviamo i dati direttamente per un'ulteriore elaborazione, che potrebbe essere, ad esempio, l'inserimento nel database. Tuttavia, durante l'elaborazione può verificarsi un errore, ad esempio il nome utente è già occupato. In tal caso, restituiamo l'errore al form utilizzando addError() e lo facciamo renderizzare di nuovo, insieme al messaggio di errore.

$form->addError('Siamo spiacenti, questo nome utente è già in uso.');

Dopo aver elaborato il form, reindirizziamo a un'altra pagina. Ciò impedisce l'invio involontario ripetuto del form tramite il pulsante aggiorna, indietro o spostandosi nella cronologia del browser.

Il form viene inviato per default con il metodo POST alla stessa pagina. Entrambi possono essere modificati:

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

E questo è tutto :-) Abbiamo un form funzionante e perfettamente sicuro.

Provate ad aggiungere anche altri elementi del form.

Accesso agli Elementi

Chiamiamo componenti sia il form che i suoi singoli elementi. Formano un albero di componenti, dove la radice è proprio il form. Possiamo accedere ai singoli elementi del form in questo modo:

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

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

Gli elementi vengono rimossi usando unset:

unset($form['name']);

Regole di Validazione

Abbiamo menzionato la parola valido, ma il form non ha ancora regole di validazione. Rimediamo.

Il nome sarà obbligatorio, quindi lo marchiamo con il metodo setRequired(), il cui argomento è il testo del messaggio di errore che verrà visualizzato se l'utente non compila il nome. Se non specifichiamo l'argomento, verrà utilizzato il messaggio di errore predefinito.

$form->addText('name', 'Nome:')
	->setRequired('Inserisci un nome');

Provate a inviare il form senza compilare il nome e vedrete che verrà visualizzato un messaggio di errore e il browser o il server lo rifiuteranno finché non compilerete il campo.

Allo stesso tempo, non ingannerete il sistema scrivendo, ad esempio, solo spazi nel campo. Niente affatto. Nette rimuove automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste sempre fare con ogni input a riga singola, ma spesso viene dimenticata. Nette lo fa automaticamente. (Potete provare a ingannare il form e inviare una stringa multilinea come nome. Anche qui, Nette non si lascerà ingannare e convertirà gli a capo in spazi.)

Il form viene sempre validato lato server, ma viene generata anche una validazione JavaScript, che avviene istantaneamente e l'utente viene informato dell'errore immediatamente, senza dover inviare il form al server. Questo è gestito dallo script netteForms.js. Inseritelo nella pagina:

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

Se guardate il codice sorgente della pagina con il form, potete notare che Nette inserisce gli elementi obbligatori in elementi con la classe CSS required. Provate ad aggiungere il seguente foglio di stile al template e l'etichetta “Nome” diventerà rossa. In questo modo, indichiamo elegantemente agli utenti gli elementi obbligatori:

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

Aggiungiamo ulteriori regole di validazione con il metodo addRule(). Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore e può seguire un argomento della regola di validazione. Cosa significa?

Estendiamo il form con un nuovo campo opzionale “età”, che deve essere un numero intero (addInteger()) e inoltre in un intervallo consentito ($form::Range). E qui utilizzeremo proprio il terzo parametro del metodo addRule(), con cui passiamo al validatore l'intervallo richiesto come coppia [da, a]:

$form->addInteger('age', 'Età:')
	->addRule($form::Range, 'L\'età deve essere compresa tra 18 e 120', [18, 120]);

Se l'utente non compila il campo, le regole di validazione non verranno verificate, poiché l'elemento è opzionale.

Qui c'è spazio per un piccolo refactoring. Nel messaggio di errore e nel terzo parametro, i numeri sono indicati in modo duplicato, il che non è ideale. Se stessimo creando form multilingue e il messaggio contenente numeri fosse tradotto in più lingue, un'eventuale modifica dei valori sarebbe più difficile. Per questo motivo, è possibile utilizzare i segnaposto %d e Nette completerà i valori:

	->addRule($form::Range, 'L\'età deve essere compresa tra %d e %d anni', [18, 120]);

Torniamo all'elemento password, che renderemo anch'esso obbligatorio e verificheremo anche la lunghezza minima della password ($form::MinLength), sempre utilizzando il segnaposto:

$form->addPassword('password', 'Password:')
	->setRequired('Scegli una password')
	->addRule($form::MinLength, 'La password deve contenere almeno %d caratteri', 8);

Aggiungiamo al form anche il campo passwordVerify, dove l'utente inserirà nuovamente la password, per verifica. Utilizzando le regole di validazione, verificheremo se entrambe le password sono uguali ($form::Equal). E come parametro, daremo un riferimento alla prima password usando le parentesi quadre:

$form->addPassword('passwordVerify', 'Password per verifica:')
	->setRequired('Inserisci nuovamente la password per verifica')
	->addRule($form::Equal, 'Le password non corrispondono', $form['password'])
	->setOmitted();

Con setOmitted() abbiamo contrassegnato l'elemento il cui valore in realtà non ci interessa e che esiste solo ai fini della validazione. Il valore non verrà passato a $data.

Con questo, abbiamo un form completamente funzionante con validazione in PHP e JavaScript. Le capacità di validazione di Nette sono molto più ampie, è possibile creare condizioni, far visualizzare e nascondere parti della pagina in base ad esse, ecc. Imparerete tutto nel capitolo sulla validazione dei form.

Valori Predefiniti

Di solito impostiamo valori predefiniti per gli elementi del form:

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

Spesso è utile impostare valori predefiniti per tutti gli elementi contemporaneamente. Ad esempio, quando il form serve per modificare record. Leggiamo il record dal database e impostiamo i valori predefiniti:

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

Chiamate setDefaults() dopo aver definito gli elementi.

Rendering del Form

Per default, il form viene renderizzato come una tabella. I singoli elementi soddisfano la regola di base dell'accessibilità: tutte le etichette sono scritte come <label> e collegate all'elemento del form corrispondente. Facendo clic sull'etichetta, il cursore appare automaticamente nel campo del form.

Possiamo impostare attributi HTML arbitrari per ogni elemento. Ad esempio, aggiungere un placeholder:

$form->addInteger('age', 'Età:')
	->setHtmlAttribute('placeholder', 'Inserisci l\'età');

Ci sono davvero molti modi per renderizzare un form, quindi c'è un capitolo separato sul rendering dedicato a questo.

Mapping su Classi

Torniamo all'elaborazione dei dati del form. Il metodo getValues() ci restituiva i dati inviati come oggetto ArrayHash. Poiché si tratta di una classe generica, qualcosa come stdClass, ci mancherà una certa comodità nel lavorarci, come il suggerimento delle proprietà negli editor o l'analisi statica del codice. Questo potrebbe essere risolto avendo una classe specifica per ogni form, le cui proprietà rappresentano i singoli elementi. Ad esempio:

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

In alternativa, potete usare il costruttore:

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

Le proprietà della classe dati possono anche essere enum e verranno mappate automaticamente.

Come dire a Nette di restituirci i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta specificare il nome della classe o l'oggetto da idratare come parametro:

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

Come parametro si può specificare anche 'array' e allora i dati verranno restituiti come array.

Se i form formano una struttura multilivello composta da container, create una classe separata per ognuno:

$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;
}

Il mapping riconoscerà quindi dal tipo della proprietà $person che deve mappare il container sulla classe PersonFormData. Se la proprietà contenesse un array di container, specificate il tipo array e passate la classe per il mapping direttamente al container:

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

Potete far generare il design della classe dati del form usando il metodo Nette\Forms\Blueprint::dataClass($form), che lo stamperà sulla pagina del browser. Quindi basta selezionare il codice con un clic e copiarlo nel progetto.

Più Pulsanti

Se il form ha più di un pulsante, di solito dobbiamo distinguere quale è stato premuto. Questa informazione ci viene restituita dal metodo isSubmittedBy() del pulsante:

$form->addSubmit('save', 'Salva');
$form->addSubmit('delete', 'Elimina');

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

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

Non omettete la domanda $form->isSuccess(), verificherete così la validità dei dati.

Quando il form viene inviato premendo il tasto Invio, viene considerato come se fosse stato inviato dal primo pulsante.

Protezione dalle Vulnerabilità

Nette Framework pone grande enfasi sulla sicurezza e quindi si preoccupa meticolosamente della buona protezione dei form.

Oltre a proteggere i form dagli attacchi Cross Site Scripting (XSS) e Cross-Site Request Forgery (CSRF), implementa molte piccole misure di sicurezza a cui non dovete più pensare.

Ad esempio, filtra tutti i caratteri di controllo dagli input e verifica la validità della codifica UTF-8, quindi i dati del form saranno sempre puliti. Per le select box e le radio list, verifica che gli elementi selezionati fossero effettivamente tra quelli offerti e che non ci sia stata alcuna falsificazione. Abbiamo già menzionato che per gli input di testo a riga singola, rimuove i caratteri di fine riga che un attaccante potrebbe aver inviato. Per gli input multilinea, invece, normalizza i caratteri di fine riga. E così via.

Nette risolve per voi i rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza.

L'attacco CSRF menzionato consiste nel fatto che un attaccante attira la vittima su una pagina che esegue discretamente una richiesta nel browser della vittima al server su cui la vittima è loggata, e il server crede che la richiesta sia stata eseguita dalla vittima di sua spontanea volontà. Pertanto, Nette impedisce l'invio di form POST da un altro dominio. Se per qualche motivo volete disabilitare la protezione e consentire l'invio del form da un altro dominio, usate:

$form->allowCrossOrigin(); // ATTENZIONE! Disabilita la protezione!

Questa protezione utilizza un cookie SameSite chiamato _nss. Pertanto, create l'oggetto form prima di inviare il primo output, in modo che il cookie possa essere inviato.

La protezione tramite cookie SameSite potrebbe non essere affidabile al 100%, quindi è consigliabile abilitare anche la protezione tramite token:

$form->addProtection();

Si consiglia di proteggere in questo modo i form nella parte amministrativa del sito che modificano dati sensibili nell'applicazione. Il framework si difende dall'attacco CSRF generando e verificando un token di autorizzazione, che viene memorizzato nella sessione. Pertanto, è necessario avere una sessione aperta prima di visualizzare il form. Nella parte amministrativa del sito, di solito la sessione è già avviata a causa del login dell'utente. Altrimenti, avviate la sessione con il metodo Nette\Http\Session::start().

Bene, abbiamo completato una rapida introduzione ai form in Nette. Provate a dare un'occhiata alla directory examples nella distribuzione, dove troverete ulteriore ispirazione.

versione: 4.0