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 (Symfony\Component\Form
)
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.
<?php
$myForm = $this->createForm(new MyType(), $entity, array(...));
Symfony’s 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 Type
’s 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::<wbr />create()
. 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 $builder->getForm()
.
<?php
class MyType extends AbstractType {...}
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.
<?php
public function buildForm(FormBuilder $bldr, array $opts) {...}
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.
<?php
$bldr->add('name', 'text', array('required' => true));
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.
<?php
return array('view' => $form->createView());
Because the 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.
Entities
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 (Symfony\Component\Validator
)
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:
<?php
$validator = $this->get('validator');
$errors = $validator->validate($entity);
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.