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.

versione: 4.0