Form¶
L’utilizzo dei form HTML è una delle attività più comuni e stimolanti per uno sviluppatore web. Symfony integra un componente Form che permette di gestire facilmente i form. Con l’aiuto di questo capitolo si potrà creare da zero un form complesso, e imparare le caratteristiche più importanti della libreria dei form.
Note
Il componente form di Symfony è una libreria autonoma che può essere usata al di fuori dei progetti Symfony. Per maggiori informazioni, vedere la documentazione del componente Form su GitHub.
Creazione di un form semplice¶
Supponiamo che si stia costruendo un semplice applicazione “elenco delle cose da fare” che dovrà
visualizzare le “attività”. Poiché gli utenti avranno bisogno di modificare e creare attività, sarà
necessario costruire un form. Ma prima di iniziare, si andrà a vedere la generica
classe Task
che rappresenta e memorizza i dati di una singola attività:
// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
class Task
{
protected $task;
protected $dueDate;
public function getTask()
{
return $this->task;
}
public function setTask($task)
{
$this->task = $task;
}
public function getDueDate()
{
return $this->dueDate;
}
public function setDueDate(\DateTime $dueDate = null)
{
$this->dueDate = $dueDate;
}
}
Questa classe è un “vecchio-semplice-oggetto-PHP”, perché finora non ha nulla
a che fare con Symfony o qualsiasi altra libreria. È semplicemente un normale oggetto PHP,
che risolve un problema direttamente dentro la propria applicazione (cioè la necessità di
rappresentare un task nella propria applicazione). Naturalmente, alla fine di questo capitolo,
si sarà in grado di inviare dati all’istanza di un Task
(tramite un form HTML), validare
i suoi dati e persisterli nella base dati.
Costruire il Form¶
Ora che la classe Task
è stata creata, il prossimo passo è creare e
visualizzare il form HTML. In Symfony, lo si fa costruendo un oggetto
form e poi visualizzandolo in un template. Per ora, lo si può fare
all’interno di un controllore:
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
use AppBundle\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// crea un task fornendo alcuni dati fittizi per questo esempio
$task = new Task();
$task->setTask('Scrivere un post sul blog');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->add('save', 'submit', array('label' => 'Crea post'))
->getForm();
return $this->render('default/new.html.twig', array(
'form' => $form->createView(),
));
}
}
Tip
Questo esempio mostra come costruire il form direttamente nel controllore. Più tardi, nella sezione “Creare classi per i form”, si imparerà come costruire il form in una classe autonoma, metodo consigliato perché in questo modo il form diventa riutilizzabile.
La creazione di un form richiede relativamente poco codice, perché gli oggetti form di Symfony sono costruiti con un “costruttore di form”. Lo scopo del costruttore di form è quello di consentire di scrivere una semplice “ricetta” per il form e fargli fare tutto il lavoro pesante della costruzione del form.
In questo esempio sono stati aggiunti due campi al form, task
e dueDate
,
corrispondenti alle proprietà task
e dueDate
della classe Task
.
È stato anche assegnato un “tipo” ciascuno (ad esempio text
, date
), che, tra
le altre cose, determina quale tag form HTML viene utilizzato per tale campo.
Infine, è stato aggiunto un bottone submit, con un’etichetta personalizzata, per l’invio del form.
New in version 2.3: Il supporto per i bottoni submit è stato aggiunto in Symfony 2.3. Precedentemente, era necessario aggiungere i bottoni manualmente nel codice HTML.
Symfony ha molti tipi predefiniti che verranno trattati a breve (see Tipi di campo predefiniti).
Visualizzare il Form¶
Ora che il modulo è stato creato, il passo successivo è quello di visualizzarlo. Questo viene
fatto passando uno speciale oggetto form “view” al template (notare il
$form->createView()
nel controllore sopra) e utilizzando una serie di funzioni
aiutanti per i form:
Note
Questo esempio presuppone che sia stata creata una rotta chiamata task_new
che punta al controllore AcmeTaskBundle:Default:new
che
era stato creato precedentemente.
Questo è tutto! Bastano tre righe per rendere completamente il form:
form_start(form)
- Rende il tag iniziale del form, incluso l’attributo
enctype
, se si usa un caricamento di file; form_widget(form)
- Rende tutti i campi, inclusi l’elemento stesso, un’etichetta ed eventuali messaggi di errori;
form_end(form)
- Rende il tag finale del form e ogni campo che non sia ancora stato reso, nel caso in cui i campi siano stati resti singolarmante a mano. È utile per rendere campi nascosti e sfruttare la protezione CSRF automatica.
See also
Pur essendo facile, non è (ancora) flessibile. Di solito, si vorranno rendere i singoli campi, in modo da poter controllare l’aspetto del form. Si vedrà come fare nella sezione “Rendere un form in un template”.
Prima di andare avanti, notare come il campo input task
reso ha il value
della proprietà task
dall’oggetto $task
(ad esempio “Scrivere un post sul blog”).
Questo è il primo compito di un form: prendere i dati da un oggetto e tradurli
in un formato adatto a essere reso in un form HTML.
Tip
Il sistema dei form è abbastanza intelligente da accedere al valore della proprietà
protetta task
attraverso i metodi getTask()
e setTask()
della
classe Task
. A meno che una proprietà non sia privata, deve avere un metodo “getter” e uno
“setter”, in modo che il componente form possa ottenere e mettere dati nella
proprietà. Per una proprietà booleana, è possibile utilizzare un metodo “isser” o “hasser”
(per esempio isPublished()
o hasReminder
) invece di un getter (per esempio
getPublished()
o getReminder()
).
Gestione dell’invio del form¶
Il secondo compito di un form è quello di tradurre i dati inviati dall’utente alle proprietà di un oggetto. Affinché ciò avvenga, i dati inviati dall’utente devono essere associati al form. Aggiungere le seguenti funzionalità al controllore:
// ...
use Symfony\Component\HttpFoundation\Request;
public function newAction(Request $request)
{
// crea un nuovo oggetto $task (rimuove i dati fittizi)
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->add('save', 'submit', array('label' => 'Crea post'))
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
// esegue alcune azioni, come ad esempio salvare il task nella base dati
return $this->redirect($this->generateUrl('task_success'));
}
// ...
}
New in version 2.3: Il metodo :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` è stato
aggiunto in Symfony 2.3. In precedenza, veniva passata $request
al
metodo submit
, una strategia deprecata, che sarà rimossa
in Symfony 3.0. Per dettagli sul metodo, vedere cookbook-form-submit-request.
Questo controllore segue uno schema comune per gestire i form e ha tre possibili percorsi:
Quando in un browser inizia il caricamento di una pagina, il form viene creato e reso. :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` capisce che il form non è stato inviato e non fa nulla. :method:`Symfony\\Component\\Form\\FormInterface::isValid` restituisce
false
se il form non è stato inviato.Quando l’utente invia il form, :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` lo capisce e scrive immediatamente i dati nelle proprietà
task
edueDate
dell’oggetto$task
. Quindi tale oggetto viene validato. Se non è valido (la validazione è trattata nella prossima sezione), :method:`Symfony\\Component\\Form\\FormInterface::isValid` restituiscefalse
di nuovo, quindi il form viene reso insieme agli errori di validazione;Note
Si può usare il metodo :method:`Symfony\\Component\\Form\\FormInterface::isSubmitted` per verificare se il form sia stato inviato, indipendentemente dal fatto che i dati inviati siano validi o meno.
Quando l’utente invia il form con dati validi, i dati inviati sono scritti nuovamente nel form, ma stavolta :method:`Symfony\\Component\\Form\\FormInterface::isValid` restituisce
true
. Ora si ha la possibilità di eseguire alcune azioni usando l’oggetto$task
(ad esempio persistendolo nella base dati) prima di rinviare l’utente a un’altra pagina (ad esempio una pagina “thank you” o “success”).
Note
Reindirizzare un utente dopo aver inviato con successo un form impedisce l’utente di essere in grado di premere il tasto “aggiorna” del suo browser e reinviare i dati.
See also
Se occorre maggior controllo su quando esattamente il form è inviato o su quali dati siano passati, si può usare il metodo :method:`Symfony\\Component\\Form\\FormInterface::submit`. Si può approfondire nel ricettario.
Inviare form con bottoni di submit multipli¶
New in version 2.3: Il supporto per i bottoni nei form è stato aggiunto in Symfony 2.3.
Quando un form contiene più di un bottone di submit, si vuole sapere quale dei bottoni sia stato cliccato, per adattare il flusso del controllore. Aggiungiamo un secondo bottone “Salva e aggiungi” al form:
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->add('save', 'submit', array('label' => 'Crea post'))
->add('saveAndAdd', 'submit', array('label' => 'Salva e aggiungi'))
->getForm();
Nel controllore, usare il metodo :method:`Symfony\\Component\\Form\\ClickableInterface::isClicked` del bottone per sapere se sia stato cliccato il bottone “Salva e aggiungi”:
if ($form->isValid()) {
// ... eseguire un'azione, come salvare il task nella base dati
$nextAction = $form->get('saveAndAdd')->isClicked()
? 'task_new'
: 'task_success';
return $this->redirect($this->generateUrl($nextAction));
}
Validare un form¶
Nella sezione precedente, si è appreso come un form può essere inviato con dati
validi o invalidi. In Symfony, la validazione viene applicata all’oggetto sottostante
(per esempio Task
). In altre parole, la questione non è se il “form” è
valido, ma se l’oggetto $task
è valido o meno dopo che al form sono stati
applicati i dati inviati. La chiamata di $form->isValid()
è una scorciatoia
che chiede all’oggetto $task
se ha dati validi o meno.
La validazione è fatta aggiungendo di una serie di regole (chiamate vincoli) a una classe. Per
vederla in azione, verranno aggiunti vincoli di validazione in modo che il campo task
non possa
essere vuoto e il campo dueDate
non possa essere vuoto e debba essere un oggetto DateTime
valido.
Questo è tutto! Se si re-invia il form con i dati non validi, si vedranno i rispettivi errori visualizzati nel form.
La validazione è una caratteristica molto potente di Symfony e dispone di un proprio capitolo dedicato.
Gruppi di validatori¶
Se un oggetto si avvale dei gruppi di validatori, occorrerà specificare quali gruppi di convalida deve usare il form:
$form = $this->createFormBuilder($users, array(
'validation_groups' => array('registrazione'),
))->add(...);
Se si stanno creando classi per i form (una
buona pratica), allora si avrà bisogno di aggiungere quanto segue al metodo
setDefaultOptions()
:
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array('registrazione'),
));
}
In entrambi i casi, solo il gruppo di validazione registrazione
verrà
utilizzato per validare l’oggetto sottostante.
Disabilitare la validazione¶
New in version 2.3: La possibilità di impostare validation_groups
a false
è stata aggiunta in Symfony 2.3.
A volte è utile sopprimere la validazione per un intero form. Per questi
casi, si può impostare l’opzione validation_groups
a false
:
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => false,
));
}
Notare che in questo caso il form eseguirà comunque alcune verifiche basilari di integrità, per esempio se un file caricato è troppo grande o se dei campi non esistenti sono stati inviati. Se si vuole sopprimere completamente la validazione, si può usare l’evento POST_SUBMIT.
Gruppi basati su dati inseriti¶
Se si ha bisogno di una logica avanzata per determinare i gruppi di validazione (p.e.
basandosi sui dati inseriti), si può impostare l’opzione validation_groups
a
un callback o a una Closure
:
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array(
'AppBundle\Entity\Client',
'determineValidationGroups',
),
));
}
Questo richiamerà il metodo statico determineValidationGroups()
della classe
Client
, dopo il bind del form ma prima dell’esecuzione della validazione.
L’oggetto Form è passato come parametro del metodo (vedere l’esempio successivo).
Si può anche definire l’intera logica con una Closure:
use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
if (Client::TYPE_PERSON == $data->getType()) {
return array('person');
}
return array('company');
},
));
}
L’uso dell’opzione validation_groups
sovrascrive il gruppo di validazione predefinito
in uso. Se si vogliono validare anche i vincoli predefiniti
dell’entità, si deve cambiare l’opzione in questo modo:
use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
if (Client::TYPE_PERSON == $data->getType()) {
return array('Default', 'person');
}
return array('Default', 'company');
},
));
}
Si possono trovare maggiori informazioni su come funzionino i gruppi di validazione e i vincoli predefiniti nella sezione del libro relativa ai gruppi di validazione.
Gruppi basati sul bottone cliccato¶
New in version 2.3: Il supporto per i bottoni nei form è stato aggiunto in Symfony 2.3.
Se un form contiene più bottoni submit, si può modificare il gruppo di validazione, a seconda di quale bottone sia stato usato per inviare il form. Per esempi, consideriamo un form in sequenza, in cui si può avanzare al passo successivo o tornare al passo precedente. Ipotizziamo anche che, quando si torna al passo precedente, i dati del form debbano essere salvati, ma non validati.
Prima di tutto, bisogna aggiungere i due bottoni al form:
$form = $this->createFormBuilder($task)
// ...
->add('nextStep', 'submit')
->add('previousStep', 'submit')
->getForm();
Quindi, occorre configurare il bottone che torna al passo precedente per eseguire
specifici gruppi di validazione. In questo esempio, vogliamo sopprimere la validazione,
quindi impostiamo l’opzione validation_groups
a false
:
$form = $this->createFormBuilder($task)
// ...
->add('previousStep', 'submit', array(
'validation_groups' => false,
))
->getForm();
Ora il form salterà i controlli di validazione. Validerà comunque i vincoli basilari di integrità, come il controllo se un file caricato sia troppo grande o se si sia tentato di inserire del testo in un campo numerico.
Tipi di campo predefiniti¶
Symfony dispone di un folto gruppo di tipi di campi che coprono tutti i campi più comuni e i tipi di dati di cui necessitano i form:
È anche possibile creare dei tipi di campi personalizzati. Questo argomento è trattato nell’articolo “/cookbook/form/create_custom_field_type” del ricettario.
Opzioni dei tipi di campo¶
Ogni tipo di campo ha un numero di opzioni che può essere utilizzato per la configurazione.
Ad esempio, il campo dueDate
è attualmente reso con 3 menu
select. Tuttavia, il campo data può essere
configurato per essere reso come una singola casella di testo (in cui l’utente deve inserire
la data nella casella come una stringa):
->add('dueDate', 'date', array('widget' => 'single_text'))
Ogni tipo di campo ha un numero di opzioni differente che possono essere passate a esso. Molte di queste sono specifiche per il tipo di campo e i dettagli possono essere trovati nella documentazione di ciascun tipo.
Indovinare il tipo di campo¶
Ora che sono stati aggiunti i metadati di validazione alla classe Task
, Symfony
sa già un po’ dei campi. Se lo si vuole permettere, Symfony può “indovinare”
il tipo del campo e impostarlo al posto vostro. In questo esempio, Symfony può
indovinare dalle regole di validazione che il campo task
è un normale
campo text
e che il campo dueDate
è un campo date
:
public function newAction()
{
$task = new Task();
$form = $this->createFormBuilder($task)
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit')
->getForm();
}
Questa funzionalità si attiva quando si omette il secondo parametro del metodo
add()
(o se si passa null
a esso). Se si passa un array di opzioni come
terzo parametro (fatto sopra per dueDate
), queste opzioni vengono applicate
al campo indovinato.
Caution
Se il form utilizza un gruppo specifico di validazione, la funzionalità che indovina il tipo di campo prenderà ancora in considerazione tutti i vincoli di validazione quando andrà a indovinare i tipi di campi (compresi i vincoli che non fanno parte del processo di convalida dei gruppi in uso).
Indovinare le opzioni dei tipi di campo¶
Oltre a indovinare il “tipo” di un campo, Symfony può anche provare a indovinare i valori corretti di una serie di opzioni del campo.
Tip
Quando queste opzioni vengono impostate, il campo sarà reso con speciali attributi
HTML che forniscono la validazione HTML5 lato client. Tuttavia,
non genera i vincoli equivalenti lato server (ad esempio Assert\MaxLength
).
E anche se si ha bisogno di aggiungere manualmente la validazione lato server, queste
opzioni dei tipi di campo possono essere ricavate da queste informazioni.
required
- L’opzione
required
può essere indovinata in base alle regole di validazione (cioè se il campo èNotBlank
oNotNull
) o dai metadati di Doctrine (vale a dire se il campo ènullable
). Questo è molto utile, perché la validazione lato client corrisponderà automaticamente alle vostre regole di validazione. max_length
- Se il campo è un qualche tipo di campo di testo, allora l’opzione
max_length
può essere indovinata dai vincoli di validazione (se viene utilizzatoLength
oRange
) o dai metadati Doctrine (tramite la lunghezza del campo).
Note
Queste opzioni di campi vengono indovinate solo se si sta usando Symfony per ricavare
il tipo di campo (ovvero omettendo o passando null
nel secondo parametro di add()
).
Se si desidera modificare uno dei valori indovinati, è possibile sovrascriverlo passando l’opzione nell’array di opzioni del campo:
->add('task', null, array('max_length' => 4))
Rendere un form in un template¶
Finora si è visto come un intero form può essere reso con una sola linea di codice. Naturalmente, solitamente si ha bisogno di molta più flessibilità:
Abbiamo già visto le funzioni form_start()
e form_end()
, ma cosa fanno
le altre funzioni?
form_errors(form)
- Rende eventuali errori globali per l’intero modulo (gli errori specifici dei campi vengono visualizzati accanto a ciascun campo);
form_row(form.dueDate)
- Rende l’etichetta, eventuali errori e il widget HTML del form per il dato
campo (p.e.
dueDate
) all’interno, per impostazione predefinita, di un elementodiv
;
La maggior parte del lavoro viene fatto dall’helper form_row
, che rende
l’etichetta, gli errori e i widget HTML del form di ogni campo all’interno di un tag div
per impostazione predefinita. Nella sezione Temi con i form, si apprenderà come l’output
di form_row
possa essere personalizzato su diversi livelli.
Tip
Si può accedere ai dati attuali del form tramite form.vars.value
:
Rendere manualmente ciascun campo¶
L’aiutante form_row
è utile, perché si può rendere ciascun campo del form
molto facilmente (e il markup utilizzato per la “riga” può essere personalizzato
a piacere). Ma poiché la vita non è sempre così semplice, è anche possibile rendere ogni campo
interamente a mano. Il risultato finale del codice che segue è lo stesso di quando
si è utilizzato l’aiutante form_row
:
Se la label auto-generata di un campo non è giusta, si può specificarla esplicitamente:
Alcuni tipi di campi hanno opzioni di resa aggiuntive che possono essere passate
al widget. Queste opzioni sono documentate con ogni tipo, ma un’opzione
comune è attr
, che permette di modificare gli attributi dell’elemento form.
Di seguito viene aggiunta la classe task_field
al resa del campo
casella di testo:
Se occorre rendere dei campi “a mano”, si può accedere ai singoli valori dei campi,
come id
, name
e label
. Per esempio, per ottenere
id
:
Per ottenere il valore usato per l’attributo nome dei campi del form, occorre usare
il valore full_name
:
Riferimento alle funzioni del template Twig¶
Se si utilizza Twig, un riferimento completo alle funzioni di resa è disponibile nel manuale di riferimento. Leggendolo si può sapere tutto sugli helper disponibili e le opzioni che possono essere usate con ciascuno di essi.
Cambiare azione e metodo di un form¶
Finora, è stato usato l’helper form_start()
per rendere il tag di aperture del form,
ipotizzando che ogni form sia inviato allo stesso URL in POST.
A volte si vogliono cambiare questi parametri. Lo si può fare in modi diversi.
Se si costruisce il form nel controllore, si può usare setAction()
e
setMethod()
:
$form = $this->createFormBuilder($task)
->setAction($this->generateUrl('target_route'))
->setMethod('GET')
->add('task', 'text')
->add('dueDate', 'date')
->add('save', 'submit')
->getForm();
Note
Questo esempio ipotizza la presenza di una rotta target_route
,
che punti al controllore che processerà il form.
In Creare classi per i form, vedremo come spostare il codice di costruzione del form in una classe separata. Quando si usa una classe form esterna nel controllore, si possono passare azione e metodo come opzioni:
$form = $this->createForm(new TaskType(), $task, array(
'action' => $this->generateUrl('target_route'),
'method' => 'GET',
));
Infine, si possono sovrascrivere azione e metodo nel template, passandoli all’aiutante
form()
o form_start()
:
Note
Se il metodo del form non è GET o POST, ma PUT, PATCH o DELETE, Symfony inserirà un campo nascosto chiamato “_method”, per memorizzare il metodo. Il form sarà inviato in POST, ma il router di Symfony’s è in grado di rilevare il parametro “_method” e interpretare la richiesta come PUT, PATCH o DELETE. Si veda la ricetta “/cookbook/routing/method_parameters” per maggiori informazioni.
Creare classi per i form¶
Come si è visto, un form può essere creato e utilizzato direttamente in un controllore. Tuttavia, una pratica migliore è quella di costruire il form in una apposita classe PHP, che può essere riutilizzata in qualsiasi punto dell’applicazione. Creare una nuova classe che ospiterà la logica per la costruzione del form task:
// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, array('widget' => 'single_text'))
->add('save', 'submit')
;
}
public function getName()
{
return 'task';
}
}
Questa nuova classe contiene tutte le indicazioni necessarie per creare il form
task (notare che il metodo getName()
dovrebbe restituire un identificatore univoco per questo
“tipo” di form). Può essere usato per costruire rapidamente un oggetto form nel controllore:
// src/AppBundle/Controller/DefaultController.php
// add this new use statement at the top of the class
use AppBundle\Form\Type\TaskType;
public function newAction()
{
$task = ...;
$form = $this->createForm(new TaskType(), $task);
// ...
}
Porre la logica del form in una classe a parte significa che il form può essere facilmente riutilizzato in altre parti del progetto. Questo è il modo migliore per creare form, ma la scelta in ultima analisi, spetta allo sviluppatore.
Tip
Quando si mappano form su oggetti, tutti i campi vengono mappati. Ogni campo nel form che non esiste nell’oggetto mappato causerà il lancio di un’eccezione.
Nel caso in cui servano campi extra nel form (per esempio, un checkbox “accetto
i termini”), che non saranno mappati nell’oggetto sottostante,
occorre impostare l’opzione mapped
a false
:
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('task')
->add('dueDate', null, array('mapped' => false))
->add('save', 'submit')
;
}
Inoltre, se ci sono campi nel form che non sono inclusi nei dati inviati,
tali campi saranno impostati esplicitamente a null
.
Si può accedere ai dati del campo in un controllore con:
$form->get('dueDate')->getData();
Inoltre, anche i dati di un campo non mappato si possono modificare direttamente:
$form->get('dueDate')->setData(new \DateTime());
Definire i form come servizi¶
La definizione dei form type come servizi è una buona pratica e li rende riusabili facilmente in un’applicazione.
Note
I servizi e il contenitore di servizi saranno trattati più avanti nel libro. Le cose saranno più chiaro dopo aver letto quel capitolo.
Ecco fatto! Ora si può usare il form type direttamente in un controllore:
// src/AppBundle/Controller/DefaultController.php
// ...
public function newAction()
{
$task = ...;
$form = $this->createForm('task', $task);
// ...
}
o anche usarlo in un altro form:
// src/AppBundle/Form/Type/ListType.php
// ...
class ListType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('someTask', 'task');
}
}
Si veda form-cookbook-form-field-service per maggiori informazioni.
I form e Doctrine¶
L’obiettivo di un form è quello di tradurre i dati da un oggetto (ad esempio Task
) a un
form HTML e quindi tradurre i dati inviati dall’utente indietro all’oggetto originale. Come
tale, il tema della persistenza dell’oggetto Task
nella base dati è interamente
non correlato al tema dei form. Ma, se la classe Task
è stata configurata
per essere salvata attraverso Doctrine (vale a dire che per farlo si è aggiunta la
mappatura dei metadati), allora si può salvare
dopo l’invio di un form, quando il form stesso è valido:
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($task);
$em->flush();
return $this->redirect($this->generateUrl('task_success'));
}
Se, per qualche motivo, non si ha accesso all’oggetto originale $task
,
è possibile recuperarlo dal form:
$task = $form->getData();
Per maggiori informazioni, vedere il capitolo ORM Doctrine.
La cosa fondamentale da capire è che quando il form viene riempito, i dati inviati vengono trasferiti immediatamente all’oggetto sottostante. Se si vuole persistere i dati, è sufficiente persistere l’oggetto stesso (che già contiene i dati inviati).
Incorporare form¶
Spesso, si vuole costruire form che includono campi provenienti da oggetti
diversi. Ad esempio, un form di registrazione può contenere dati appartenenti
a un oggetto User
così come a molti oggetti Address
. Fortunatamente, questo
è semplice e naturale con il componente per i form.
Incorporare un oggetto singolo¶
Supponiamo che ogni Task
appartenga a un semplice oggetto Category
. Si parte,
naturalmente, con la creazione di un oggetto Category
:
// src/AppBundle/Entity/Category.php
namespace AppBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Category
{
/**
* @Assert\NotBlank()
*/
public $name;
}
Poi, aggiungere una nuova proprietà category
alla classe Task
:
// ...
class Task
{
// ...
/**
* @Assert\Type(type="AppBundle\Entity\Category")
* @Assert\Valid()
*/
protected $category;
// ...
public function getCategory()
{
return $this->category;
}
public function setCategory(Category $category = null)
{
$this->category = $category;
}
}
Tip
Il vincolo Valid
è stato aggiunto alla proprietà category
. In questo modo si valida a
cascata l’entità corrispondente. Se si omette tale vincolo, l’entità
figlia non sarà validata.
Ora che l’applicazione è stata aggiornata per riflettere le nuove esigenze,
creare una classe di form in modo che l’oggetto Category
possa essere modificato dall’utente:
// src/AppBundle/Form/Type/CategoryType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Category',
));
}
public function getName()
{
return 'category';
}
}
L’obiettivo finale è quello di far si che la Category
di un Task
possa essere correttamente modificata
all’interno dello stesso form task. Per farlo, aggiungere il campo category
all’oggetto TaskType
, il cui tipo è un’istanza della nuova classe
CategoryType
:
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('category', new CategoryType());
}
I campi di CategoryType
ora possono essere resi accanto a quelli
della classe TaskType
.
Rendere i campi di Category
allo stesso modo dei campi Task
originali:
Quando l’utente invia il form, i dati inviati con i campi Category
sono utilizzati per costruire un’istanza di Category
, che viene poi impostata sul
campo category
dell’istanza Task
.
L’istanza Category
è accessibile naturalmente attraverso $task->getCategory()
e può essere memorizzata nella base dati o utilizzata quando serve.
Incorporare un insieme di form¶
È anche possibile incorporare un insieme di form in un form (si immagini un form
Category
con tanti sotto-form Product
).
Lo si può fare utilizzando il tipo di campo collection
.
Per maggiori informazioni, vedere la ricetta “/cookbook/form/form_collections” e il riferimento al tipo collection.
Temi con i form¶
Ogni parte nel modo in cui un form viene reso può essere personalizzata. Si è liberi di cambiare
come ogni “riga” del form viene resa, modificare il markup utilizzato per rendere gli errori, o
anche personalizzare la modalità con cui un tag textarea
dovrebbe essere rappresentato. Nulla è off-limits,
e personalizzazioni differenti possono essere utilizzate in posti diversi.
Symfony utilizza i template per rendere ogni singola parte di un form, come ad esempio
i tag label
, i tag input
, i messaggi di errore e ogni altra cosa.
In Twig, ogni “frammento” di form è rappresentato da un blocco Twig. Per personalizzare una qualunque parte di come un form è reso, basta sovrascrivere il blocco appropriato.
In PHP, ogni “frammento” è reso tramite un file template individuale. Per personalizzare una qualunque parte del modo in cui un form viene reso, basta sovrascrivere il template esistente creandone uno nuovo.
Per capire come funziona, cerchiamo di personalizzare il frammento form_row
e
aggiungere un attributo class all’elemento div
che circonda ogni riga. Per
farlo, creare un nuovo file template per salvare il nuovo codice:
Il frammento di form field_row
è utilizzato per rendere la maggior parte dei campi attraverso la
funzione form_row
. Per dire al componente form di utilizzare il nuovo frammento
field_row
definito sopra, aggiungere il codice seguente all’inizio del template che
rende il form:
Il tag form_theme
(in Twig) “importa” i frammenti definiti nel dato
template e li usa quando deve rendere il form. In altre parole, quando la
funzione form_row
è successivamente chiamata in questo template, utilizzerà il
blocco field_row
dal tema personalizzato (al posto del blocco predefinito field_row
fornito con Symfony).
Non è necessario che il tema personalizzato sovrascriva tutti i blocchi. Quando viene reso un blocco non sovrascrritto nel tema personalizzato, il sistema dei temi userà il tema globale (definito a livello di bundle).
Se vengono forniti più temi personalizzati, saranno analizzati nell’ordine elencato, prima di usare il tema globale.
Per personalizzare una qualsiasi parte di un form, basta sovrascrivere il frammento appropriato. Sapere esattamente qual è il blocco o il file da sovrascrivere è l’oggetto della sezione successiva.
Per una trattazione più ampia, vedere /cookbook/form/form_customization.
Nomi per i frammenti di form¶
In Symfony, ogni parte di un form che viene reso (elementi HTML del form, errori, etichette, ecc.) è definito in un tema base, che in Twig è una raccolta di blocchi e in PHP una collezione di file template.
In Twig, ogni blocco necessario è definito in un singolo file template (p.e. form_div_layout.html.twig) che si trova all’interno di Twig Bridge. Dentro questo file, è possibile ogni blocco necessario alla resa del form e ogni tipo predefinito di campo.
In PHP, i frammenti sono file template individuali. Per impostazione predefinita sono posizionati nella cartella Resources/views/Form del bundle framework (vedere su GitHub).
Ogni nome di frammento segue lo stesso schema di base ed è suddiviso in due pezzi,
separati da un singolo carattere di sottolineatura (_
). Alcuni esempi sono:
field_row
- usato daform_row
per rendere la maggior parte dei campi;textarea_widget
- usato daform_widget
per rendere un campo di tipotextarea
;field_errors
- usato daform_errors
per rendere gli errori di un campo;
Ogni frammento segue lo stesso schema di base: type_part
. La parte type
corrisponde al campo type che viene reso (es. textarea
, checkbox
,
date
, ecc) mentre la parte part
corrisponde a cosa si sta
rendendo (es. label
, widget
, errors
, ecc). Per impostazione predefinita, ci
sono 4 possibili parti di un form che possono essere rese:
label |
(es. form_label ) |
rende l’etichetta dei campi |
widget |
(es. form_widget ) |
rende la rappresentazione HTML dei campi |
errors |
(es. form_errors ) |
rende gli errori dei campi |
row |
(es. form_row ) |
rende l’intera riga del campo (etichetta, widget ed errori) |
Note
In realtà ci sono altre 3 parti (rows
, rest
e enctype
),
ma raramente c’è la necessità di sovrascriverle.
Conoscendo il tipo di campo (ad esempio textarea
) e che parte si vuole
personalizzare (ad esempio widget
), si può costruire il nome del frammento che
deve essere sovrascritto (esempio textarea_widget
).
Ereditarietà dei frammenti di template¶
In alcuni casi, il frammento che si vuole personalizzare sembrerà mancare.
Ad esempio, non c’è nessun frammento textarea_errors
nei temi predefiniti
forniti con Symfony. Quindi dove sono gli errori di un campo textarea che deve essere reso?
La risposta è: nel frammento field_errors
. Quando Symfony rende gli errori
per un tipo textarea, prima cerca un frammento textarea_errors
, poi cerca
un frammento form_errors
. Ogni tipo di campo ha un tipo genitore
(il tipo genitore di textarea
è text
) e Symfony utilizza il
frammento per il tipo del genitore se il frammento di base non
esiste.
Quindi, per ignorare gli errori dei soli campi textarea
, copiare il
frammento form_errors
, rinominarlo in textarea_errors
e personalizzarlo. Per
sovrascrivere la resa degli errori predefiniti di tutti i campi, copiare e personalizzare
direttamente il frammento form_errors
.
Tip
Il tipo “genitore” di ogni tipo di campo è disponibile per ogni tipo di campo in form type reference
Temi globali per i form¶
Nell’esempio sopra, è stato utilizzato l’helper form_theme
(in Twig) per “importare”
i frammenti personalizzati solo in quel form. Si può anche dire a Symfony
di importare personalizzazioni del form nell’intero progetto.
Twig¶
Per includere automaticamente i blocchi personalizzati del template
fields.html.twig
creato in precedenza, in tutti i template, modificare il file
della configurazione dell’applicazione:
Tutti i blocchi all’interno del template fields.html.twig
vengono ora utilizzati a livello globale
per definire l’output del form.
PHP¶
Per includere automaticamente i template personalizzati dalla cartella app/Resources/views/Form
creata in precedenza in tutti i template, modificare il file con la configurazione
dell’applicazione:
Ogni frammento all’interno della cartella app/Resources/views/Form
è ora
usato globalmente per definire l’output del form.
Protezione da CSRF¶
CSRF, o Cross-site request forgery, è un metodo mediante il quale un utente malintenzionato cerca di fare inviare inconsapevolmente agli utenti legittimi dati che non vorrebbero inviare. Fortunatamente, gli attacchi CSRF possono essere prevenuti, utilizzando un token CSRF all’interno dei form.
La buona notizia è che, per impostazione predefinita, Symfony integra e convalida i token CSRF automaticamente. Questo significa che è possibile usufruire della protezione CSRF, senza dover far nulla. Infatti, ogni form in questo capitolo sfrutta la protezione CSRF!
La protezione CSRF funziona con l’aggiunta al form di un campo nascosto, il cui nome
predefinito è _token
, che contiene un valore noto solo allo sviluppatore e all’utente. Questo
garantisce che proprio l’utente, e non qualcun altro, stia inviando i dati.
Symfony valida automaticamente la presenza e l’esattezza di questo token.
Il campo _token
è un campo nascosto e sarà reso automaticamente
se si include la funzione form_end()
nel template, perché questa assicura
che tutti i campi non ancora resi vengano visualizzati.
Il token CSRF può essere personalizzato specificatamente per ciascun form. Per esempio:
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskType extends AbstractType
{
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
'csrf_protection' => true,
'csrf_field_name' => '_token',
// una chiave univoca per generare il token
'intention' => 'task_item',
));
}
// ...
}
Per disabilitare la protezione CSRF, impostare l’opzione csrf_protection
a false
.
Si può anche personalizzare a livello globale nel progetto. Per ulteriori informazioni,
vedere la sezione
riferimento della configurazione dei form.
Note
L’opzione intention
è facoltativa, ma migliora notevolmente la sicurezza
del token generato, rendendolo diverso per ogni modulo.
Caution
I token CSRF sono pensati per essere diversi per ciascun utente. Per questo motivo, occorre cautela nel provare a mettere in cache pagine con form che includano questo tipo di protezione. Per maggiori informazioni, vedere /cookbook/cache/form_csrf_caching.
Usare un form senza una classe¶
Nella maggior parte dei casi, un form è legato a un oggetto e i campi del form prendono i loro dati dalle proprietà di tale oggetto. Questo è quanto visto finora in questo capitolo, con la classe Task.
A volte, però, si vuole solo usare un form senza classi, per ottenere un array di dati inseriti. Lo si può fare in modo molto facile:
// assicurarsi di aver importato lo spazio dei nomi Request all'inizio della classe
use Symfony\Component\HttpFoundation\Request
// ...
public function contactAction(Request $request)
{
$defaultData = array('message' => 'Type your message here');
$form = $this->createFormBuilder($defaultData)
->add('name', 'text')
->add('email', 'email')
->add('message', 'textarea')
->add('send', 'submit')
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
// data è un array con "name", "email", e "message" come chiavi
$data = $form->getData();
}
// ... rendere il form
}
Per impostazione predefinita, un form ipotizza che si voglia lavorare con array di dati, invece che con oggetti. Ci sono due modi per modificare questo comportamento e legare un form a un oggetto:
- Passare un oggetto alla creazione del form (come primo parametro di
createFormBuilder
o come secondo parametro dicreateForm
); - Dichiarare l’opzione
data_class
nel form.
Se non si fa nessuna di queste due cose, il form restituirà i dati come
array. In questo esempio, poiché $defaultData
non è un oggetto (e
l’opzione data_class
è omessa), $form->getData()
restituirà
un array.
Tip
Si può anche accedere ai valori POST (“name”, in questo caso) direttamente tramite
l’oggetto Request
, in questo modo:
$this->get('request')->request->get('name');
Tuttavia, si faccia attenzione che in molti casi l’uso del metodo getData()
è
preferibile, poiché restituisce i dati (solitamente un oggetto) dopo
che sono stati manipolati dal sistema dei form.
Aggiungere la validazione¶
L’ultima parte mancante è la validazione. Solitamente, quando si richiama $form->isValid()
,
l’oggetto viene validato dalla lettura dei vincoli applicati alla
classe. Se il form è legato a un oggetto (cioè se si sta usando l’opzione data_class
o passando un oggetto al form), questo è quasi sempre l’approccio
desiderato. Vedere /book/validation per maggiori dettagli.
Ma se il form non è legato a un oggetto e invece si sta recuperando un semplice array di dati inviati, come si possono aggiungere vincoli al form?
La risposta è: impostare i vincoli in modo autonomo e passarli al form. L’approccio generale è spiegato meglio nel capitolo sulla validazione, ma ecco un breve esempio:
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
$builder
->add('firstName', 'text', array(
'constraints' => new Length(array('min' => 3)),
))
->add('lastName', 'text', array(
'constraints' => array(
new NotBlank(),
new Length(array('min' => 3)),
),
))
;
Tip
Se si usano i gruppi di validazione, occorre fare riferimento al gruppo
Default
quando si crea il form, oppure impostare il gruppo corretto
nel vincolo che si sta aggiungendo.
new NotBlank(array('groups' => array('create', 'update'))
Considerazioni finali¶
Ora si è a conoscenza di tutti i mattoni necessari per costruire form complessi e
funzionali per la propria applicazione. Quando si costruiscono form, bisogna tenere presente che
il primo obiettivo di un form è quello di tradurre i dati da un oggetto (Task
) a un
form HTML in modo che l’utente possa modificare i dati. Il secondo obiettivo di un form è quello di
prendere i dati inviati dall’utente e ri-applicarli all’oggetto.
Ci sono altre cose da imparare sul potente mondo dei form, ad esempio come gestire il caricamento di file con Doctrine o come creare un form dove un numero dinamico di sub-form possono essere aggiunti (ad esempio una todo list in cui è possibile continuare ad aggiungere più campi tramite Javascript prima di inviare). Vedere il ricettario per questi argomenti. Inoltre, assicurarsi di basarsi sulla documentazione di riferimento sui tipi di campo, che comprende esempi di come usare ogni tipo di campo e le relative opzioni.
Saperne di più con il ricettario¶
- /cookbook/doctrine/file_uploads
- Riferimento del tipo di campo file
- Creare tipi di campo personalizzati
- /cookbook/form/form_customization
- /cookbook/form/dynamic_form_modification
- /cookbook/form/data_transformers
- /cookbook/security/csrf_in_login_form
- /cookbook/cache/form_csrf_caching