This post was partially triggered by the release of the new Zend Framework 2 Form RFC because I think that a lot of duplicated effort is going on there. I completely understand that Zend Framework 2 needs a form layer that is tailored to the components delivered by the framework. The purpose of this post is to demonstrate that the Symfony2 Form component is perfectly suited for this requirement. Symfony2-specific functionality can be unplugged, leaving only the raw core dealing with form processing and abstraction. As a replacement, functionality can be developed for supporting Zend’s or any other framework’s components.
Creating a generic form library that elegantly solves all the various use-cases that can be found in web form construction and processing has been a challenging, long-lasting and complex task that is not over yet. Cooperating and continuing development from this common base on seems like a big chance to make form handling in PHP more powerful – and easier – than it has ever been before.
The post starts with crediting all the other great frameworks and form libraries that influenced this work. Then I would like to introduce you to the key aspects of the Form component before continuing to describe its high and low-level architecture.
The Form component has been influenced by many other frameworks written in different languages, including symfony 1, Zend Framework 1, Django, Ruby on Rails, Struts and JSF. Apart from that, it shows many similarities with Formlets, WUI and iData, form libraries written for the functional languages Links, Curry and Clean.
The key aspects of the Form component are:
- Separation of Concerns
- Model Binding
- Dynamic Behavior
Abstraction describes the ability to take any part of a form – or even the whole form – and put it into a reusable data structure. Consider a form with three drop down boxes to select the day, month and year for a date. First, you need code that generates the HTML with all of its option tags. Second, you need code that converts from the application’s data type (for example, PHP’s
DateTime) to the view’s representation (which option is selected?) and back. If you add another date selector to a form in your application, you need to duplicate and adapt all of that code.
Abstraction solves this problem by providing suitable data structures for describing and reusing your code.
Extensibility refers to two main concepts related to abstraction:
- Specialization is a logical consequence of abstraction. When it is possible to abstract functionality into generic data structures, it should also be possible to extend these data structures into custom, specialized ones. A simple example is to extend the above date selector to also show selectors for the time. Without the ability to specialize the existing date selector, a large part of its functionality needs to be rewritten.
- Mixins are an orthogonal concept to specialization. Assume that you want to change all existing fields to include an asterisk (“*”) in their label if they require user input. Doing so by using specialization is a tedious task, because it requires you to extend every existing field with a custom one, implementing the same new functionality. Mixins, on the other hand, allow to attach functionality to existing objects without the need to specialize them. As a bonus, the added functionality is inherited by all descendants in the inheritance tree.
Extensibility also refers to more indepth extensiblity by means of events, which will be discussed later.
If we examine the last examples a bit more, we discover that there is no relevant difference between fields (complex ones, such as in the example before, and primitive ones, such as a text input tag) and forms. Both fields and forms
- accept default values from the model (an array, a date, a string…)
- convert the value to a representation suitable for use in the view
- render HTML
- accept values submitted by the user
- convert these values back to the model’s format
- optionally perform validation
We can implement fields and forms using the same fundamental data structure. By adding compositionality – the ability to nest this data structure into itself (see the Composite pattern) – we can create forms of arbitrary complexity. Instead of forms and fields, we will talk about forms and their children from now on. Once that a form has children, it also needs to
- forward (map) its default value (an array or an object) to its children
- extract (also map) the submitted value of each child back into the original array/object
Separation of Concerns
We can group the tasks in the above list to several, distinct responsibilities:
- Data Transformation
- HTML Generation (the View)
- Data Mapping
These responsibilities should be implemented by decoupled components with clearly defined interfaces. As a result, any of these components can be replaced by a custom implementation, such as a custom view or validator layer.
In many cases, forms directly relate to structures that have already been described otherwise in the domain model. Consider a form to submit the profile information of a user. Consider further that these profiles are stored in a table in your database. The table has information about the properties stored in the profile, about the types of these properties, their default values and their constraints. Ideally, your application also features a class
Profile that is mapped to this database table with an ORM such as Doctrine 2. This class may exhibit more information about the profile, for example, that a profile can be related to any number of subjects that the user is intersted in. These subjects must be selected from a list that is stored in a configuration file.
Usually, the information listed here (we will call it metadata) must be replicated in the form layer. The user must know what properties he can edit, the form must display appropriate HTML widgets that correspond to the types of the properties, the user must know which fields may not be left empty and so on. This is why creating forms usually sucks.
Model Binding tries to change this situation. It refers to two ideas:
- reuse existing metadata during form construction in order to reduce duplication of code and configuration
- read default values from a domain object (an instance of
Profile) and write the submitted values back into the object
Just consider a tabular form. Each column contains fields of the same type, each row represents an object on the server. Little buttons allow to delete or to add new rows. Whenever the form is submitted, the server must adjust the form’s model to match the deleted and added rows in order to successfully process and validate it.
Dynamic behavior shouldn’t be restricted to tabular forms though. Suitable mechanisms in the architecture should allow reactions to any kind of change on the client. Unfortunately, this problem isn’t addressed by many libraries.
Let me outline the high-level architecture of forms in Symfony2. A their core lies the Form component. This component provides the basic architecture for defining and processing forms and uses Symfony2’s Event Dispatcher internally for processing events. On top of the component lie a series of pluggable extensions:
- The Core extension provides all field definitions (called form types) implemented by the framework.
- The Validation extension integrates the Symfony2 Validator to implement form validation.
- The DI extension adds support for Symfony2’s Dependency Injection component.
- The CSRF extension adds CSRF protection to forms.
- The Doctrine 2 extension (shipped with the Doctrine bridge) adds a Doctrine-specific drop down field and provides components that let forms know about Doctrine metadata.
The topmost layer contains the components responsible for rendering HTML. Symfony2 provides two such components: One for rendering forms in Twig (shipped with the Twig bridge) and another for rendering it with it’s PHP Templating component (contained in FrameworkBundle).
The most interesting fact for other frameworks here is that every component apart from Form is replaceable. A custom extension could be written to support Zend Validator, another could be written for Smarty and so on. You could even go so far to remove the Core extension and write an own set of basic fields. Even the underlying Event Dispatcher can be replaced by writing a custom one that implements Symfony2’s EventDispatcherInterface. You win a lot of flexibility compared to little loss.
This section continues to discuss the internal architecture of the Form component. As mentioned before, a form and all of its children can be represented by the same data structure that implements the Composite pattern. In the Form component, this data structure is described by the
FormInterface. The main implementation of
FormInterface is the class
Form, which uses three components to do its work:
- A data mapper distributes the data of a form to its children and merges the data of the children back into the form’s data. The default data mapper allows forms to load their values both from arrays and objects or object graphs. After the form’s submission, the new values are written back into the original data structure.
- Two chains of data transformers convert values between different representations. Data transformers guarantee to output values of predefined types to your application, regardless of the format used to display and modify the values in the view.
- An event dispatcher allows you to execute custom code at predefined points during form processing. It enables you to adapt the form’s structure to match the submitted data, or to filter, modify or validate the submitted data and so on.
These components are passed to the constructor of
Form and cannot be changed after construction in order to avoid corruption of the form’s state. Because the constructor signature is quite long and complicated, a form builder simplifies the construction of
The form view is the view representation of a form. This means that you never deal with
Form instances in the template, but with
FormView instances. These store additional, view-specific inforrmation, such as HTML names, IDs and so on.
The following UML diagram illustrates the architecture.
As can be seen in the previous diagram, a form has three different representations throughout its lifecycle:
- During construction, it is represented by a hierarchy of
- In the controller, it is represented by a hierarchy of
- In the view, it is represented by a hierarchy of
Because the configuration of form builders and form views is repetitive, Symfony2 implements form types that group such configuration. Form types support dynamic inheritance, meaning that they can extend different base types, depending on the options passed at the the construction of a form. The following diagram illustrates all types that come bundled with the Symfony2 extensions (green types are provided by the Core extension, yellow types by additional ones):
Mixins, as described before, are supported in Symfony2 by so-called type extensions. These type extensions can be attached to existing form types and add additional behavior. Symfony2, for example, contains type extensions for adding CSRF protection to the “form” type (and consequently all of its subtypes).
A form factory retrieves the type hierarchy from the loaded extensions and uses them to configure new
FormView objects. It is important to know that this configuration itself can be controlled by user-provided options. For example, the “choice” type supports an option “choices” in which all selectable values need to be passed.
The last important concept in the Form component is that of type guessers. Type guessers try to derive the type and options of a field in the form based on the metadata available for the domain object backing the form (if any). For example, if a property of the object is configured to be a one-to-many-relation to a model
Tag, type guessers automatically configure this property to be represented by a multiple-choice field with all
Tag instances loaded by default. This concept is similar to ModelForms in Django. The main difference is that your application can use various type guessers to use metadata from different sources instead of just relying on the ORM definition. Symfony2, for example, ships with three guessers: One for reading Doctrine2 metadata, one for Propel metadata and a last one for reading metadata of the Symfony2 validator.
The concepts described in the last paragraphs are summarized again in the following UML diagram.
As I have tried to show in this post, the Symfony2 Form component features a carefully engineered architecture that takes many important aspects of modern form processing into account.
It solves the problem of abstraction, specialization and mixins by providing a dynamic inheritance tree of form types and form type extensions. It solves the compositionality problem by distributing the work and responsibility of processing a form among all of its elements. It offers a clear separation of concerns in order to easily replace different layers of the component. It achieves model binding by involving the existing domain model metadata into the construction of a form and by reading from and writing into domain objects directly. And it supports dynamic behavior by offering events at predefined points during its processing that can be handled by custom listeners, such as for validation or filtering.