Form

L’utilizzo dei form HTML è una delle attività più comuni e stimolanti per uno sviluppatore web. Symfony2 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 Symfony2. Per maggiori informazioni, vedere il Componente Form di Symfony2 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/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\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;
    }
}

Note

Se si sta provando a digitare questo esempio, bisogna prima creare AcmeTaskBundle lanciando il seguente comando (e accettando tutte le opzioni predefinite):

$ php app/console generate:bundle --namespace=Acme/TaskBundle

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 Symfony2, 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/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\TaskBundle\Entity\Task;
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('AcmeTaskBundle: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 Symfony2 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.

Symfony2 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:

images/book/form-simple.png

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! Scrivendo form(form), ciascun campo del form viene reso, insieme a un’etichetta e a un messaggio di errore (se presente). La funzione form inserisce anche il tag form necessario. Per quanto semplice, questo metodo non è molto flessibile (ancora). Di solito, si ha bisogno di rendere individualmente ciascun campo in modo da poter controllare la visualizzazione del form. Si imparerà a farlo 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 straetegia 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:

  1. 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.

  2. Quando l’utente invia il form, :method:`Symfony\\Component\\Form\\FormInterface::handleRequest` lo capisce e scrive immmediatamente i dati nelle proprietà task e dueDate dell’oggetto $task. Quindi tale oggetto viene validato. Se non è valido (la validazione è trattata nella prossima sezione), :method:`Symfony\\Component\\Form\\FormInterface::isValid` restituisce false 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.

  3. Quando l’utente invia il form con dati validi, i dati inviati sono scritti nuvamente 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 si ha bisogno di maggiore controllo su quando esattamente il form sia inviato o sui dati passati, si può usare il metodo :method:`Symfony\\Component\\Form\\FormInterface::submit`. Si può approdonfire 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 fluso 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, usaree 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 Symfony2, 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 Symfony2 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(
            'Acme\AcmeBundle\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 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 (Entity\Client::TYPE_PERSON == $data->getType()) {
                return array('person');
            } else {
                return array('company');
            }
        },
    ));
}

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'))
images/book/form-simple2.png

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 o NotNull) 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 utilizzato Length o Range) 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à:

Diamo uno sguardo a ogni parte:

  • form_start(form) - Rende il tag di apetrua del form;
  • 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 (ad esempio dueDate) all’interno, per impostazione predefinita, di un elemento div;
  • form_end(form) - Rende il tag di chiusura del form e tutti i campi che non sono ancora stati resi. Questo è utile per rendere campi nascosti e per sfruttare i vantaggi della protezione CSRF.

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, Symfony2 inserirà un campo nascosto chiamato “_method”, per memorizzare il metodo. Il form sarà inviato in POST, ma il router di Symfony2’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/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\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/Acme/TaskBundle/Controller/DefaultController.php

// aggiungere questa istruzione "use" in cima alla classe
use Acme\TaskBundle\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/Acme/TaskBundle/Controller/DefaultController.php
// ...

public function newAction()
{
    $task = ...;
    $form = $this->createForm('task', $task);

    // ...
}

o anche usarlo in un altro form:

// src/Acme/TaskBundle/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/Acme/TaskBundle/Entity/Category.php
namespace Acme\TaskBundle\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="Acme\TaskBundle\Entity\Category")
     */
    protected $category;

    // ...

    public function getCategory()
    {
        return $this->category;
    }

    public function setCategory(Category $category = null)
    {
        $this->category = $category;
    }
}

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/Acme/TaskBundle/Form/Type/CategoryType.php
namespace Acme\TaskBundle\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' => 'Acme\TaskBundle\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. Per attivare la validazione su CategoryType, aggiungere l’opzione cascade_validation a TaskType:

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Acme\TaskBundle\Entity\Task',
        'cascade_validation' => true,
    ));
}

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.

{# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #}

{% form_theme form with 'AcmeTaskBundle:Form:fields.html.twig' %}

{% form_theme form with ['AcmeTaskBundle:Form:fields.html.twig', 'AcmeTaskBundle:Form:fields2.html.twig'] %}

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 (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 da form_row per rendere la maggior parte dei campi;
  • textarea_widget - usato da form_widget per rendere un campo di tipo textarea;
  • field_errors - usato da form_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 personalizzrlo. 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 Acme/TaskBundle/Resources/views/Form creata in precedenza in tutti i template, modificare il file con la configurazione dell’applicazione:

Ogni frammento all’interno della cartella Acme/TaskBundle/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'      => 'Acme\TaskBundle\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 personalizzarei 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.

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:

  1. Passare un oggetto alla creazione del form (come primo parametro di createFormBuilder o come secondo parametro di createForm);
  2. 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