Form nei presenter
Nette Forms semplifica notevolmente la creazione e l'elaborazione dei form web. In questo capitolo imparerete a usare i form dentro i presenter.
Se vi interessa usarli in modo completamente autonomo, senza il resto del framework, c'è una guida sull'uso autonomo.
Il primo form
Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà questo:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Nome:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Registrati');
$form->onSuccess[] = $this->formSucceeded(...);
e nel browser verrà mostrato così:

Un form in un presenter è un oggetto della classe Nette\Application\UI\Form; il suo predecessore
Nette\Forms\Form è destinato all'uso autonomo. Vi abbiamo aggiunto i controlli name, password e un pulsante di
invio. Infine la riga $form->onSuccess dice che dopo l'invio e la validazione riuscita deve essere chiamato il
metodo $this->formSucceeded().
Dal punto di vista del presenter, il form è un normale componente. Viene quindi trattato come un componente e integrato nel presenter con un metodo factory. Avrà questo aspetto:
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Registrati');
$form->onSuccess[] = $this->formSucceeded(...);
return $form;
}
private function formSucceeded(Form $form, $data): void
{
// qui elaboreremo i dati inviati dal form
// $data->name contiene il nome
// $data->password contiene la password
$this->flashMessage('Vi siete registrati con successo.');
$this->redirect('Home:');
}
}
E nel template il form si disegna con il tag {control}:
<h1>Registrazione</h1>
{control registrationForm}
E in sostanza è tutto :-) Abbiamo un form funzionante e perfettamente protetto.
Ora starete pensando che è andata troppo in fretta e vi chiederete come sia possibile che il metodo
formSucceeded() venga chiamato e quali parametri riceva. Sì, avete ragione, la cosa merita una spiegazione.
Nette introduce un meccanismo rinfrescante, chiamato stile hollywoodiano. Invece che voi, come sviluppatori, dobbiate chiedere di continuo se è successo qualcosa (“il form è stato inviato?”, “è stato inviato in modo valido?” e “non è stato falsificato?”), dite al framework “quando il form è compilato validamente, chiama questo metodo” e gli lasciate il lavoro successivo. Se programmate in JavaScript conoscete benissimo questo stile di programmazione: scrivete funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro gli argomenti appropriati.
È esattamente così che è costruito il codice del presenter qui sopra. L'array $form->onSuccess rappresenta
un elenco di callback PHP che Nette chiama nel momento in cui il form viene inviato e compilato correttamente (cioè è valido).
Nel ciclo di vita del presenter si
tratta di un cosiddetto segnale, quindi vengono chiamate dopo il metodo action* e prima del metodo
render*. E a ogni callback passa come primo parametro il form stesso e come secondo i dati inviati, come oggetto ArrayHash (oppure stdClass, oppure una classe personalizzata).
Potete omettere il primo parametro se non vi serve l'oggetto form. Il secondo parametro può essere più intelligente, ma ne
parliamo più avanti.
L'oggetto $data contiene le proprietà name e password con i dati inseriti dall'utente.
Di solito inviamo i dati direttamente a un'ulteriore elaborazione, che può essere per esempio l'inserimento 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à in uso.');
Oltre a onSuccess esiste anche onSubmit: le callback vengono chiamate ogni volta che il form viene
inviato, anche se non è compilato correttamente. E anche onError: le callback vengono chiamate solo se l'invio non
è valido. Vengono chiamate anche se invalidiamo il form dentro onSuccess con addError().
Dopo aver elaborato il form reindirizziamo a un'altra pagina. Questo impedisce il reinvio indesiderato del form usando il pulsante aggiorna, il pulsante indietro oppure navigando nella cronologia del browser.
Se il form viene inviato via AJAX, di norma invece di reindirizzare ridisegnate uno snippet con il form disegnato di nuovo.
Provate ad aggiungere anche altri controlli.
Accesso ai controlli
Il form è un componente del presenter, nel nostro caso chiamato registrationForm (dal nome del metodo factory
createComponentRegistrationForm), quindi in qualsiasi punto del presenter potete accedere al form così:
$form = $this->getComponent('registrationForm');
// sintassi alternativa: $form = $this['registrationForm'];
Anche i singoli controlli del form sono componenti, quindi potete accedervi allo stesso modo:
$input = $form->getComponent('name'); // oppure $input = $form['name'];
$button = $form->getComponent('send'); // oppure $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 l'argomento viene omesso, viene usato il messaggio di errore
predefinito.
$form->addText('name', 'Nome:')
->setRequired('Inserite il vostro 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 inserendo nel campo, per esempio, 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, che gira all'istante e
permette all'utente di conoscere l'errore subito, senza dover inviare il form al server. Se ne occupa lo script
netteForms.js. Includetelo nel vostro template di layout:
<script src="https://unpkg.com/nette-forms@3"></script>
Se guardate il codice sorgente della pagina con il form, noterete forse che Nette racchiude i controlli obbligatori in
elementi con la classe CSS required. Provate ad aggiungere al template il foglio di stile seguente e l'etichetta
“Nome” sarà rossa. È un modo elegante di evidenziare i campi 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 della regola di validazione. Cosa significa?
Estendiamo il form con un nuovo campo facoltativo “età”, che deve essere un numero intero (addInteger()) e
rientrare anche 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é l'elemento è facoltativo.
Questo apre spazio a un piccolo refactoring. Nel messaggio di errore e nel terzo parametro i numeri sono duplicati, 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 sostituirà 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 vostra 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 conferma.
Con le regole di validazione controlliamo che le due password siano uguali ($form::Equal). Come argomento indichiamo
un riferimento alla prima password usando le parentesi quadre:
$form->addPassword('passwordVerify', 'Password di nuovo:')
->setRequired('Inserite di nuovo la password per controllare eventuali errori di battitura')
->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: si possono creare condizioni, mostrare o nascondere parti della pagina in base a esse e altro ancora. Imparerete tutto nel capitolo sulla validazione dei form.
Valori predefiniti
Impostiamo comunemente 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 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 regole di base
dell'accessibilità web: tutte le etichette sono scritte come elementi <label> e associate ai rispettivi
controlli. Cliccando sull'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à');
I modi di disegnare un form sono davvero tanti, quindi al rendering è dedicato un capitolo a parte.
Mappatura sulle classi
Torniamo al metodo formSucceeded(), che riceve nel secondo parametro $data i dati inviati, come
oggetto ArrayHash (o stdClass). Poiché si tratta di una classe generica, simile a
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 un 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 la
classe come tipo del parametro $data nel metodo gestore:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $data è un'istanza di RegistrationFormData
$name = $data->name;
// ...
}
Come tipo potete indicare anche array, e allora i dati verranno passati come array.
Allo stesso modo potete usare il metodo getValues(), passando come parametro il nome della classe o un oggetto da
riempire:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Se dovete leggere i valori prima che il form venga validato, di norma dentro un gestore onValidate, usate invece
il metodo getUntrustedValues(). Accetta gli stessi parametri di getValues(), ma restituisce i valori
inviati senza garantire che abbiano superato la validazione.
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 deduce 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 generare una proposta della classe dei dati del form con il metodo
Nette\Forms\Blueprint::dataClass($form), che la stampa 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. Possiamo creare una funzione gestore
separata per ogni pulsante. La impostiamo come gestore dell'evento
onClick:
$form->addSubmit('save', 'Salva')
->onClick[] = $this->saveButtonPressed(...);
$form->addSubmit('delete', 'Elimina')
->onClick[] = $this->deleteButtonPressed(...);
Un gestore si può passare al pulsante anche direttamente, come terzo argomento del metodo
addSubmit().
Questi gestori vengono chiamati solo se il form è compilato validamente (a meno che per il pulsante la validazione non sia
disattivata), come per l'evento onSuccess. La differenza è che come primo parametro può essere passato l'oggetto
del pulsante di invio invece del form, a seconda del tipo che dichiarate:
private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Quando il form viene inviato premendo il tasto Invio, viene trattato come se fosse stato inviato dal primo pulsante di invio.
Evento onAnchor
Quando costruite un form in un metodo factory (come createComponentRegistrationForm), esso non sa ancora se sia
stato inviato né con quali dati. Ci sono però casi in cui abbiamo bisogno di conoscere i valori inviati: magari l'aspetto del
form dipende da essi, oppure servono per select box dipendenti e così via.
Potete quindi far chiamare il codice che costruisce il form solo quando esso è “ancorato”, cioè è già collegato al
presenter e conosce i propri dati inviati. Collocate questo codice nell'array $onAnchor:
$country = $form->addSelect('country', 'Paese:', $this->model->getCountries());
$city = $form->addSelect('city', 'Città:');
$form->onAnchor[] = function () use ($country, $city) {
// questa funzione verrà chiamata quando il form conoscerà i dati con cui è stato inviato
// così potete usare il metodo getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Protezione dalle vulnerabilità
Nette Framework dà grande importanza alla sicurezza e si preoccupa quindi scrupolosamente della sicurezza dei form. Lo fa in modo completamente trasparente e senza richiedere alcuna configurazione manuale.
Oltre a proteggere i form da attacchi 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 è connessa. Il server crede allora 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. Per i browser più vecchi, che non li
supportano, vale un cookie SameSite di ripiego, che un'applicazione Nette imposta automaticamente. 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.
Usare uno stesso form in più presenter
Se dovete usare lo stesso form in più presenter, consigliamo di creare una factory, che poi iniettate nei presenter. Un posto
adatto per una classe del genere è, per esempio, la directory app/Forms.
La classe factory potrebbe avere questo aspetto:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addSubmit('send', 'Accedi');
return $form;
}
}
Chiediamo alla classe di produrre il form nel metodo factory del componente, dentro il presenter:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// possiamo modificare il form, qui per esempio cambiamo l'etichetta del pulsante
$form['send']->setCaption('Continua');
$form->onSuccess[] = $this->signInFormSuceeded(...); // e aggiungiamo il gestore
return $form;
}
Il gestore dell'elaborazione del form può essere fornito anche dalla factory stessa:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addSubmit('send', 'Accedi');
$form->onSuccess[] = function (Form $form, $data): void {
// qui elaboriamo il nostro form inviato
};
return $form;
}
}
Abbiamo così visto una rapida introduzione ai form in Nette. Provate a guardare nella directory degli esempi della distribuzione per altre idee.