Nella scorsa puntata abbiamo parlato di Doctrine, un componente che useremo per interfacciarci al database. Oggi scopriremo come trattare i form, attraverso un apposito componente. Dunque, procediamo con l’installazione:
composer require form
Perché un componente?
Personalmente odio le cose fatte a caso, senza capirne il motivo, perciò, prima di cominciare a bestemmiare senza sapere il perché, vorrei che capiste qual è il senso di gestire i form in questa maniera. Dopotutto potremmo benissimo farne a meno: quello che vogliamo ottenere dal componente Form non è altro che un codice HTML del tipo:
<form method="post">
<label for="#title">Titolo</label>
<input id="title" type="text" name="title">
<label for="#content">Contenuto</label>
<textarea id="content" name="content"></textarea>
<button type="submit">Salva</button>
</form>
Dunque, perché complicarci la vita con un componente PHP? La ragione è che ogni progetto ha bisogno di una certa manutenibilità, quindi di un modo efficiente per gestire determinate situazioni e definire dei vincoli, per dare un certo livello di integrità alla struttura del codice.
Il componente Form, oltre a definire i campi, gli attributi, le proprietà – e tutto quello che possa venirvi in mente – del form, permette di definire con facilità dei comportamenti in determinati momenti (al caricamento della pagina, al submit, e così via), di controllare meglio il codice, di renderlo portabile per poterlo usare in punti diversi del sito, di stabilire vincoli sui dati, di gestire il flusso in maniera molto strutturata e così via.
Form Type
Introduciamo, innanzitutto, il form type. Concettualmente possiamo dire che descrive il modulo che sarà renderizzato nell’HTML. In pratica è una normalissima classe PHP che definisce essenzialmente titolo e la tipologia di ogni campo.
Riprendendo l’esempio del blog, supponiamo che vogliamo inserire un nuovo articolo. Nella scorsa puntata abbiamo definito l’entity Article.php, che contiene l’id, il titolo, lo slug per definire l’URL, il contenuto, le date di creazione e di modifica e i relativi getter e setter. Rivediamola:
<?php // Article.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Article
*
* @ORM\Table(name="articles")
* @ORM\Entity
*/
class Article
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var String
*
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* @var string
*
* @ORM\Column(name="slug", type="string", length=100, unique=true)
*/
private $slug;
/**
* @var string
*
* @ORM\Column(name="content", type="text")
*/
private $content;
/**
* @var datetime
*
* @ORM\Column(name="creation_datetime", type="datetime", nullable=false)
*/
private $creationDateTime;
/**
* @var datetime
*
* @ORM\Column(name="last_modification_datetime", type="datetime", nullable=true)
*/
private $lastModificationDateTime;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setSlug($slug)
{
$this->slug = $slug;
}
public function getSlug()
{
return $this->slug;
}
public function setContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function setCreationDateTime($creationDateTime)
{
$this->creationDateTime = $creationDateTime;
}
public function getCreationDateTime()
{
return $this->creationDateTime;
}
public function setLastModificationDateTime($lastModificationDateTime)
{
$this->lastModificationDateTime = $lastModificationDateTime;
}
public function getLastModificationDateTime()
{
return $this->lastModificationDateTime;
}
}
Ora definiamo un form type che rappresenta il modulo di inserimento di un nuovo articolo. Creiamo dunque il file /src/form/ArticleType.php:
<?php // ArticleType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, ['required' => true])
->add('content', TextareaType::class, ['required' => true])
->getForm();
}
}
La classe del form type contiene fondamentalmente il metodo buildForm, in cui iniettiamo un oggetto Symfony/Component/Form/FormBuilderInterface, che utilizzeremo per definire i campi che vogliamo renderizzare. Tra tutti i campi presenti nell’entity Article, ha senso inserire nel form solo titolo e corpo (perché richiedono un inserimento da tastiera), mentre gli altri saranno generati automaticamente al submit (vedremo più tardi come fare). Il metodo add del builder richiede essenzialmente tre parametri. Il primo è il nome del campo e corrisponde alla proprietà dell’entity Article, letteralmente: cioè Symfony si aspetta di trovare nella classe una proprietà con quel nome! Gli altri due parametri sono opzionali e indicano rispettivamente il tipo (se non specificato, Symfony cerca di dedurlo) e un array che può contenere varie informazioni utili al rendering (required, placeholder, classi, id, attributi di vario genere).
Creazione del form
Prima di renderizzare il form, sostanzialmente, abbiamo bisogno di creare un oggetto di tipo Symfony/Component/Form/Form che segua la struttura definita in ArticleType e di passarlo poi alla view.
<?php // ArticleController.php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use App\Form\ArticleType;
use App\Entity\Article;
class ArticleController extends Controller
{
/**
* @Route("/create-new", name="article_new")
*/
public function newArticle(Request $request)
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Il form è stato sottomesso
}
return $this->render('Backend/new.html.twig', [
'form' => $form->createView()
]);
}
}
Abbiamo creato l’oggetto Form con la linea:
$form = $this->createForm(ArticleType::class, $article);
Come definito in ArticleType, il form deve renderizzare i campi title e content e deve essere “applicato” all’oggetto di tipo Article. A questo punto potreste storcere il naso: se l’oggetto Form serve a renderizzare il modulo HTML, a che serve passare un oggetto Article? Lo vedremo tra un attimo, quando parleremo del legame del form al dominio dell’applicazione.
Rendering
Vediamo di visualizzare questo maledetto form. Dalla action abbiamo passato alla view $form->createView(), che ritorna un oggetto pronto per essere manipolato da Twig:
{{ form_start(form) }}
{{ form_row(form) }}
<button type="submit">Pubblica</button>
{{ form_end(form) }}
Queste poche righe genereranno il codice HTML seguente:
<form name="article" method="post">
<div>
<div id="article">
<div>
<label for="article_title" class="required">Titolo</label>
<input type="text" id="article_title" name="article[title]" required="required" />
</div>
<div>
<label for="article_content" class="required">Corpo</label>
<textarea id="article_content" name="article[content]" required="required"></textarea>
</div>
</div>
</div>
<button type="submit">Pubblica</button>
</form>
Twig offre varie funzioni per manipolare il form, che puoi vedere nella doc ufficiale qui e qui.
Form e dominio dell’applicazione
Prima ci siamo chiesti a cosa servisse passare l’oggetto Article al form: ArticleType contiene tutto quello che serve per creare il modulo HTML! Dal punto di vista logico possiamo cautamente affermare che, attraverso un form, esprimiamo l’intenzione di creare un nuovo oggetto, generalmente una Entity. In pratica la funzione createForm crea un legame tra il modulo e l’oggetto Article. Grossolanamente vuol dire che Symfony sa che da quel form è usato per creare quell’oggetto e questo comporta il vantaggio che, quando il form viene sottomesso, abbiamo già l’oggetto Article con i valori title e content settati. Per essere più espliciti, in un’applicazione PHP pura, avremmo dovuto prendere tutti i valori $_POST (o quello che è) e settare una ad una le proprietà dell’oggetto Article. Per essere un pelo più tecnici, possiamo dire che il componente Form è legato al dominio dell’applicazione.
Il form è una maschera
Dovessi descrivere con una parola il form in Symfony, direi che è una maschera. Nonostante quello che abbiamo detto a proposito del legame con il dominio dell’applicazione, il Type che abbiamo definito sopra è legato all’oggetto che abbiamo passato, ma non è legato alla classe Article. In pratica abbiamo creato una “maschera” che accetta qualsiasi tipo di oggetto (quindi anche non Article) che abbia all’interno le proprietà title e content. Per dare una maggior solidità al progetto, potrebbe essere opportuno, invece, specificare la classe dell’oggetto a cui il form dovrebbe prestarsi. Per farlo basta inserire il metodo configureOptions nella classe ArticleType.php e configurarlo con l’oggetto OptionsResolver iniettato come segue:
<?php // ArticleType.php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, ['required' => true])
->add('content', TextareaType::class, ['required' => true])
->getForm();
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => App\Entity\Article::class
]);
}
}
Submit del form
A meno che non lo esplicitiamo, l’attributo HTML action del form non è settato, perciò quando il form viene sottomesso viene richiamata la stessa pagina. In pratica, al momento del submit, viene eseguita nuovamente l’action di Symfony. Per distinguere la fase di caricamento della pagina da quella di sottomissione del form, passiamo l’oggetto Request all’oggetto Form e controlliamo se il form è stato sottomesso e validato:
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { ... }
In pratica, tutto ciò che viene incluso in quell’if, è quello che succede quando il form viene sottomesso. Nel nostro esempio, con la sottomissione abbiamo creato l’oggetto Article con title e content, ma notiamo che non abbiamo settato ancora lo slug e le date di creazione e modifica. Inoltre non abbiamo nemmeno detto a Doctrine di persistere l’oggetto nel database! Dovrebbe essere banale comprendere il codice seguente:
<?php // ArticleType.php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use App\Form\ArticleType;
use App\Entity\Article;
class ArticleController extends Controller
{
/**
* @Route("/create-new", name="article_new")
*/
public function newArticle(Request $request)
{
$article = new Article();
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Il form è stato sottomesso
$now = new \DateTime();
$article->setCreationDateTime($now);
$article->setLastModificationDateTime(null);
$slug = $this->slugify($article->getTitle());
$article->setSlug($slug);
$em = $this->getDoctrine()->getManager();
$em->persist($article);
$em->flush();
return $this->redirectToRoute('article_show', [
'slug' => $article->getSlug()
]);
}
return $this->render('Backend/new.html.twig', [
'form' => $form->createView()
]);
}
}
Da notare che alla fine dell’if, viene effettuato un redirect con redirectToRoute ad un’altra rotta, che presumibilmente è quella che ci mostra l’articolo appena inserito. In questo caso possiamo distinguere due flussi: il primo, precedente al submit, è quello che crea il form, non entra nell’if, ed esegue il rendering di Backend/new.html.twig. Il secondo, successivo al submit, crea il form, entra nell’if e va in un’altra rotta.
I form in Symfony non sono immediatissimi da comprendere, ma spero di essere stato chiaro sui punti più importanti da cogliere. Le possibilità del componente Form sono molto ampie, quindi torneremo ancora sull’argomento. Nel prossimo articolo parleremo dei servizi, importanti contenitori attraverso cui è distribuita la logica dell’applicazione