TSM - Data Management with Symfony Forms

Sergiu Stupariu - PHP Developer @Pentalog

From the development of simple CRUD applications to the development of more complex ones: websites or HTML form management, developers find this type of work to be the most frequent and most challenging of tasks.

In addition to numerous modules and functionalities, the PHP Symfony framework provides a specialized component for forms, whose purpose is to ease the entire process of storage for entry data and that of data manipulation. This component can also be used outside Symfony projects, since it is a standalone library, easy to integrate, specially designed for applications endowed with an architecture built according to the separation of concerns principle, generally referred to as Model-View-Controller. We will further see how, with the help of this approach, the forms are represented as classes, which are tightly connected to Model entities.

The simplest example would be the registration of a new user onto a website. To this end, two classes will be defined: User - the entity representing the user, UserType - the class which corresponds to the form associated to the User entity.

A form can be created and used directly in the Controller. However, a "best practice" recommendation for how the framework should be properly used is for it to be built in a separate, standalone class which can be reused across the application. Broadly speaking, the class, which contains the logic behind how the form is built for a User, looks like this:

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',
 ));
 }

}

Therefore, the logic, which lies at the basis of representing and manipulating the data in a form, is performed separately from the Model and separately from the representation template (Twig, in general). The form is instantiated in the Controller and it is sent forward to View, where it is displayed in a small number of code lines, with the help of the functions made available by Twig (e.g.: 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) }}

 
 {{ form_end(form) }}

A logical follow-up question would be: Why should we use Symfony Forms? Why shouldn't we stick to the classic HTML way of using forms? We will elaborate on the advantages the framework authors present, an argument which is strong enough to persuade us. As in the case of many libraries, the efficiency of using this component proves its worth when the task difficulty increases and there is a need to develop new functionality, which would entail decisions which are hard to make, from an architecture point of view.

The dynamic change of Forms using events

Many times, a form cannot be created statically. At a point, there will be a need for the "backend" data to be dynamically modified by the developer according to certain needs. By using form events, we can modify this data at various stages of the data workflow: the initial moment the form is populated, the moment of data extraction, and the submit phase proper.

Here is the first example: we have a "User" form, similar to the one in the previous example. As a rule, the form, which is generated from this class, looks the same irrespective of whether a new User is created or an existing one is edited. Let us assume that the "username" property cannot be changed once the user was created and, implicitly, the corresponding field does not have to be displayed any longer. To do this, we can use the EventDispatcher component to analyze the object, by modifying the form according to the component. This is done by adding Event Listeners to the form class, and by dispatching onto them the responsibility of creating a field. Symfony provides many available events in order to work with forms. These include: PRE_SET_DATA, POST_SET_DATA, PRE_SUBMIT, SUBMIT and POST_SUBMIT.

Two events are triggered during the form pre-population process: PRE_SET_DATA and POST_SET_DATA, while the next three events: PRE_SUBMIT, SUBMIT and POST_SUBMIT are triggered upon the dispatch proper. These events are triggered by EventDispatcher and are "caught" or identified with the help of Event Listeners or Subscribers (a collection of Listenera). Here is one example of 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);
 }
 }
}

Moreover, a great advantage of the events made available by Symfony is the possibility of modifying the data (the entity) sent via forms, right after Submit, thus giving the developers many possibilities of intervening in the data component efficiently and in an isolated manner.

Data Transformers

Within a Symfony Form, there are three types of data:

Figure 1. Data Types in Symfony

Data transformers have the following purposes: translating the data from one field to a format that can be displayed on a sheet (transform) and enabling the reversed process (from the form -> towards the entity: reverse transform). A simple example, in this respect, would be the activity a user performs in an online shop when they want to find out the status of an order they previously placed, by entering a tracking number. By using a Data transformer, we have the possibility of moving/switching the respective logic from the Controller onto the Form proper, this being a co-dependent action that can be repeated over time. Therefore, the OrderToNumberTransformer class will be in charge of converting the tracking number to Order and vice versa.

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;
}

Conclusion

With the help of simple, relevant examples, we presented the basic components of Symfony Forms, necessary to build complex forms within an application. In the case of the projects based on Symfony, we must consider that the main purpose of a form is that of translating data from an object (e.g. User) into an HTML form, to allow the user to change the data. The second goal is to collect the data transmitted via submit, so that they may be reapplied to the object. In addition to the aspects so far presented, the framework allows for many other functionalities which prove their worth throughout the development cycle of the project.