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 or ArrayAccess 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 :)

Published in #Living on the edge