Symfony Forms, Entities, and Validation
In a previous post, I discussed some of the design patterns behind Symfony 2’s Form component that make it so fast and stable. As requested by Luis Cordova from Craft it Online, I have done further research into how Symfony 2 deals with forms, validation, and entities. Much of the information in this post can be learned from the online documentation, and I highly recommend reading it.
Forms are created through a Factory service (
form.factory), which subsequently uses a Builder object, to automate the creation of fields and assembly of the
Form Composite tree. The best way to illustrate and explain this process is to walk through an example usage scenario.
Controller class (
Symfony\<wbr />Bundle\<wbr />FrameworkBundle\<wbr />Controller\<wbr />Controller) defines the
createForm method shortcut, which accepts the following parameters: a string identifier or an instance of a
Type class, an entity to which the form will bind its information, and an array of options to pass to the
buildForm() function. The last two parameters, the entity and the options array, are optional. If no entity is provided, then the data can be retrieved by calling
$form->getData() after binding.
Controller::createForm() delegates to
FormFactory acts as a Façade to the Form component by automating and abstracting much of the creation process.
FormFactory::<wbr />create() then delegates to a
FormBuilder object, which creates the form recursively for each of the nested
Type objects. Afterwards, the
FormFactory returns the completed Form tree by calling
This is the definition of a
Type class that will control how a node on the
Form tree will be constructed by the builder. Note that Symfony 2 discourages users from directly defining
Form subclasses: using the builder reduces dependency on the
Form’s interface and allows its internal structure to vary as Symfony evolves without affecting the user’s code.
This function is a member of the
MyType class and is the callback for when a node of type MyType is created. The form factory passes a builder to this callback so this node’s children can be created (even recursively, if necessary by calling
$fieldOrSubForm->buildForm($bldr, array(...))). The options are passed from the parent node (or the constructor if this particular
Type object was instantiated) and can be used to dynamically configure form elements.
This statement would reside in the definition of
MyType::buildForm() and gives me the opportunity to explain how the builder works. The builder’s
add function creates a child on the current Form node with the given arguments: name, type, options.
The name acts as an identifier for the child (and the input field’s name attribute when rendered as HTML), much like the key to a PHP array value.
The type is optional and can be either a string or an instance of Form (or any of its subclasses, including Type objects). If omitted, the builder can guess the type using doctrine metadata. If a string is given for the type, the builder uses it to look up the type class in a registry of built-in Type classes (all of which are defined in
Extension\CoreType). I cannot determine whether the registry of built-in types follows the Flyweight pattern or the Prototype pattern (or a combination of both), but that’s not important now. If a Type instance is given, the builder can skip the lookup step and process it immediately. This is how one can embed a form within a form in Symfony 2.
The options array will be used for configuration in the child node’s
buildForm()method. Note that the ‘required’ option in the code example above is NOT a part of the validation model. Rather, it is an HTML5 attribute that is added to the rendered field for browser-based, client-side validation. This is not meant to be used in place of server-side validation as the ‘required’ attribute is not supported by all browsers.
When the builder creates a child node, it sets the new node’s parent reference to the current node that called for the child’s construction. The builder is actually a lot more complicated than how I have explained it here: it uses lazy loading so that it does not create any child objects until it absolutely has to. Calling
$builder->getForm() triggers this mechanism. The resulting
Form object will allow for data binding and validation and acts as a wrapper for the data object underneath.
Form object only serves as a wrapper to an entity, array, or other data form, it cannot be rendered into HTML as-is. To resolve this, the controller passes an instance of
FormView to the templating engine at render-time. A
FormView is a PHP abstraction of the HTML input tags and provides a way for the templating engine to iterate over the fields in the form.
In summary, the
FormFactory is the main interface for the Form component; the
FormBuilder is responsible for adding fields (Types) to the Form object being built; the
Form is a lightweight composite wrapper for the data object that stores the information, and the
FormView encapsulates information pertinent to rendering HTML input fields.
Dynamic web applications depend heavily on storing and retrieving data. Symfony has altered the traditional norm in favor of Domain Driven Design.
Entities are nothing more than plain-old PHP classes that mimic the properties and behavior of real-life objects. They do not extend any base Entity class and are thus entirely self-contained. This is another reason Symfony is so fast and lightweight. Entities help the Symfony framework feel more like working with a statically typed object-oriented language like Java or C++.
Entities are persisted by the Doctrine ORM. The ORM understands how to communicate with the entities because of the metadata information associated with the fields to be saved and by conventional getter and setter names. I prefer using annotations because I can define mapping and validation metadata for each field in the same file. I am also amused by the fact that Doctrine (and other Symfony components) can read and cache the PHPdoc comments that are ignored by the PHP interpreter. Symfony also provides ways to configure entity metadata using YAML, XML, or PHP files.
Validation, the act of checking data legitimacy, has traditionally been closely integrated with forms: when a user submits data, a validation script checks that all required fields have been filled, certain numeric or date values are within a predetermined range, and email addresses are correctly formatted. Validation also applies on a coding level: a RESTful web service could submit information directly into the controller without the use of an HTML form; unit tests populate entities directly through setter methods.
Validation must be made available to both Forms and Entities. Symfony 2 solves this problem by extracting the validation subsystem into its own Validator component. According to the Symfony 2 online documentation, “This component is based on the JSR303 Bean Validation specification.”
Like Doctrine, the Validator component knows how to validate an object based on metadata, which can be defined on the Entity using Annotations or in separate YAML, XML, or PHP configuration files. Regardless of how the metadata is defined, the Validator caches it for faster performance.
There are two ways to use the Validator component: either directly or through the Form component. Using it directly is very straightforward. From the controller level, the Validator service can be obtained by calling
$this->get('validator'). Inside a Unit test, the Validator is created with
ValidatorFactory::buildDefault()->getValidator(). Once we have a Validator, we can pass it an entity to be validated:
When binding data to a Form, the Form object automatically transfers the data to the underlying entity object and calls the Validation component to check it.