Form nei presenter
Nette Forms facilita enormemente la creazione e l'elaborazione dei form web. In questo capitolo imparerete come utilizzare i form all'interno dei presenter.
Se siete interessati a come usarli completamente da soli senza il resto del framework, è per voi la guida per l'uso indipendente.
Primo form
Proviamo a scrivere un semplice form di registrazione. Il suo codice sarà il seguente:
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à visualizzato così:

Il form nel presenter è un oggetto della classe Nette\Application\UI\Form
, il suo predecessore
Nette\Forms\Form
è destinato all'uso indipendente. Vi abbiamo aggiunto i cosiddetti elementi nome, password e
pulsante di invio. E infine, la riga con $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 componente comune. Pertanto, viene trattato come un componente e lo integriamo nel presenter tramite un metodo factory. Sarà simile a questo:
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;
}
public function formSucceeded(Form $form, $data): void
{
// qui elaboriamo i dati inviati dal form
// $data->name contiene il nome
// $data->password contiene la password
$this->flashMessage('Sei stato registrato con successo.');
$this->redirect('Home:');
}
}
E nel template renderizziamo il form con il tag {control}
:
<h1>Registrazione</h1>
{control registrationForm}
E questo è praticamente tutto :-) Abbiamo un form funzionante e perfettamente protetto.
E ora probabilmente state pensando che sia stato troppo veloce, vi state chiedendo come sia possibile che venga chiamato il
metodo formSucceeded()
e quali siano i parametri che riceve. Certo, avete ragione, questo merita una
spiegazione.
Nette infatti introduce un meccanismo fresco, che chiamiamo Hollywood style. Invece di dovervi chiedere costantemente come sviluppatori se è successo qualcosa (“il form è stato inviato?”, “è stato inviato validamente?” e “non è stato manomesso?”), dite al framework “quando il form sarà compilato validamente, chiama questo metodo” e lasciate il resto del lavoro a lui. Se programmate in JavaScript, questo stile di programmazione vi è familiare. Scrivete funzioni che vengono chiamate quando si verifica un certo evento. E il linguaggio passa loro gli argomenti appropriati.
È proprio così che è costruito anche il codice del presenter sopra riportato. 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). Nell'ambito del ciclo di vita
del presenter si tratta del cosiddetto segnale, vengono quindi chiamati dopo il metodo action*
e prima del metodo
render*
. E ad ogni callback passa come primo parametro il form stesso e come secondo i dati inviati sotto forma di
oggetto ArrayHash. Il primo parametro può essere omesso se non
si necessita dell'oggetto form. E il secondo parametro può essere più intelligente, ma di questo parleremo più avanti.
L'oggetto $data
contiene le chiavi name
e password
con i dati compilati dall'utente. Di
solito inviamo i dati direttamente per un'ulteriore elaborazione, che può essere ad esempio l'inserimento nel database. Durante
l'elaborazione, però, può verificarsi un errore, ad esempio il nome utente è già occupato. In tal caso, restituiamo l'errore
al form tramite addError()
e lo facciamo renderizzare di nuovo, anche con il messaggio di errore.
$form->addError('Ci dispiace, il nome utente è già in uso.');
Oltre a onSuccess
esiste anche onSubmit
: i callback vengono chiamati sempre dopo l'invio del form,
anche se non è compilato correttamente. E inoltre onError
: i callback vengono chiamati solo se l'invio non è
valido. Vengono chiamati anche se in onSuccess
o onSubmit
invalidiamo il form tramite
addError()
.
Dopo l'elaborazione del form, reindirizziamo alla pagina successiva. Ciò impedisce l'invio involontario ripetuto del form tramite il pulsante aggiorna, indietro o muovendosi nella cronologia del browser.
Provate ad aggiungere anche altri elementi del form.
Accesso agli elementi
Il form è un componente del presenter, nel nostro caso chiamato registrationForm
(dal nome del metodo factory
createComponentRegistrationForm
), quindi ovunque nel presenter potete accedere al form tramite:
$form = $this->getComponent('registrationForm');
// sintassi alternativa: $form = $this['registrationForm'];
Anche i singoli elementi del form sono componenti, quindi potete accedervi allo stesso modo:
$input = $form->getComponent('name'); // o $input = $form['name'];
$button = $form->getComponent('send'); // o $button = $form['send'];
Gli elementi vengono rimossi tramite unset:
unset($form['name']);
Regole di validazione
Abbiamo menzionato la parola valido, ma il form per ora non ha regole di validazione. Rimediamo.
Il nome sarà obbligatorio, quindi lo contrassegniamo 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 il nome per favore');
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 potete ingannare il sistema scrivendo nel campo, ad esempio, solo spazi. Niente da fare. Nette rimuove automaticamente gli spazi iniziali e finali. Provate. È una cosa che dovreste fare sempre 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. Nemmeno qui Nette si lascia ingannare e trasforma 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 nel template del layout:
<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 al template il seguente foglio di stile e l'etichetta “Nome”
diventerà rossa. In questo modo elegante segnaliamo 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ò ancora seguire un argomento della regola di validazione. Cosa si intende con
questo?
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 useremo 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 si crea 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 diventerebbe 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 inoltre 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 avere almeno %d caratteri', 8);
Aggiungiamo al form anche il campo passwordVerify
, dove l'utente inserirà nuovamente la password, per controllo.
Tramite le regole di validazione verifichiamo se entrambe le password sono uguali ($form::Equal
). E come parametro
diamo un riferimento alla prima password usando le parentesi quadre:
$form->addPassword('passwordVerify', 'Password di controllo:')
->setRequired('Inserisci nuovamente la password per controllo')
->addRule($form::Equal, 'Le password non corrispondono', $form['password'])
->setOmitted();
Tramite setOmitted()
abbiamo contrassegnato l'elemento il cui valore in realtà non ci interessa e che esiste solo
per motivi di validazione. Il valore non viene passato a $data
.
Con questo abbiamo un form completamente funzionante con validazione sia in PHP che in JavaScript. Le capacità di validazione di Nette sono molto più ampie, si possono creare condizioni, far visualizzare e nascondere parti della pagina in base ad esse, ecc. Tutto questo lo imparerete nel capitolo sulla validazione dei form.
Valori predefiniti
Agli elementi del form impostiamo comunemente valori predefiniti:
$form->addEmail('email', 'E-mail')
->setDefaultValue($lastUsedEmail);
Spesso è utile impostare i 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.
Renderizzazione del form
Standardmente il form viene renderizzato come una tabella. I singoli elementi soddisfano la regola base di accessibilità –
tutte le etichette sono scritte come <label>
e collegate al rispettivo elemento del form. Cliccando
sull'etichetta, il cursore appare automaticamente nel campo del form.
A ogni elemento possiamo impostare attributi HTML arbitrari. Ad esempio, aggiungere un placeholder:
$form->addInteger('age', 'Età:')
->setHtmlAttribute('placeholder', 'Inserisci l\'età per favore');
I modi per renderizzare un form sono davvero tanti, quindi c'è un capitolo separato sulla renderizzazione.
Mappatura su classi
Torniamo al metodo formSucceeded()
, che nel secondo parametro $data
riceve i dati inviati come
oggetto ArrayHash
. Poiché si tratta di una classe generica, qualcosa come stdClass
, ci mancherà una
certa comodità nel lavorare con essa, 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, puoi utilizzare 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 semplicemente
indicare la classe come tipo del parametro $data
nel metodo handler:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $data è un'istanza di RegistrationFormData
$name = $data->name;
// ...
}
Come tipo si può indicare anche array
e allora i dati verranno passati come array.
In modo analogo si può usare anche la funzione getValues()
, alla quale passiamo il nome della classe o l'oggetto
da idratare come parametro:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Se i form formano una struttura multilivello composta da container, create una classe separata per ciascuno:
$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 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 la mappatura direttamente al container:
$person->setMappedType(PersonFormData::class);
Puoi farti generare il design della classe dati del form tramite il metodo
Nette\Forms\Blueprint::dataClass($form)
, che la stamperà nella pagina del browser. Basta quindi cliccare per
selezionare il codice e copiarlo nel progetto.
Più pulsanti
Se il form ha più di un pulsante, di solito abbiamo bisogno di distinguere quale di essi è stato premuto. Possiamo creare una
funzione handler separata per ogni pulsante. La impostiamo come handler per l'evento onClick
:
$form->addSubmit('save', 'Salva')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Elimina')
->onClick[] = [$this, 'deleteButtonPressed'];
Questi handler vengono chiamati solo nel caso di un form compilato validamente, proprio come nel caso dell'evento
onSuccess
. La differenza è che come primo parametro, invece del form, può essere passato il pulsante di invio,
dipende dal tipo che specificate:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Quando il form viene inviato con il tasto Invio, viene considerato come se fosse stato inviato con il primo pulsante.
Evento onAnchor
Quando nel metodo factory (come ad esempio createComponentRegistrationForm
) costruiamo il form, questo non sa
ancora se è stato inviato, né con quali dati. Ci sono però casi in cui abbiamo bisogno di conoscere i valori inviati, ad
esempio se da essi dipende l'ulteriore aspetto del form, o se ne abbiamo bisogno per selectbox dipendenti, ecc.
La parte del codice che costruisce il form può quindi essere fatta chiamare solo nel momento in cui è cosiddetto ancorato,
cioè è già collegato al presenter e conosce i suoi dati inviati. Tale codice lo passiamo all'array $onAnchor
:
$country = $form->addSelect('country', 'Stato:', $this->model->getCountries());
$city = $form->addSelect('city', 'Città:');
$form->onAnchor[] = function () use ($country, $city) {
// questa funzione viene chiamata solo quando il form sa se è stato inviato e con quali dati
// si può quindi usare il metodo getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Protezione dalle vulnerabilità
Nette Framework pone grande enfasi sulla sicurezza e quindi si preoccupa meticolosamente della buona protezione dei form. Lo fa in modo completamente trasparente e non richiede alcuna impostazione manuale.
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 dal form saranno sempre puliti. Per i select box e i radio list, verifica che gli elementi selezionati fossero effettivamente tra quelli offerti e che non ci sia stata manomissione. Abbiamo già menzionato che per gli input di testo a riga singola rimuove i caratteri di fine riga che un utente malintenzionato 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 utente malintenzionato attira la vittima su una pagina che esegue discretamente nel browser della vittima una richiesta al server su cui la vittima è loggata, e il server crede che la richiesta sia stata eseguita dalla vittima di sua volontà. Pertanto, Nette impedisce l'invio di form POST da un dominio diverso. Se per qualche motivo volete disattivare la protezione e consentire l'invio del form da un dominio diverso, usate:
$form->allowCrossOrigin(); // ATTENZIONE! Disattiva la protezione!
Questa protezione utilizza un cookie SameSite chiamato _nss
. La protezione tramite cookie SameSite potrebbe non
essere affidabile al 100%, quindi è consigliabile attivare anche la protezione tramite token:
$form->addProtection();
Raccomandiamo 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 salvato
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()
.
Stesso form in più presenter
Se avete bisogno di utilizzare lo stesso form in più presenter, vi consigliamo di creare una factory per esso, che poi
passerete al presenter. Una posizione adatta per una tale classe è ad esempio la directory app/Forms
.
La classe factory può assomigliare a questo:
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 per i componenti nel presenter:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// possiamo modificare il form, qui ad esempio cambiamo l'etichetta sul pulsante
$form['send']->setCaption('Continua');
$form->onSuccess[] = [$this, 'signInFormSucceeded']; // e aggiungiamo l'handler
return $form;
}
L'handler per l'elaborazione del form può anche essere fornito già dalla factory:
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 eseguiamo l'elaborazione del form
};
return $form;
}
}
Bene, abbiamo completato una rapida introduzione ai form in Nette. Provate a dare un'occhiata anche alla directory examples nella distribuzione, dove troverete ulteriore ispirazione.