Understanding Symfony2 Forms

Posted on 11 Sep 2015
Tagged with: [ forms ]  [ framework ]  [ PHP ]  [ rainbow ]  [ symfony2

To actually use Symfony2 forms, all you need to do is read some documentation, a few blog posts and you’ll be up and running in a couple of minutes. Understanding Symfony2 forms however, is a whole different ballgame. In order to understand a seemingly simple process of “adding fields to a form”, we must understand a lot of the basic foundation of the Symfony2 Form component. In these blog posts, I’ll try and give some more insights on this foundation.

Understanding Symfony2 Forms

A lot of questions I get about the Symfony2 Form component, or questions that are asked on sites like Stack Overflow are most often based due to the lack of understanding how the Form component actually works. And this is not surprising, as the Form component isn’t really the most accessible component to begin with. The main reason for this is probably because (web) forms come in so many shapes and sizes, and having a generic form component that is able to handle this, requires a complex architecture. Could the Symfony2 Form component be architected in a more simplistic setup: sure! But it would lose a lot of its power, and you wouldn’t able to create such flexible forms as you can now, making the whole component less usable.

The Symfony2 Form component in essence has three main functions:

  • Building forms.
  • Populating and validating data.
  • Rendering forms.

Building forms

Building forms is most likely the hardest part of the whole component to understand. It is here where forms are designed. Sometimes easy ones, for instance a login form with just a name and password field, and sometimes very complex ones, like a dynamic ajax powered wizard form where each step depends on previously submitted data.

This building process is fairly simple in its usage for a regular developer: you create a builder class, add your fields to it, maybe configure some options here and there, and you’re done. But the actual build process that lies behind it, is much more complicated than you would imagine. And it’s also a very confusing process as well, where some classes behave differently based on their context. Often when using forms, it’s hard to figure out why sometimes seemingly simple operations like adding a new field to a form is not allowed in certain points in time. This happens especially a lot when trying to create dynamic forms. When we dive into the foundations of the form building process, we will find out why these things happen, but before we do that, we will grasp some of the basic ideas behind the Form component.

Populating and validating data

The second function of the Form component is to populate and validate incoming data. There are two ways for data to enter a form: there is initial data that is set during the creation/building of the form. For instance, when you want to edit a user, the form would already contain all the information for that particular user so it can display this on the page.

The second way for data to enter the form is when a user submitted this data, which is normally triggered by a HTTP POST request. Once a form is submitted, this data must be validated by the form: Is the data submitted by the user actual form data? Is that data correct? Are all required fields present? Do we need to sanitize/filter data etc. A lot of this is done automatically through the help of some additional components, like the validation and the property accessor components.

Data population is in itself a pretty complex matter. Data can come in many different sizes and shapes, and - just like everything else - populating forms with data is done through external classes called DataTransformers. They allow you to convert things like ORM entities, (JSON) strings, arrays etc into form data. Also, Symfony2 forms actually uses internally three different representations for the data, which by itself is a very interesting (and sometimes confusing) subject as well, and one we surely will be exploring later on.

Rendering forms

The last function of the Form component is the rendering of forms. But the component doesn’t do the actual rendering itself. At least, it does not know about <form>, <input>, <button> HTML tags. In fact, it does not tie into HTML as the actual output.

When rendering a Symfony2 form, it will actually render not directly a form, but a representation of the form which only contains (template) variables. This representation is the so-called FormView class. This is why for instance, when you send a form to a Twig template, you must do this with the $form->createView() method. It creates the representation of the current form so it can be used by template renderers.

This architecture has a few advantages:

  • A form isn’t tied to HTML.
  • A form isn’t tied to a particular template engine.
  • A render doesn’t know (or need to know) anything about the Form component itself.

Even though forms mostly are used for generating HTML forms, it would be possible (and not even that hard), to output different formats, like generating forms for PDF files, or output it as XML or WSDL. As soon as we have our FormView object, we can have any rendering engine (Twig, Smarty, even PHP itself), to render our form data based on this data. If we would have a render engine that could generate PDF output (like FPDF or TCPDF and probably others too), we could with just the FormView data easily generate our custom PDF forms.

But even though the Form component itself is template agnostic, it does come with rendering functionality in the form of the FormRenderer class. This class primarily takes care of finding the correct form templates to render, and does some additional work in rendering housekeeping and caching of data to make the render process faster. Just remember that the Form component by itself has absolutely no knowledge of anything HTML: there are not form tags to generate, there are no input boxes or select and radio boxes. All the renderer do is figuring out what “block” it needs to render, and calls the actual templating engine to do this rendering. How the component knows which blocks to render, we will discuss later on in another blog post.

The Form basics

To understand the Form component and how the whole process of building forms works, we must have some basic understanding of some of its internal components.

What forms are made out of

To understand what forms actually are, we can use a reductionistic approach and see what forms are made out of. You would expect that a form is nothing more than a container that contains different form fields like text, email, textarea and select boxes for instance, but this is not completely true when it comes to Symfony2 forms.

Instead, in Symfony2, a form consists of nothing more than other forms. Each “form element” (ie: an input field for your first name, or a select box to select a country), is in itself a form as well. This is how we can with easy add subforms to our forms: since everything is a form, adding a subform or a simple text field are literally the same thing.

To create forms, we use small building blocks are called form types and these blocks are the foundation of the Form component. A form type is not directly the same thing as a form, but a form type knows how a form can be created. When you build your own form, you are in fact creating your own custom form type. This is why you often see that forms in Symfony2 applications are actually called like CustomerFormType or commentFormType. Saying that you create a form in Symfony is not technically correct: you are creating a form type. And this form type will be used as a blue print by the Form component to create the actual form class later on.

Ultimately, a form type can be seen as a collection of other form types, where each form type can have zero or more children. Some of these form types will not contain other form types, for instance, the textType, which represents an text input field does not contain any other form types, and we call these types a single form types (but there is nothing really special about them, just that they cannot contain children). Other form types, like the dateType often do have child form types: for instance, a select box for the day, one for the month, and one for the year. These select boxes are nothing more than other form types (single form types actually, as they do not contain any other form types themselves).

Form types that contain other form types are called compound form types. So a dateType form type is a compound form type that contains three single form types.

With form types, we can create a tree-like data structure of all kind of different form types. A simple form that consists of form types could look something like this:

At the root of our form tree, we actually see that there is another form type, the so-called formType (yes, it’s a form type called formType, which can be confusing). We’ll see later that this type is actually nothing special, but merely a simple compound form type to act as a base to add other form types to.

The form type

So a form type is a basic building block within the Form component. Even though you might think so, they do not simply represent the different possible (HTML) form fields. They represent much more: they can be seen as the “DNA” of forms, so the Form component can use these form type to not only create an actual form, but also to figure out how to render the form type and which options are available for that form type.

So a form type itself has a few functions:
- They are used to build an actual form.
- They are used to create a formView class for rendering.
- They can be configured with settings.

All form type classes implement the FormTypeInterface, and the following methods:

public function buildForm(FormBuilderInterface $builder, array $options);
This method can be used to make changes in the $builder entity that is passed. This builder class, as we will discus in a later blogpost, forms the blue print of your form, and each form type that you have added, can make changes.

This doesn’t always has to happen: some more simplistic form types like the textType or emailType, do not do change or add anything. But others, like the dateType can add additional form types (like the day, month and year form types we discussed earlier), but it could also change behavior by adding event handlers and data transformers.

Since options are passed to this buildForm() method as well, we can actually use them do make the building process more dynamic. For instance, it is possible to configure the dateType to not use three select boxes, but a single text input field (in HTML5 this would automatically become a date picker in your browser). If the option widget is set to single_text, the buildForm() method of dateType it will not add the three additional form types.

To see this in action, take a look at the following code snippets and their results:

$form = $formFactory->createBuilder()
    ->add('name', 'text')
    ->add('arrival_dt', 'date') 
    ->getForm();
----------------------------------------------------
Name: form                Type: form
    Name: name                Type: text
    Name: arrival_dt          Type: date
        Name: year                Type: choice
        Name: month               Type: choice
        Name: day                 Type: choice
----------------------------------------------------

You see that we even don’t explicitly add the year, month and day choice fields in our arrival_dt field. This is done automatically by the buildForm() method of the dateType. Now, let’s change the widget option when adding our form type:

 $form = $formFactory->createBuilder()
    ->add('name', 'text')
    ->add('arrival_dt', 'date', array('widget' => 'single_text'))
    ->getForm();
----------------------------------------------------
Name: form                Type: form
    Name: name                Type: text
    Name: arrival_dt          Type: date
----------------------------------------------------

Here, the arrival_dt does not have the three separate fields, as we have configured our arrival_dt type with the option widget set to single_text. This triggers the buildForm() method of the dateType to not add our additional fields.

Note that the DateType is actually one of the most complex types available, as it also deals with a very flexible way of adding data to it. But we talk about this later.

public function buildView(FormView $view, FormInterface $form, array $options);

This method is called when building a formView instance. It allows the form type to add or change template variables that are send to the renderer. It can be used for specific settings, for instance with the timeType form type. This form type can be configured to display the time as a simple text field with 00:00:00 format, as a set of three drop down boxes, or as a HTML5 enabled input field. And even though the buildView() method does not do the actual rendering, it will set variables inside the form view that allows the renderer to do so.

public function finishView(FormView $view, FormInterface $form, array $options);

The finishView() method is similar to the buildView() method, except that this is called after the form views of the children of a form type has been build. This is of no consequences for simple form types like text or email, but it does so with compound form types. Inside the buildView() method, the children of a compound form type are not yet converted into a formView class, but inside the finishView() method, the formView objects of the children are available.

public function setDefaultOptions(OptionsResolverInterface $resolver);
public function configureOptions(OptionsResolver $resolver);

The setDefaultoptions allows you to set, unset or configure options that we can specify for a form type. We already saw this for the widget option in the dateType, but for instance, the NumberType form type allows you to set options like precision, scale, grouping and rounding, that allows you to set the behavior of a number inside the element.

If you have special options for your form type, which you want to use in the buildForm(), buildView() or finishView() methods, you can define them here. All options are set through Symfony2’s OptionResolver component, which allows you to easily work with options (including setting default values, mandatory values, filtering etc).

Since Symfony v2.6, the setDefaultOptions() method is deprecated in favor for the configureOptions() method. From Symfony 3.0 onwards, the setDefaultOptions() will be removed and only configureOptions() can be used.

public function getParent();

A form type can extend other form types through PHP’s inheritance model (like class MyType extends ParentType), however, the Form component uses its own inheritance model that is preferred over extending through OO.

The getParent() method returns the name of the form type that this form type extends. Almost all form types will extend other form types, most notable the formType. We will discuss the inheritance model of Form later in this blogpost as well.

public function getName();

This method returns the name of the type. For a textType, this would be ‘text’, for the birthDayType, it’s ‘birthday’ etc.

Starting from Symfony v2.8, this method is deprecated. The reason for this is because the name of the form is already available form the class name, and we use the name mostly for referencing form types. This is why since v2.8, we reference types not by name, but by class which can be done like: textType::class instead of text. It seems strange, but it will be easy enough to change and work with.

The Form inheritance and extension model

Even though the Form component uses the form type “DNA” blocks to create forms, by itself it is not enough. Form types uses a manual inheritance model through the help of the getParent() method. So every time a form type needs to call its methods, it must not only call the methods of the actual form type, but also the method of the form type’s parent. And the method of that types parent and so on.

For instance, the emailType form type uses the textType as its parent. The textType in turn uses the formType as the parent, and the formType does not specify any parents (the getParent() method of the formType return empty).

So the inheritance structure of the emailType is emailType -> textType -> formType. For the curious ones, all types actually do extend other types in the classic PHP’s OO model (namely the AbstractType), but this abstract class is mostly to remove all the boilerplate code, not so much for real logic.

As a common role: do not mix the form’s inheritance model and PHP’s OO model. I would recommend using the form’s inheritance model, as it will do more (as we see soon enough), rather than using PHP’s OO. If you end up using both, you might run into some serious issues like duplicated calls to parent methods for instance. Those can be a nightmare to debug!

Extending types

But form types do not only use an inheritance system, they also use an extension system. For each form type you can register so-called type extensions. With these type extensions you can modify the form type in a more dynamic way. For instance, suppose you want to change all your text input fields so they automatically add a new CSS class to the template variables (the actual class name is dynamic, and depends on something external). We could add this directly to the templates themselves, but instead, we could also create an simple type extension on the textType form type, that will add these classes whenever the form’s buildView() method is called.

And because it’s more dynamic, we can simply opt to not add the extension when it’s not needed, or only in certain conditions (for instance, when a user is a premium user, or when a user has certain rights etc). These type extensions provide a very flexible way of changing behavior without the direct need for creating new form types.

Calling our form type methods

So now a simple form type call to buildForm(), buildView() etc is getting very complex: we need to take the inheritance model into account, but each form type can also have type extensions which must be called (and to make matters worse: those parent form types can have extensions as well).

So a form type by itself isn’t enough to work with. Instead, we need something more robust that deals with this inheritance and extension functionality. Here is where the ResolvedFormType comes into play.

The ResolvedFormType

Internally, the Symfony2 Form component doesn’t directly use form types, but actually uses something called “resolved form types”. A resolved form type is nothing more than a wrapper around a regular (let’s call them unresolved form types from now on) form type, which adds all the inheritance and extension logic to it.

A resolved form type contains many of the methods from an unresolved form type, like buildForm(), buildView(), finishView() etc. When we call buildView() from a resolved form type, it automatically calls all the buildView() methods from all the inherited parents, and all extensions in the correct order.

So every time an unresolved form type enters the form component (this happens on only a few occasions, like when adding them to a form builder for instance), the form component will actually convert this unresolved form type into a new class: the resolvedFormType. There is only one such class, as all form types will be converted to this class.

003-2

Note that not all methods from unresolved form types uses the inheritance model. Some of them, for instance, the createBuilder() and createView() method doesn’t, as they are used for creating a specific builder and/or view for that type (but they do inherit options through the parent types and type extensions).

Now that we know about form types, in both resolved and unresolved forms, and know about how forms are build up, and that we have formView that are representations of form, it’s time to figure out how everything will come together. IN the next post, we will talk about how a form builder will take all these form types and convert them into an actual form. Later on, we might talk about how data is entered and submitted. But for now, I hope you have at least a better understanding on what form types are.


Did you find this blogpost interesting?

The Symfony2 Form Recipe book contains a set of recipes to solve common problems when dealing with Symfony forms. It's available for purchase for just $7.95 as PDF, MOBI or EPUB on LeanPub: https://leanpub.com/symfonyframeworkrecipes-forms