Symfony2 Form Architecture
Symfony2 features a brand-new Form component that, to my knowledge, supersedes most existing PHP form libraries in functionality and extensibility (not counting the still lacking, native JavaScript support). It has been in development for two years, even though I was already thinking about it since 2009 and earlier. It is becoming more and more stable recently, with a first completely stable release expected for Symfony 2.2.
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.
This post is not intended to show off the usage or killer features of the Form component. If you are looking for this, you can find examples and explanations in the Form documentation. Neither will this post explain what steps are necessary to use the Form component without Symfony2. This also has been covered, for example in this Gist.
Influences
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.
Key Aspects
The key aspects of the Form component are:
- Abstraction
- Extensibility
- Compositionality
- Separation of Concerns
- Model Binding
- Dynamic Behavior
I will give a short explanation for each of this aspects before I continue to explain how the component's architecture realizes them.
Abstraction
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
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.
Compositionality
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)
- Validation
- 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.
Model Binding
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 interested 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
Dynamic Behavior
Last but not least, forms should support dynamic behavior. Gone are the times were you could statically code your forms on the server and avoid security issues by making sure that every submitted form corresponds to the predefined structure. Nowadays, client-side JavaScripts change the DOM of forms in order to enhance usability.
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.
High-Level Architecture
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 its 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
as far as removing 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.
Low-Level Architecture
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 Form
instances.
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 diagram, a form has three different representations throughout its lifecycle:
- During construction, it is represented by a hierarchy of
FormBuilder
objects. - In the controller, it is represented by a hierarchy of
Form
objects. - In the view, it is represented by a hierarchy of
FormView
objects.
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 FormBuilder
and 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.
Summary
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.
Interested? Explore the code. Play with it. And help us to integrate it into your favourite framework.