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

2

16

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!

Pruritus answered 31/1, 2014 at 17:39 Comment(2)
+1. I had a same question but have no answer to this.Mantoman
@byf-ferdy thanks, it sounds like a similar question but I'm not sure if it is the same. I believe my case might have more options for a workaround since there is a custom form type in between.Pruritus
U
13

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
Unalienable answered 3/2, 2014 at 9:47 Comment(3)
Thank you, what you say makes sense. How would I re-use this TextWithAutoValue type for different entities with different constraints?Pruritus
Thank you for elaborating. I understand the workings of everything you say up until the very last part. It sounds like a workable solution. I will play around with it and see what I run in to.Pruritus
I'm going to award you the bounty for giving the most helpful answer. I didn't get it to work as I wanted, but then again, there might not be an answer to my question. Thanks again for your help.Pruritus
H
2

As described here https://speakerdeck.com/bschussek/3-steps-to-symfony2-form-mastery#39 (slide 39) by Bernhard Schussek (the main contributor of symofny form extension), a transformer should never change the information, but only change its representation.

Adding the information (checkbox' => true), you are doing something wrong.

In Edit:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->add('value', 'text', $options);

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

    $builder->addModelTransformer(
        new TextWithAutoValueTransformer()
    );
}
Hue answered 3/2, 2014 at 11:15 Comment(2)
Fair enoguh. This is just my attempt to get a working checkbox in the frontend. I just need it for the interface, I don't need it as a part of an entity or anything like that. I just use it to autofill the accompanied text field when it's checked. I added it to the form because I want to keep it checked/unchecked between POST requests. Do u know a way to accomplish that?Pruritus
If you use a class TextWithCheckbox as described by Zephyr, you can set the default true value of the checkbox in the class constructor.Interpretive

© 2022 - 2024 — McMap. All rights reserved.