Those of you who already upgraded to Symfony 2.1 Beta probably noticed that
the new version comes with many backwards compatibility breaks in the Form
component. Many of you probably ask yourselves: Why? The simple answer is that
the Form component is one of the most complex components in Symfony at all. Real
life form processing is simply so much more than just ctype_digit($_POST['age'])
.
Such complexity is really hard to get right at the first time. Of course, this
is hardly an argument that makes you want to
take the hassle of upgrading your code.
Instead, I would like to use this post to highlight all the cool changes in
Symfony 2.1 that make your daily coder life a bit more joyful.
Note
If any of the following code samples are hard to understand, I recommend to read the Form documentation for a start, which does an excellent job at explaining the necessary basics.
No more bindRequest()¶
The method bind()
is now intelligent enough to check whether you pass a
Symfony Request
instance or data extracted from the $_POST
array. No more
do you have to use bindRequest()
in the first case. If you're unsure what I
mean, look at this sample:
1 2 3 4 5 | // Before, you had to call bindRequest($request) here
// Now you can always use bind()
$form->bind($request);
$form->bind($_POST[$form->getName());
|
It is only a small improvement, but hopefully you like it as much as I do.
Custom field constraints¶
The next cool thing I want to show you is the new "constraints" option. This option was called "validation_constraint" before, but the new version is much more powerful. Look at the following sample to get a feeling for its usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\NotBlank;
$builder
->add('firstName', 'text', array(
'constraints' => new MinLength(3),
))
->add('lastName', 'text', array(
'constraints' => array(
new NotBlank(),
new MinLength(3),
),
))
;
|
As you can see, you can set the option on any field in the form and pass one or
more validation constraints for that field. In fact, creating a form backed by
an array is now a bliss! If you store the above code in a PersonType
class,
the below code is all you need to process and validate that form.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $form = $this->createForm(new PersonType(), array(
'firstName' => 'John',
'lastName' => 'Wayne',
));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
// do something with $form->getData() and redirect
}
}
// render template
|
If your form is backed by an object, the constraints defined in the "constraints" option will be evaluated in addition to the ones defined in the object's class.
The OptionsResolver component¶
Symfony 2.1 also ships with a new component, the
OptionsResolver, which is heavily
used by the Form component. Your form types may now contain the method
setDefaultOptions()
which receives an instance of such an "options resolver".
But what does this resolver do? A lot of great things. You can obviously still
set default values for your options. But you can also limit the values that
the option may contain or set the data type(s) accepted by the options.
You can also mark options as required or optional. Optional options don't have
default values, so they are a great way to find out whether a user has set
that option, even if he set it to null
.
As code speaks more than thousand words, here comes again an example demonstrating these features:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class AjaxType extends AbstractType
{
public function getName()
{
return 'ajax';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(array(
'help' => '',
'mode' => 'compact',
))
->setRequired(array(
'theme',
))
->setOptional(array(
'ajax_data',
))
->setAllowedTypes(array(
'help' => 'string',
'theme' => 'Acme\AjaxBundle\ThemeInterface',
'ajax_data' => array('null', 'scalar'),
))
->setAllowedValues(array(
'mode' => array('compact', 'wide'),
))
;
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!array_key_exists('ajax_data', $options)) {
// option was not set, do something...
}
}
}
|
These options can be passed when adding a field of that type to the form:
1 2 3 4 | $builder->add('tags', 'ajax', array(
'theme' => $shinyTheme,
'help' => 'Please enter one or more tags, e.g. "framework, php"',
));
|
Based on the above definition, the resolver now does the following checks:
- you must not pass any invalid option names
- the option "theme" has to be passed
- the options "help", "theme" and "ajax_data" must have the configured data types
- the option "mode" may only be one of "compact" and "wide"
But the OptionsResolver component allows you to do even more. You can set default values of options that depend on the values of other options. Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | use Symfony\Component\OptionsResolver\Options;
$mode = function (Options $options) {
if ($options['max_length'] > 20) {
return 'wide';
}
return 'compact';
};
$resolver->setDefaults(array(
'mode' => $mode,
));
|
This code sets the option "mode" to "wide" by default if the value of the other option "max_length" is bigger than 20. Note that we're only talking about a default value here, so you can easily override the default "mode" when you create the form.
More information about the OptionsResolver component and - more importantly - how to use it independently of the Form component can be found in its README file.
Error mapping fu¶
Symfony 2.0 is a bit "special" when it comes to mapping validation errors to the fields of a form. Don't worry, because this has been vastly improved in Symfony 2.1. And should the built-in mapping not do what you want, you can customize it with the revamped "error_mapping" option.
Let's take the following model class for example:
1 2 3 4 5 6 7 8 9 10 | class Address
{
/**
* @Assert\True(message = "The given city does not match the zip code")
*/
public hasMatchingCityAndZipCode()
{
// do the logic and return true or false
}
}
|
A custom method in this class validates whether the city and zip code match. Unfortunately, there is no "matchingCityAndZipCode" field in your form, so all that Symfony can do for you is display the error on top of the form. With customized error mapping, you can do better:
1 2 3 4 5 6 7 8 9 10 11 | class AddressType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'error_mapping' => array(
'matchingCityAndZipCode' => 'city',
),
));
}
}
|
Now the error is neatly displayed with the "city" field. The important thing here is to understand the left and the right side of the mapping:
- The left side contains property paths. If the violation is generated on a
property or method of a class, its path is simply
propertyName
. If the violation is generated on an entry of an array orArrayAccess
object, the property path is[indexName]
. You can construct nested property paths by concatenating them, separating properties by dots, for example:addresses[work].matchingCityAndZipCode
- The right side contains simply the names of fields in the form.
The left side of the error mapping also accepts a dot .
, which refers to the
field itself. That means that any error added to the field is added to the
given nested field instead.
1 2 3 4 5 | $resolver->setDefaults(array(
'error_mapping' => array(
'.' => 'city',
),
));
|
Collection improvements¶
The "collection" type,
which was rather unusable in Symfony 2.0, has been improved a lot. The type
is finally able to call addXxx()
and removeXxx()
methods in your model
class. The following snippet illustrates an excerpt of such a class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Article
{
public function addTag(Tag $tag)
{
// ...
}
public function removeTag(Tag $tag)
{
// ...
}
public function getTags()
{
// ...
}
}
|
In the form for that class, you can add a tag collection field as usual:
1 2 3 4 5 | $builder->add('tags', 'collection', array(
'type' => 'text',
'allow_add' => true,
'allow_remove' => true,
));
|
When you add or remove tags through this field, Symfony will now call the
correct methods in your object. If no addTag()
or removeTag()
method is
found, the framework will use setTags()
instead or throw an exception if that
too isn't found.
Note
There are still a few issues with edge cases that we couldn't fix anymore in version 2.1. We are working on fixing those in the next release.
A second, nice improvement in the collection form concerns theming. You probably knew that you can style individual elements of a form by defining blocks with names equal to the HTML IDs of these elements, prefixed by underscores:
{% block _article_tags_widget %}
{# custom HTML #}
{% endblock %}
Unfortunately this wasn't very usable in collection fields, because every row
of that field has a different ID, such as _article_tags_0_widget
. This has
been fixed: You can now style all rows at once by replacing the row index with
the keyword "entry":
{% block _article_tags_entry_widget %}
{# custom HTML #}
{% endblock %}
Note
Styling individual rows by index is not supported anymore. This had a horrible impact on performance, which simply wasn't justifiable.
HTML5 date and datetime tags¶
Symfony now fully supports the HTML5 specification when you add "date" or "datetime" fields to your form and set the "widget" option to "single_text".
1 2 3 | $builder->add('createdAt', 'date', array(
'widget' => 'single_text',
));
|
Browsers with HTML5 support are able to display localized date and time pickers for these fields, under the condition that the "value" attribute contains a date that follows the RFC3339 standard. Symfony now does this for you. If you don't want HTML5 support though, you can add a custom "format" option to the field:
1 2 3 4 | $builder->add('createdAt', 'date', array(
'widget' => 'single_text',
'format' => 'dd.MM.yyyy',
));
|
Symfony will recognize this case and output a regular "text" tag instead of the HTML5 variant.
Speed, speed, speed¶
The last thing I would like to mention are the vast improvements that have been made concerning the speed of the Form component. As Fabien mentioned in the beta4 announcement, the processing and rendering time on large, complex forms was reduced dramatically, with an 85% speed gain with PHP templates and a 60% speed gain for Twig. We are not satisfied with these results yet, as the complete time for such forms still surpasses one or two seconds, depending on the power of your server. So we will continue this quest in the upcoming releases.
I hope this blog post could give you an overview of the most important new features in the Symfony 2.1 Form component. Of course, we also worked hard to improve existing features and eliminated a large number of bugs. You can get a picture of all these changes by reading the components CHANGELOG. If you are interested in detailed steps for upgrading your applications, I advice you to consult the UPGRADE file.
So long, enjoy :)
Thank you Bernhard for these great features, the form component is really awesome!
Remarkable work Bernhard!
"As code speaks more than thousand words..." Great work!
Excellent! each day closer to the final version
Thank you for all of your hard work!
The form component is one of the reasons I chose Symfony2 and now it's getting even better. I have use cases for each improvement. Thanks for your great work.
Thanks, I think there is an error at the first code snippet:
$form->bind($_POST[$form->getName());
A closed bracket is missing.
Great work Benhard. The Form Component is the first thing I usually compare to other frameworks when debating with friends. It is more than obvious that I always win :)
Thanks Bernhard, very impressive perf improvements !
Great new, why named it "Form" component only ?
I am using it with JSON services (iphone/android clients) like the Java "request data binder" component. In my opinion "Form" should be a sub component of a request data binder component, since you maybe not need to render it as HTML.
Thanks for the hard work!
Oh my !
Great :) I'll spend more night on hacking then :)
Many thanks for all the great work on the forms - very promising for me still a symphony 1.4 buddy... A question though, it appears that symphony.com/doc has not been updated since more than a month (namely 20th June)? The recent commits on github against the forms chapter provide a lot of amendments including the code examples. I may be wrong, but earlier I observed that synchronisation would occur automatically on a daily base? TIA!
@Raphael: Did you switch to the "master" version of the doc in the top right corner?
Oops didn't think of this, my fault. The issue holds for the PDF download though, just now I've checked again the master version download. Many thanks, RAPHAEL
Excellent work, thanks!!
Great work! And adding the release today of the stable version of S2.1 is even greater.