Form autonomi

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 mostrato in questo capitolo.

Se però usate Nette Application e i presenter, c'è una guida dedicata a voi: form nei presenter.

Il primo form

Prima di iniziare, installate il pacchetto con Composer:

composer require nette/forms

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

use Nette\Forms\Form;

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

E disegniamolo in modo semplicissimo:

$form->render();

Il risultato nel browser dovrebbe avere questo aspetto:

Il form è un oggetto della classe Nette\Forms\Form (nei presenter si usa la classe Nette\Application\UI\Form). Vi abbiamo aggiunto i controlli chiamati ‘name’, ‘password’ e un pulsante di invio.

Diamo ora vita al form. Interrogando $form->isSuccess() scopriamo se il form è stato inviato e se è stato compilato in modo valido. In tal caso stamperemo i dati. Dopo la definizione del form aggiungete:

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

Il metodo getValues() restituisce i dati inviati come oggetto ArrayHash. Mostreremo più avanti come cambiarlo. L'oggetto $data contiene le chiavi name e password con i dati inseriti dall'utente.

Di solito inviamo i dati direttamente a un'ulteriore elaborazione, per esempio per inserirli in un database. Durante l'elaborazione può però verificarsi un errore, per esempio se il nome utente è già occupato. In tal caso passiamo l'errore al form con addError() e lo facciamo disegnare di nuovo, insieme al messaggio di errore.

$form->addError('Spiacenti, questo nome utente è già occupato.');

Dopo aver elaborato il form reindirizziamo alla pagina successiva. Questo impedisce che il form venga reinviato involontariamente cliccando i pulsanti aggiornaindietro, oppure navigando nella cronologia del browser.

Per impostazione predefinita il form viene inviato con il metodo POST alla stessa pagina. Entrambe le cose si possono cambiare:

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

E in sostanza è tutto :-) Abbiamo un form funzionante e perfettamente protetto.

Provate ad aggiungere anche altri controlli.

Accesso ai controlli

Il form e i suoi singoli controlli si chiamano componenti. Formano un albero di componenti, la cui radice è il form. Ai singoli controlli del form potete accedere così:

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

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

I controlli si rimuovono con unset:

unset($form['name']);

Regole di validazione

Abbiamo usato la parola valido, ma il form non ha ancora alcuna regola di validazione. Rimediamo.

Il nome sarà obbligatorio, quindi lo contrassegniamo con il metodo setRequired(). Il suo argomento è il testo del messaggio di errore mostrato se l'utente non compila il nome. Se non viene indicato alcun argomento, viene usato il messaggio di errore predefinito.

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

Provate a inviare il form senza compilare il nome e vedrete comparire un messaggio di errore. Il browser o il server lo rifiuteranno finché non compilate il campo.

Allo stesso tempo non potete imbrogliare il sistema scrivendo nel campo solo degli spazi. Niente da fare. Nette elimina automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste sempre fare con ogni campo a riga singola, ma che spesso si dimentica. Nette la fa automaticamente. (Potete provare a ingannare il form inviando come nome una stringa su più righe. Anche qui Nette non si farà ingannare e gli a capo verranno convertiti in spazi.)

Il form viene sempre validato sul lato server, ma viene generata anche la validazione JavaScript. Questa gira all'istante e l'utente viene a conoscenza degli errori subito, senza dover inviare il form al server. Se ne occupa lo 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, noterete forse che Nette inserisce i controlli obbligatori in elementi con la classe CSS required. Provate ad aggiungere al template il foglio di stile seguente e l'etichetta “Nome” diventerà rossa. È un modo elegante di evidenziare i controlli obbligatori per gli utenti:

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

Aggiungiamo altre 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 facoltativo della regola di validazione. Cosa significa?

Estendiamo il form con un nuovo campo facoltativo “età”, che deve essere un numero intero (addInteger()) e rientrare in un intervallo consentito ($form::Range). Qui useremo il terzo parametro del metodo addRule() per passare al validatore l'intervallo richiesto come coppia [min, max]:

$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 controllate, perché il controllo è facoltativo.

Questo apre spazio a un piccolo refactoring. I numeri sono duplicati nel messaggio di errore e nel terzo parametro, il che non è ideale. Se creassimo form multilingue e il messaggio contenente i numeri venisse tradotto in più lingue, cambiare i valori diventerebbe difficile. Per questo si possono usare i segnaposto %d, che Nette riempirà con i valori:

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

Torniamo al controllo password, rendiamolo obbligatorio e verifichiamo anche la lunghezza minima della password ($form::MinLength), usando di nuovo un segnaposto nel messaggio:

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

Aggiungiamo al form un altro campo passwordVerify, in cui l'utente inserisce di nuovo la password per verifica. Con le regole di validazione controlliamo che le due password siano uguali ($form::Equal). Come parametro indichiamo un riferimento alla prima password usando le parentesi quadre:

$form->addPassword('passwordVerify', 'Password di nuovo:')
	->setRequired('Inserite di nuovo la password per verifica')
	->addRule($form::Equal, 'Le password non coincidono', $form['password'])
	->setOmitted();

Con setOmitted() abbiamo contrassegnato un controllo il cui valore non ci interessa davvero e che esiste solo a scopo di validazione. Il suo valore non viene passato a $data.

Con questo abbiamo un form pienamente funzionante, con validazione sia in PHP sia in JavaScript. Le capacità di validazione di Nette sono molto più ampie: potete creare condizioni, mostrare e nascondere parti della pagina in base a esse e altro ancora. Imparerete tutto nel capitolo sulla validazione dei form.

Valori predefiniti

Spesso impostiamo valori predefiniti per i controlli del form:

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

Spesso è utile impostare i valori predefiniti di tutti i controlli in una volta, per esempio quando il form serve a modificare dei record. Leggiamo il record dal database e ne impostiamo i valori come predefiniti:

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

Chiamate setDefaults() dopo aver definito i controlli.

Su un form già inviato setDefaults() non ha effetto: non sovrascrive ciò che l'utente ha compilato, quindi è sicuro chiamarlo incondizionatamente nella factory del form. Se dovete forzare i valori anche dopo l'invio, usate invece setValues().

Disegnare il form

Per impostazione predefinita il form viene disegnato come una tabella. I singoli controlli rispettano le linee guida di base sull'accessibilità: tutte le etichette sono generate come elementi <label> e associate ai rispettivi controlli. Cliccando su un'etichetta il cursore si posiziona automaticamente nel campo del form.

A ogni controllo possiamo impostare attributi HTML qualsiasi. Aggiungiamo per esempio un placeholder:

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

Ci sono molti modi di disegnare un form, quindi al rendering è dedicato un capitolo a parte.

Rendering con Latte

Se avete a portata di mano il motore di template Latte, potete lasciargli disegnare il form e ottenere il pieno controllo sull'HTML risultante. Create il motore, registrate l'estensione dei form e passate il form al template come variabile:

$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);

Nel template lavorate poi con il form tramite la variabile $form e tag come {input}, {label} oppure n:name. Un esempio completo, template compreso, si trova nella directory degli esempi (i file latte.php e latte/). I singoli tag sono descritti nel capitolo sul rendering.

Mappatura sulle classi

Torniamo all'elaborazione dei dati del form. Il metodo getValues() restituiva i dati inviati come oggetto ArrayHash. Poiché si tratta di una classe generica, come stdClass, lavorandoci ci mancano certe comodità, come il completamento automatico delle proprietà negli editor o l'analisi statica del codice. Lo si potrebbe risolvere avendo una classe specifica per ogni form, le cui proprietà rappresentino i singoli controlli. Per 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 dei dati possono essere anche enum, e verranno mappate automaticamente.

Come diciamo a Nette di restituire i dati come oggetti di questa classe? Più facile di quanto pensiate. Basta indicare come parametro il nome della classe o l'oggetto da riempire:

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

Come parametro potete indicare anche 'array' e i dati verranno restituiti come array.

Se i form hanno una struttura a più livelli 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;
}

La mappatura capisce allora, dal tipo della proprietà $person, che deve mappare il container sulla classe PersonFormData. Se la proprietà dovesse contenere un array di container, indicate il tipo array e passate la classe da mappare direttamente al container:

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

Potete farvi generare una proposta della classe dei dati del form con il metodo Nette\Forms\Blueprint::dataClass($form), che la stamperà nella pagina del browser. Poi vi basta selezionare con un clic e copiare il codice nel vostro progetto.

Più pulsanti di invio

Se il form ha più di un pulsante, di norma dobbiamo distinguere quale sia stato premuto. Il metodo isSubmittedBy() del pulsante restituisce questa informazione:

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

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

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

Non omettete il controllo $form->isSuccess(): verifica la validità dei dati.

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

Protezione dalle vulnerabilità

Nette Framework dà grande importanza alla sicurezza e si preoccupa quindi scrupolosamente della corretta protezione dei form.

Oltre a proteggere i form dalle vulnerabilità più note, come il Cross-Site Scripting (XSS) e il Cross-Site Request Forgery (CSRF), adotta molte piccole misure di sicurezza a cui non dovete più pensare.

Per esempio filtra dagli input tutti i caratteri di controllo e controlla la validità della codifica UTF-8, garantendo che i dati del form siano sempre puliti. Per i select box e le liste di radio button verifica che gli elementi selezionati fossero davvero tra quelli offerti e che non ci siano state falsificazioni. Abbiamo già detto che, per i campi di testo a riga singola, sostituisce con spazi i caratteri di fine riga che un aggressore potrebbe inviare. Per i campi su più righe normalizza i caratteri di fine riga. E così via.

Nette si occupa al posto vostro di rischi di sicurezza di cui molti programmatori non sospettano nemmeno l'esistenza.

L'attacco CSRF menzionato consiste nell'attirare la vittima su una pagina che, in silenzio, esegue nel browser della vittima una richiesta al server su cui la vittima è attualmente connessa. Il server crede che la richiesta sia stata fatta volontariamente dalla vittima. Nette rifiuta quindi i form POST inviati da un'origine estranea; anche un sottodominio diverso dello stesso sito conta come estraneo. Se dovete permettere l'invio da un'altra origine, disattivate la protezione con:

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

Questo però disattiva la protezione per qualsiasi origine. Per permetterne solo alcune, disattivate la protezione e verificate voi stessi l'header Origin rispetto a un vostro elenco di origini consentite.

La protezione si basa sull'header Sec-Fetch-Site del browser (Fetch Metadata), che il browser invia automaticamente e che non si può falsificare nemmeno con una vulnerabilità XSS. I browser più vecchi, che non inviano questi header, non supereranno il controllo. L'articolo The browser finally solves CSRF lo descrive in dettaglio.

La protezione precedente, basata su un token di autorizzazione salvato nella sessione e attivata con $form->addProtection(), non serve più ed è deprecata dalla versione 3.3.

Abbiamo avuto così una rapida introduzione ai form in Nette. Provate a guardare nella directory degli esempi della distribuzione per altre idee.

versione: 4.x