Understanding Symfony2 Forms
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:
---------------------------------------------------- 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:
---------------------------------------------------- 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.
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.
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