Symfony 2 has a completely new
component for working with forms , which, as far as I know, will easily replace most of these libraries for PHP both in functionality and, if possible, in extending it (of course, if you do not take into account small flaws when working with JavaScript . The development of this component took more than two years, although I started thinking about it
somewhere else in 2009 or even earlier. With each new version, this component becomes more and more stable, and a completely stable version is expected with the release of Symfony 2.2.
This post is dedicated to the release of Zend Framework 2 Form RFC, since it seems to me that its developers, in fact, have done a lot of what has already been done by us. Of course, it is clear to everyone that Zend Framework 2 should have an interlayer for working with forms, which fully takes into account the features of the components supplied with the framework. The goal of this post is to attempt to show that Symfony2 Forms fits these requirements perfectly. The functionality inherent in Symfony2 can be easily removed: the code for processing forms and all levels of abstractions are completely independent. Attaching the same support for the features of the Zend components is also not difficult.
')
Creating a generic library for working with forms, which covers all possible use cases arising during development, was not an easy test, a long and complicated affair, which, moreover, has not yet been completed. Collaboration and continued collaborative development should help achieve more flexible and simple form management from PHP.
We begin this post, perhaps, with thanks to all those who participated in the development of frameworks and libraries for processing forms that influenced our work. Then, before describing the architecture itself, I would like to familiarize you with the key aspects of designing a convenient and flexible component for working with forms.
In this post you will not find examples of the use of "killer features" of this component, for this there is documentation . Also, you will not find here any information on its use, so to speak, out of context, regardless of the framework. Such information should be described, for example, in Gist .
Influence
The form component has been influenced by a variety of frameworks written in various languages, including
symfony 1 ,
Zend Framework 1 ,
Django ,
Ruby on Rails ,
Struts and
JSF . In addition, it contains similarities with
Formlets ,
WUI, and
iData , libraries for working with forms written for functional programming languages such as Links, Curry, and Clean.
Key aspects
Key aspects in designing a component for working with forms are:
- Increased level of abstraction
- Extensibility
- Composition
- Separation of tasks
- Model binding
- Dynamic behaviors
Let's briefly go through each of these aspects before discussing how the implemented architecture covers them.
Increased level of abstraction
Raising the level of abstraction allows you to take any part of the form — even the entire form — and place it in a data structure that you can then use again. Consider a date selection form in which there are three drop-down lists for selection by day, month, and year. First you need a code that will generate HTML with all the options drop-down lists. Then, you need code that converts data from the format of your application (for example, a
DateTime object if we are talking about PHP) into a format suitable for our presentation (in order to mark the selected value of the lists). Of course, you will need the reverse procedure. Now imagine a situation where you have to add another drop-down list for some additional data. In this case, you will have to duplicate all the code for this list and adapt it to the new requirements.
Introducing abstraction solves this problem by providing suitable structures for describing and reusing your code.
Extensibility
Extensibility is based on two concepts related to abstraction:
- Specialization is a logical sequence of abstractions. If it is possible to abstract the functional into generalized data structures, then they can also be expanded into more specialized structures. For example, we can extend the above example with the selection of a date by adding fields for choosing the time. If it were not possible to expand the functionality of the existing field to select a date, then we would have to rewrite a substantial part of the functionality.
- Impurities are the opposite of specialization. For example, you wanted to change all existing fields so that there would be an asterisk ("*") in their descriptions, which would show that these fields are required. Using a specialization approach for this is somewhat impractical, since you will have to expand all the available fields , implementing the same functionality. Impurities allow you to connect functionality to existing objects without having to redefine them. In addition, the functionality added in this way will be inherited for all child objects in the inheritance tree.
Extensibility also refers to a more in-depth expansion of the functional, with the help of events. This we will consider later.
Linker
If we analyze the last example, we can notice that there is no particular difference between the
fields (
composite fields , such as in the considered example, or
primitive ones , such as text input field) and the
forms are not present. And fields and forms
- Accept default values from models (from arrays, from dates, from strings ...)
- Transform values so that it can be used in a view.
- Generate HTML
- Accept user input
- Convert value back to model format.
- Can validate data
We can implement fields and forms using the same fundamental data structures. By adding a linker, a tool that allows you to add nested structures (see the Linker design pattern), we can create a form of any complexity. Instead of delimiting the fields and forms, let's talk about the
forms and their constituent parts . As soon as a form has a descendant, it must also
- pass default values (array or object) to its descendants
- transfer the input values of each child back to the array or object
Separation of tasks
We can divide the tasks described above into several specific groups:
- Data conversion
- HTML generation (presentation)
- Validation
- Data binding
Each of these groups of tasks must be implemented by a separate component with an explicitly defined interface. This will allow you to replace any of these components with your own: for example, replace the view processor, or, for example, the validation layer.
Model binding
In most cases, forms are directly tied to structures, one way or another, described in
the domain model . Consider the example of sending user profile information. Suppose that all user information is stored in a separate table in your database. The table stores information about the properties that the profile contains, which
data type is used for each of the
properties , their
default values and
data entry restrictions . Ideally, your application should also contain the
Profile class with all the necessary
properties , linked to a table in the database, for example, by means of the
Doctrine2 ORM . This class may contain more information about the profile than is stored in the table, for example, if a profile contains a set of items of interest to the user. For simplicity, we will take the list of these items from the configuration file.
Usually, information describing an object (let's call it
metadata ) should be duplicated in a layer of forms. The developer has to make sure that the fields have not changed, which of them can be edited, make sure that the form displays the corresponding HTML widgets, that some of the fields could not remain empty, and so on.
It is because of these things that the creation of forms becomes a rather ungrateful thing.Binding models designed to make a difference. It is based on two ideas:
- Use existing metadata during form creation in order to reduce the amount of duplicate code and / or settings.
The default values should be obtained from the domain model (in our case it will be an instance of the
Profile class) and
the input data should also be recorded in our model.
Dynamic behaviors
Last but not least, these are dynamic behaviors for forms. Gone are the days when the code for the form was fully implemented on the server, where a check on the security of the data entered by the user was carried out under rigidly defined structures. Today it is fashionable to use JavaScript to change forms on the client side to improve the usability of the system.
Imagine a form in the form of a table. Each column has fields of the same type, each row is an object on the server. The form has buttons that allow you to add or delete rows. With all this, the possibility of validation of forms should remain, they must be successfully processed on the server.
Dynamic behaviors were invented to implement such a form. Moreover, their use is not limited to relatively simple tabular forms. You can create a form that responds to any changes in its structure made on the client side. Unfortunately, this problem is not solved in most libraries, despite the demand for such mechanisms.
High level architecture
Let me provide a high-level description of the layout architecture in Symfony2, designed to work with forms. Its cornerstone is the Form component. This component implements the basis for building and processing forms, as well as using the Symfony2
event dispatcher to handle internal events. Over this component is a layer of plug-in extensions:
- Kernel-level extensions provide descriptions of the structure of fields (let's call it the types of forms ( FormType )) implemented in the framework.
- Validation extension is needed to validate user input inside forms.
- The DI extension allows you to use a dependency container .
- The CSRF extension, as the name implies, adds a CSRF protection for the form.
- Doctrine2 extensions (included with the Doctrine Bridge ) add Doctrine-specific drop-down lists and components that let you get object metadata for use in forms.
The top level contains components that implement the formation of HTML-i. By default, Symfony2 has two such components: one for rendering forms through
Twig (supplied in the
Twig bridge ) and the other for rendering through the
good old PHP (supplied with
FrameworkBundle ).
Probably the most interesting feature of this architecture is that any of the components can be replaced. You can write an extension so that the forms use the validation component from Zend. You can also write a view component that implements rendering through Smarty or any other template engine. You can even remove the kernel extension and write your basic form types for simple fields. Even the underlying event dispatcher can be replaced with your component that implements
the EventDispatcherInterface interface . This gives a good advantage in flexibility, despite some inconveniences.
Low-level architecture
This section discusses the internal architecture of the form component. As mentioned above, the form and all its parts are represented in the form of the same data structure that implements the “Linker” design pattern. In the Form component, this structure is described by the
FormInterface interface. The main class that implements this interface is the
Form class, which uses three components, with the help of which the whole thing works:
- The data mapper distributes the form data to its child elements. In addition, it is the responsibility of this component to collect data from child elements back to the form data.
- Two chains of data converters , as the name implies, convert the values for their different representations. Data transformers ensure that your application receives a value in a strictly defined format, regardless of the format used to represent it.
- Event Manager allows you to run specific code at predetermined stages of form processing. This approach allows you to adapt the structure of the form under the incoming data, or to filter out these same incoming data, to validate or change them.
All these components are passed to the
Form object constructor and cannot be changed after the form is created, as this may lead to undesirable changes in the form state. Since the interface of the designer of the
Form object is quite complex, the FormBuilder was implemented, which simplifies the creation of instances of these objects.
A form view is a data structure that describes a view. That is, instead of calling the Form class directly in templates, you will work with an instance of the
FormView class. This object stores additional information that is specific only for presentation, such as
name attributes for HTML elements of forms, their IDs, etc.
The UML diagram below shows the essence of the architecture.

As you can see from this diagram, the life cycle of a form consists of three different views:
- When created, the form is represented as a hierarchy of FormBuilder objects .
- In the controller, the form is represented by a hierarchy of Form objects.
- At the view level, it is represented by a hierarchy of FormView objects .
Since configuring form constructors their presentation is quite tedious, Symfony2 implements the basic types of forms that already implement the basic settings. Form types support dynamic inheritance. This means that you can extend various base types based on the options passed to the form constructor. The following diagram shows all types supplied with Symfony2 (types marked in green come with the kernel, while yellow types come with additional bundles):

Impurities, which we mentioned earlier, are implemented in SYmfony2 as so-called type extensions. These extensions can be connected to existing types of forms and add some behavior to them. In Symfony2, for example, there are extensions to add CSRF protection, applied to the type “form” (and as a result, all types that inherit this type).
The form factory retrieves the type hierarchy from the loaded extensions, and uses it to customize the
FormBuilder and
FormView objects . It is important to note that this stage can be controlled. For example, the type "
choice " allows you to specify in the settings attribute "
choices ", which contains all the values for the options that should be displayed.
There is another rather important concept of the Form component -
type predictors . These "predictors" are trying to predict the type and options of the form field, based on the metadata taken from the object of the domain of definition to which the form is attached (if, of course, it is fixed). For example, if any of the properties of our object contains a one-to-many relationship between the Tag object, type predictors will automatically configure this form field for this property so that it is a selection field, with the ability to select multiple values, as well as will use all instances of Tag objects as options for this field. A similar concept is used in
ModelForms in the Django framework. True, there is one rather large difference: type predictors use metadata provided not only by ORM, but also all object metadata. Symfony2 comes with three type predictors: one uses metadata provided by Doctrine2, the other is provided by Propel, and another uses validation rules.
The concepts described above can be described with the following UML diagram.

Conclusion
What I wanted to show in this post is that the Symfony2 Form component has a well-designed architecture that covers many important aspects of form processing.
This architecture solves the
problems of insufficient levels of abstractions ,
specialization and
impurities , providing a dynamic inheritance tree of types of forms and their extensions. It solves the problem of
composition , distributing the tasks of processing the form to all its elements. It also provides a clear
separation of tasks between components, so you can replace them with your own. Using the metadata of the domain objects when creating a form, the described architecture implements
data binding to these models, which allows reading and writing data directly. We also paid attention to
dynamic behaviors , providing the ability to track certain events that occur during form processing using our handlers, for example, to validate or filter data.
Interested?
Learn the code .
Experiment with it . And help us integrate it into your favorite framework.
From the translator: translations of this article exist, but they seemed to me not to be of very high quality. Yes, and I think it does not hurt. There are inaccuracies in the translation, therefore I will be grateful if you point them out in the LAN. Thanks for attention.