Symfony2: How to use constraints on custom compound form type?

Maurice picture Maurice · Jan 31, 2014 · Viewed 24.9k times · Source

Here is a question I've been breaking my head over for a while now. Please know that I'm not a Symfony2 expert (yet), so I might have made a rookie mistake somewhere.

Field1: Standard Symfony2 text field type

Field2: Custom field type compoundfield with text field + checkbox field)

preview

My Goal: Getting constraints added to the autoValue field to work on the autoValue's text input child

The reason why the constraints don't work is probably because NotBlank is expecting a string value and the internal data of this form field is an array array('input'=>'value', 'checkbox' => true). This array value gets transformed back into a string with a custom DataTransformer. I suspect however that that happens AFTER validating the field against known constraints.

As you see below in commented code, I have been able to get constraints working on the text input, however only when hardcoded into the autoValue's form type, and I want to validate against the main field's constraints.

My (simplified) sample code for controller and field:

.

Controller code

Setting up a quick form for testing purposes.

<?php
//...
// $entityInstance holds an entity that has it's own constraints 
// that have been added via annotations

$formBuilder = $this->createFormBuilder( $entityInstance, array(
    'attr' => array(
        // added to disable html5 validation
        'novalidate' => 'novalidate'
    )
));

$formBuilder->add('regular_text', 'text', array(
    'constraints' => array(
        new \Symfony\Component\Validator\Constraints\NotBlank()
    )
));

$formBuilder->add('auto_text', 'textWithAutoValue', array(
    'constraints' => array(
        new \Symfony\Component\Validator\Constraints\NotBlank()
    )
));

.

TextWithAutoValue source files

src/My/Component/Form/Type/TextWithAutoValueType.php

<?php

namespace My\Component\Form\Type;

use My\Component\Form\DataTransformer\TextWithAutoValueTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class TextWithAutoValueType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('value', 'text', array(
            // when I uncomment this, the NotBlank constraint works. I just
            // want to validate against whatever constraints are added to the
            // main form field 'auto_text' instead of hardcoding them here
            // 'constraints' => array(
            //     new \Symfony\Component\Validator\Constraints\NotBlank()
            // )
        ));

        $builder->add('checkbox', 'checkbox', array(
        ));

        $builder->addModelTransformer(
            new TextWithAutoValueTransformer()
        );
    }

    public function getName()
    {
        return 'textWithAutoValue';
    }
}

src/My/Component/Form/DataTransformer/TextWithAutoValueType.php

<?php

namespace My\Component\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;

class TextWithAutoValueTransformer 
    implements DataTransformerInterface
{
    /**
     * @inheritdoc
     */
    public function transform($value)
    {
        return array(
            'value'    => (string) $value,
            'checkbox' => true
        );
    }

    /**
     * @inheritdoc
     */
    public function reverseTransform($value)
    {
        return $value['value'];
    }
}

src/My/ComponentBundle/Resources/config/services.yml

parameters:

services:
    my_component.form.type.textWithAutoValue:
        class: My\Component\Form\Type\TextWithAutoValueType
        tags:
            - { name: form.type, alias: textWithAutoValue }

src/My/ComponentBundle/Resources/views/Form/fields.html.twig

{% block textWithAutoValue_widget %}
    {% spaceless %}

    {{ form_widget(form.value) }}
    {{ form_widget(form.checkbox) }}
    <label for="{{ form.checkbox.vars.id}}">use default value</label>

    {% endspaceless %}
{% endblock %}

.

Question

I have been reading docs and google for quite some hours now and can't figure out how to copy, bind, or reference the original constraints that have been added while building this form.

-> Does anyone know how to accomplish this?

-> For bonus points; how to enable the constraints that have been added to the main form's bound entity? (via annotations on the entity class)

PS

Sorry it became such a long question, I hope that I succeeded in making my issue clear. If not, please ask me for more details!

Answer

Zephyr picture Zephyr · Feb 3, 2014

I suggest you read again the documentation about validation first.

What we can make out of this is that validation primarily occurs on classes rather than form types. That is what you overlooked. What you need to do is:

  • To create a data class for your TextWithAutoValueType, called src/My/Bundle/Form/Model/TextWithAutoValue for instance. It must contain properties called text and checkbox and their setters/getters;
  • To associate this data class to your form type. For this, you must create a TextWithAutoValueType::getDefaultOptions() method and populate the data_class option. Go here for more info about this method;
  • Create validation for your data class. You can either use annotations or a Resources/config/validation.yml file for this. Instead of associating your constraints to the fields of your form, you must associate them to the properties of your class:

validation.yml:

src/My/Bundle/Form/Model/TextWithAutoValue:
    properties:
        text:
            - Type:
                type: string
            - NotBlank: ~
        checkbox:
            - Type:
                type: boolean

Edit:

I assume that you already know how to use a form type in another. When defining your validation configuration, you can use a very useful something, called validation groups. Here a basic example (in a validation.yml file, since I'm not much proficient with validation annotations):

src/My/Bundle/Form/Model/TextWithAutoValue:
    properties:
        text:
            - Type:
                type: string
                groups: [ Default, Create, Edit ]
            - NotBlank:
                groups: [ Edit ]
        checkbox:
            - Type:
                type: boolean

There is a groups parameter that can be added to every constraint. It is an array containing validation group names. When requesting a validation on an object, you can specify with which set of groups you want to validate. The system will then look in the validation file what constraints should be applied.

By default, the "Default" group is set on all constraints. This also is the group that is used when performing a regular validation.

  • You can specify the default groups of a specific form type in MyFormType::getDefaultOptions(), by setting the validation_groups parameter (an array of strings - names of validation groups),
  • When appending a form type to another, in MyFormType::buildForm(), you can use specific validation groups.

This, of course, is the standard behaviour for all form type options. An example:

$formBuilder->add('auto_text', 'textWithAutoValue', array(
    'label' => 'my_label',
    'validation_groups' => array('Default', 'Edit'),
));

As for the use of different entities, you can pile up your data classes following the same architecture than your piled-up forms. In the example above, a form type using textWithAutoValueType will have to have a data_class that has a 'auto_text' property and the corresponding getter/setter.

In the validation file, the Valid constraints will be able to cascade validation. A property with Valid will detect the class of the property and will try to find a corresponding validation configuration for this class, and apply it with the same validation groups:

src/My/Bundle/Form/Model/ContainerDataClass:
    properties:
        auto_text:
            Valid: ~ # Will call the validation conf just below with the same groups

src/My/Bundle/Form/Model/TextWithAutoValue:
    properties:
        ... etc