TSM - Gestionarea datelor folosind Symfony Forms

Sergiu Stupariu - PHP Developer @Pentalog

Pornind de la dezvoltarea de simple aplicații de tip CRUD și până la cele mai complexe și mari website-uri, gestionarea și lucrul cu form-uri HTML reprezintă unul dintre cele mai frecvente și provocatoare task-uri pentru un web developer. 

 Pe lîngă numeroase module și functionalităti, framework-ul de PHP Symfony pune la dispoziție o componentă specializată pe form-uri care are ca scop ușurarea întregului proces de manipulare și stocare a datelor de intrare. Această componentă poate fi folosită și în afară proiectelor Symfony, fiind o librărie de sine-stătătoare ușor integrabilă, dar este adresată în special aplicațiilor care prezintă o arhitectură construită conform principiului separation of concerns, de regulă Model-View-Controller. Vom vedea în continuare cum, cu ajutorul acestei abordări, componenta reprezintă form-urile sub forma unor clase strâns legate de entități din Model

 Cel mai simplu exemplu ar fi înregistrarea unui nou user pe website. Pentru aceasta se vor defini două clase: User - Entitatea reprezentând utilizatorul, UserType - clasa aferentă form-ului pentru entitatea User.

Un form poate fi creat și folosit direct în interiorul Controller-ului, dar o recomandare sub formă de "best practice" a framework-ului este ca acesta să fie construit într-o clasă separată, de sine stătătoare, care poate fi refolosită în orice alt loc al aplicației. În linii mari, clasa care conține logica construirii form-ului pentru un User arată în felul următor:

class UserType extends AbstractType
{
 public function buildForm(FormBuilderInterface $builder, array $options)
 {
 $builder
 ->add('email', EmailType::class)
 ->add('username', TextType::class)
 ->add('plainPassword', RepeatedType::class, array(
 'type' => PasswordType::class,
 'first_options' => array('label' => 'Password'),
 'second_options' => array('label' => 'Repeat Password'),
 )
 );
 }

 public function configureOptions(OptionsResolver $resolver)
 {
 $resolver->setDefaults(array(
 'data_class' => 'AppBundle\Entity\User',
 ));
 }

}

Astfel, logica reprezentării și manipulării datelor dintr-un form este efectuată separat de Model și separat de template-ul de reprezentare (de regulă Twig). Form-ul este instanțiat în Controller și trimis mai departe către View, unde este afișat în puține linii de cod, cu ajutorul funcțiilor puse la dispoziție de către Twig (ex: form_row, form_widget, form_label).

{# app/Resources/views/registration/register.html.twig #}

{{ form_start(form) }}
 {{ form_row(form.username) }}
 {{ form_row(form.email) }}
 {{ form_row(form.plainPassword.first) }}
 {{ form_row(form.plainPassword.second) }}

 <button type="submit">Register!</button>
 {{ form_end(form) }}

O întrebare logică care decurge din aspectele menționate mai sus este următoarea: De ce am folosi Form-urile Symfony și nu am rămâne la variantă clasică de a folosi form-urilor pur HTML? Vom vedea în continuare o serie de avantaje puse la dispoziție de către autorii framework-ului, care reprezintă un argument suficient de puternic în această direcție. La fel ca în cazul multor librării, eficacitatea folosirii acestei componente e dovedită în momentul în care dificultatea task-urilor crește și apare nevoia dezvoltării de funcționalități noi care ar implica decizii greu de luat din punct de vedere arhitectural.

 Modificarea dinamică a Form-urilor folosind event-uri 

 De multe ori, un form nu poate fi creat static și apare nevoia ca datele "din spate" să fie modificate dinamic de către developer după anumite nevoi. Folosind form events, putem modifica aceste date la stagii diferite ale întregului workflow: de la popularea inițială a form-ului până la extragerea datelor din request, după submit-ul efectiv. 

 Un prim exemplu: avem un form pentru "User" ca și cel din exemplul anterior. De regulă, form-ul generat din această clasă arată la fel, indiferent dacă un nou User este creat sau dacă unul existent este editat. Să presupunem că proprietatea "username" nu mai poate fi schimbată odată ce user-ul a fost creat și implicit field-ul corespunzător nu mai trebuie afișat. Pentru aceasta, ne putem folosi de sistemul componentei EventDispatcher pentru a analiza componența obiectului, modificând form-ul în funcție de aceasta. Acest lucru se realizează adăugând Event Listeneri clasei de form, delegându-le responsabilitatea creării unui field. Symfony pune la dispoziție o multitudine de event-uri pentru lucrul cu form-uri, cum ar fi: PRE_SET_DATA, POST_SET_DATA, PRE_SUBMIT, SUBMIT și POST_SUBMIT.

Două evenimente sunt declanșate în timpul prepopulării form-ului: PRE_SET_DATA și POST_SET_DATA, iar următoarele trei: PRE_SUBMIT, SUBMIT și POST_SUBMIT sunt declanșate în momentul trimiterii lui. Acest evenimente sunt declanșate de către EventDispatcher și sunt "prinse" cu ajutorul unor Event Listeneri sau Subscriberi (colecție de Listeneri). Un exemplu de Subscriber:

class UserFormSubscriber implements EventSubscriberInterface
{
 public static function getSubscribedEvents()
 {
 return array(FormEvents::PRE_SET_DATA => 'preSetData');
 }

public function preSetData(FormEvent $event)
 {
 $user = $event->getData();
 $form = $event->getForm();

 if (!$user || null === $user->getId()) {
 $form->add('username', TextType::class);
 }
 }
}

De asemenea, un mare avantaj al event-urilor puse la dispoziție de către Symfony este posibilitatea de a modifica datele (entitatea) trimise prin intermediul form-ului chiar după Submit, dând astfel multe posibilități programatorului să intervină asupra componenței datelor într-un mod eficient si izolat.

Data Transformers 

În cadrul unui Form Symfony se diferențiază trei tipuri de date: 

Model Data: formatul folosit în modelul de date al aplicației (ex: un obiect de tip User). Când se apelează Form::getData() sau Form::setData() se operează cu "model" data.

Norm Data: o versiune normalizată a datelor. De regulă, nu e folosită în mod direct.

View Data: formatul folosit pentru a afișa efectiv field-urile formului. De asemenea, este formatul datelor în momentul submit-ului efectiv.

Figura 1. Tipuri de Date în Symfony

Data transformers au următoarele roluri: de a translata datele unui field într-un format care să poată fi afișat în pagină (transform) și efectuarea procesului invers (din form -> entitate: reverse transform). Un simplu exemplu ar fi acțiunea unui user în cadrul unui magazin online care dorește să afle statusul unei comenzi efectuate anterior prin introducerea unui tracking number. Folosind un Data transformer avem posibilitatea de a muta logica respectivă din Controller pe Form efectiv, această acțiune fiind una repetabilă în timp și dependentă de acesta. Așadar, clasa OrderToNumberTransformer va fi responsabilă de convertirea din tracking number în Order și invers.

public function reverseTransform($trackingNumber)
{
if (!$trackingNumber) {
return;
}

$order = $this->manager
->getRepository('AppBundle:Order')
->find($trackingNumber)
;

if (null === $order) {
throw new TransformationFailedException(sprintf(
'An order with number "%s" does not exist!',
$trackingNumber
));
}

return $order;
}

Concluzie

Cu ajutorul unor exemple simple și relevante au fost prezentate părțile de bază ale componentei Symfony Forms, necesare pentru a construi form-uri complexe în cadrul unei aplicații. În cazul proiectelor bazate pe Symfony, trebuie să avem în vedere că scopul principal al unui form este acela de a translata datele dintr-un obiect (ex: User) într-un form HTML pentru a permite user-ului modificarea datelor. Al doilea scop este acela de a colecta datele transmise prin da pentru a fi re-aplicate obiectului. Pe lângă aspectele prezentate, framework-ul permite multe alte funcționalități care își dovedesc utilitatea de-a lungul dezvoltării unui proiect.