Moduli utilizzati autonomamente

I Nette Forms semplificano notevolmente la creazione e l'elaborazione dei moduli web. È possibile utilizzarli nelle proprie applicazioni completamente da soli, senza il resto del framework, come dimostreremo in questo capitolo.

Tuttavia, se utilizzate Nette Application e i presenter, c'è una guida per voi: i moduli nei presenter.

Primo modulo

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

use Nette\Forms\Form;

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

E rendiamolo:

$form->render();

e il risultato dovrebbe apparire come questo:

Il modulo è un oggetto della classe Nette\Forms\Form (la classe Nette\Application\UI\Form è utilizzata nei presentatori). Abbiamo aggiunto i controlli nome, password e pulsante di invio.

Ora rilanceremo il modulo. Chiedendo a $form->isSuccess(), scopriremo se il modulo è stato inviato e se è stato compilato in modo valido. In caso affermativo, si effettuerà il dump dei dati. Dopo la definizione del modulo, aggiungeremo:

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

Il metodo getValues() restituisce i dati inviati sotto forma di oggetto ArrayHash. Mostreremo in seguito come modificarlo. La variabile $data contiene le chiavi name e password con i dati inseriti dall'utente.

Di solito i dati vengono inviati direttamente per un'ulteriore elaborazione, che può essere, ad esempio, l'inserimento nel database. Tuttavia, durante l'elaborazione può verificarsi un errore, ad esempio il nome utente è già stato preso. In questo caso, si passa l'errore al modulo utilizzando addError() e lo si fa ridisegnare, con un messaggio di errore:

$form->addError('Sorry, username is already in use.');

Dopo aver elaborato il modulo, si passa alla pagina successiva. In questo modo si evita che il modulo venga involontariamente ripresentato facendo clic sul pulsante refresh, back o spostando la cronologia del browser.

Per impostazione predefinita, il modulo viene inviato con il metodo POST alla stessa pagina. Entrambi possono essere modificati:

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

E questo è tutto :-) Abbiamo un modulo funzionale e perfettamente protetto.

Provate ad aggiungere altri controlli al modulo.

Accesso ai controlli

Il modulo e i suoi controlli individuali sono chiamati componenti. Essi creano un albero di componenti, la cui radice è il modulo. È possibile accedere ai singoli controlli nel modo seguente:

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

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

I controlli vengono rimossi utilizzando unset:

unset($form['name']);

Regole di validazione

Qui è stata usata la parola valido, ma il modulo non ha ancora regole di convalida. Risolviamo il problema.

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

$form->addText('name', 'Name:')
	->setRequired('Please enter a name.');

Se si prova a inviare il modulo senza il nome compilato, si vedrà che viene visualizzato un messaggio di errore e il browser o il server lo rifiuteranno finché non verrà compilato.

Allo stesso tempo, non sarà possibile ingannare il sistema digitando solo spazi nell'input, ad esempio. Non c'è modo. Nette taglia automaticamente gli spazi bianchi a destra e a sinistra. Provate. È un'operazione che si dovrebbe sempre fare per ogni inserimento di una singola riga, ma che spesso viene dimenticata. Nette lo fa automaticamente. (Si può provare a ingannare i moduli e inviare una stringa multilinea come nome. Anche in questo caso, Nette non si lascia ingannare e le interruzioni di riga si trasformano in spazi).

Il modulo viene sempre convalidato sul lato server, ma viene generata anche una convalida JavaScript, che è rapida e l'utente viene a conoscenza dell'errore immediatamente, senza dover inviare il modulo al server. Questo è gestito dallo script netteForms.js. Aggiungerlo alla pagina:

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

Se si guarda nel codice sorgente della pagina con il modulo, si può notare che Nette inserisce i campi obbligatori in elementi con una classe CSS required. Provate ad aggiungere il seguente stile al template e l'etichetta “Nome” diventerà rossa. In modo elegante, contrassegniamo i campi obbligatori per gli utenti:

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

Ulteriori regole di validazione saranno aggiunte con il metodo addRule(). Il primo parametro è la regola, il secondo è di nuovo il testo del messaggio di errore e l'argomento opzionale regola di validazione può seguire. Che cosa significa?

Il modulo riceverà un altro input opzionale età con la condizione che deve essere un numero (addInteger()) e in determinati limiti ($form::Range). E qui useremo il terzo argomento di addRule(), l'intervallo stesso:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);

Se l'utente non compila il campo, le regole di validazione non saranno verificate, perché il campo è opzionale.

Ovviamente c'è spazio per un piccolo refactoring. Nel messaggio di errore e nel terzo parametro, i numeri sono elencati in duplice copia, il che non è ideale. Se stessimo creando un modulo multilingue e il messaggio contenente i numeri dovesse essere tradotto in più lingue, sarebbe più difficile modificare i valori. Per questo motivo, è possibile utilizzare i caratteri sostitutivi %d:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Torniamo al campo password, rendiamolo richiesto e verifichiamo la lunghezza minima della password ($form::MinLength), sempre utilizzando i caratteri sostitutivi del messaggio:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);

Aggiungeremo un campo passwordVerify al modulo, dove l'utente inserisce nuovamente la password, per il controllo. Utilizzando le regole di validazione, verifichiamo se le due password sono uguali ($form::Equal). E come argomento diamo un riferimento alla prima password usando le parentesi quadre:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();

Con setOmitted(), contrassegniamo un elemento il cui valore non ci interessa e che esiste solo per la validazione. Il suo valore non viene passato a $data.

Abbiamo un modulo completamente funzionale con validazione in PHP e JavaScript. Le capacità di validazione di Nette sono molto più ampie: si possono creare condizioni, visualizzare e nascondere parti di una pagina in base a esse, ecc. Potete trovare tutte le informazioni nel capitolo sulla convalida dei moduli.

Valori predefiniti

Spesso si impostano valori predefiniti per i controlli dei moduli:

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

Spesso è utile impostare i valori predefiniti per tutti i controlli contemporaneamente. Ad esempio, quando il modulo viene utilizzato per modificare i record. Leggiamo il record dal database e lo impostiamo come valore predefinito:

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

Chiamare setDefaults() dopo aver definito i controlli.

Rendering del modulo

Per impostazione predefinita, il modulo viene reso come una tabella. I singoli controlli seguono le linee guida di base per l'accessibilità del Web. Tutte le etichette sono generate come elementi <label> e sono associate ai rispettivi input; facendo clic sull'etichetta si sposta il cursore sull'input.

Possiamo impostare qualsiasi attributo HTML per ogni elemento. Ad esempio, aggiungere un segnaposto:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

Ci sono davvero molti modi per rendere un modulo, quindi è un capitolo dedicato alla resa.

Mappatura delle classi

Torniamo all'elaborazione dei dati del modulo. Il metodo getValues() restituisce i dati inviati come oggetto ArrayHash. Poiché si tratta di una classe generica, come stdClass, ci mancheranno alcune comodità per lavorare con essa, come il completamento del codice per le proprietà negli editor o l'analisi statica del codice. Questo potrebbe essere risolto con una classe specifica per ogni modulo, le cui proprietà rappresentano i singoli controlli. Ad esempio:

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

A partire da PHP 8.0, è possibile utilizzare questa elegante notazione che utilizza un costruttore:

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

Come dire a Nette di restituirci i dati come oggetti di questa classe? È più facile di quanto si pensi. Basta specificare come parametro il nome della classe o dell'oggetto da idratare:

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

Anche 'array' può essere specificato come parametro e i dati vengono restituiti come array.

Se i moduli consistono in una struttura a più livelli composta da contenitori, creare una classe separata per ciascuno di essi:

$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 sa quindi dal tipo di proprietà $person che deve mappare il contenitore alla classe PersonFormData. Se la proprietà contiene un array di contenitori, fornire il tipo array e passare la classe da mappare direttamente al contenitore:

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

È possibile generare una proposta per la classe di dati di un modulo utilizzando il metodo Nette\Forms\Blueprint::dataClass($form), che la stamperà nella pagina del browser. È quindi sufficiente fare clic per selezionare e copiare il codice nel progetto.

Pulsanti di invio multipli

Se il modulo ha più di un pulsante, di solito dobbiamo distinguere quale è stato premuto. Il metodo isSubmittedBy() del pulsante ci restituisce questa informazione:

$form->addSubmit('save', 'Save');
$form->addSubmit('delete', 'Delete');

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

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

Non omettere $form->isSuccess() per verificare la validità dei dati.

Quando un modulo viene inviato con il tasto Invio, viene trattato come se fosse stato inviato con il primo pulsante.

Protezione dalle vulnerabilità

Nette Framework si impegna a fondo per essere sicuro e poiché i moduli sono l'input più comune dell'utente, i moduli di Nette sono praticamente impenetrabili.

Oltre a proteggere i moduli da vulnerabilità ben note come Cross-Site Scripting (XSS) e Cross-Site Request Forgery (CSRF), Nette esegue molte piccole operazioni di sicurezza a cui non è più necessario pensare.

Ad esempio, filtra tutti i caratteri di controllo dagli input e controlla la validità della codifica UTF-8, in modo che i dati del modulo siano sempre puliti. Per le caselle di selezione e gli elenchi di scelta, verifica che le voci selezionate siano effettivamente quelle proposte e che non ci siano state contraffazioni. Abbiamo già detto che per gli input di testo a riga singola, rimuove i caratteri di fine riga che un utente malintenzionato potrebbe inviare. Per gli input multilinea, normalizza i caratteri di fine riga. E così via.

Nette risolve per voi vulnerabilità di sicurezza che la maggior parte dei programmatori non ha idea che esistano.

L'attacco CSRF menzionato consiste nel fatto che un aggressore attira la vittima a visitare una pagina che esegue silenziosamente una richiesta nel browser della vittima al server in cui la vittima è attualmente loggata, e il server crede che la richiesta sia stata fatta dalla vittima a suo piacimento. Pertanto, Nette impedisce l'invio del modulo tramite POST da un altro dominio. Se per qualche motivo si desidera disattivare la protezione e consentire l'invio del modulo da un altro dominio, utilizzare:

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

Questa protezione utilizza un cookie SameSite chiamato _nss. Pertanto, si deve creare un modulo prima di inviare il primo output, in modo che il cookie possa essere inviato.

La protezione dei cookie di SameSite potrebbe non essere affidabile al 100%, quindi è una buona idea attivare la protezione dei token:

$form->addProtection();

Si consiglia vivamente di applicare questa protezione ai moduli di una parte amministrativa dell'applicazione che modificano dati sensibili. Il framework protegge da un attacco CSRF generando e validando il token di autenticazione che viene memorizzato in una sessione (l'argomento è il messaggio di errore mostrato se il token è scaduto). Per questo motivo è necessario che sia avviata una sessione prima di visualizzare il modulo. Nella parte amministrativa del sito web, di solito la sessione è già avviata, grazie al login dell'utente. Altrimenti, avviare la sessione con il metodo Nette\Http\Session::start().

Ecco una rapida introduzione ai moduli in Nette. Per ulteriori spunti, si consiglia di consultare la directory degli esempi nella distribuzione.

versione: 4.0