Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specify different validation groups for each item of a collection in Symfony 2?

[Documentation about collection] When embedding forms (collection type) is possible to specify validation groups for each item, based on the current item? It seems not working ATM.

The TaskType form adding a collection of tags:

// src/Acme/TaskBundle/Form/Type/TaskType.php

// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('tags', 'collection', array(
        // ...
        'by_reference' => false,
    ));
}

For example we have two tags (tag 1 and tag 2) and a new tag is added using the "Add" button (via JavaScript):

-----------
| add tag |
-----------
- tag 1 (existing)
- tag 2 (added clicking the "add tag" button)

Tag 1 should be validated against Default, Edit groups while tag 2 against Default group only.

TagType form defining dynamic validation groups

Based on the underlying data, if tag is new it gets Default group, if exists Default, Create groups:

// ...

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function (FormInterface $form) {
            $tag = $form->getData();

            $groups = array('Default');
            if (null !== $tag && null !== $tag->getId()) {
                $groups[] = 'Edit';
            }

            return $groups;
        }
    ));
}

// ...

Tag entity with a constraint in the "Edit" group

An example with Tag defining two properties (accessors omitted):

class Tag
{
    /**
     * @Assert\NotBlank()
     */
    protected $name;

    /**
     * @Assert\NotBlank(groups={"Edit"})
     * @Assert\Length(max="255")
     */
    protected $description;

    // ...
}

For an existing tag: description should not be blank. For a new tag: description can be blank.

Proof form is valid, validator shows errors (wrong!)

Just edit an existing tag and leave the description blank. The form validates but the validator service shows errors:

$form = $this->createForm('task', $task)
    ->handleRequest($request);

$validator = $this->get('validator');

if ($form->isValid()) {
    foreach ($task->getTags() as $tag) {
        // Validate against Default, Edit groups
        $errors = $validator->validate($tag, array('Default', 'Edit'));

        if (null !== $tag && null !== $tag->getId()) {
            echo 'Existing tag #'.$tag->getId();
        } else {
            echo 'New tag';
        }

        echo ', errors: '.count($errors).'<br>';
    }

    die('Form is valid.')

    // ...
}

Output:

Existing tag #863, errors: 1
Form is valid.

Update 1: I've tried (without success) with a static method determineValidationGroups as suggested here:

public static function determineValidationGroups(FormInterface $form)
{
    $groups =  array('Default');
    if ($form->getData() !== null && null !== $form->getData()->getId())
    {
        $groups =  array('Edit');
    }

    var_dump($groups);

    return $groups;
}

In TagType form:

/**
 * {@inheritdoc}
 */
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        // ... 
        'validation_groups' => array(
            'Acme\TaskBundle\Entity\Tag',
            'determineValidationGroups'
        ),
    ));
}

Output with just one existing tag and one created using the "add tag" link seems correct. But no errors for the existing tag leaving the description blank:

array (size=1)
  0 => string 'Edit' (length=4)

array (size=1)
  0 => string 'Edit' (length=4)

rray (size=1)
  0 => string 'Default' (length=7)

rray (size=1)
  0 => string 'Default' (length=7)
like image 655
gremo Avatar asked Jan 22 '14 07:01

gremo


1 Answers

The complete code I used to test my answer is on https://github.com/guilro/SymfonyTests/tree/SO21276662.

Valid constraint force Validator to validate embed object, and AFAIK the API provides no way to set validation group.

But at a higher level, we can ask Form component to cascade ValidationListener to all embed forms, and use the Form component API to set validation group.

We must use :

  • 'cascade_validation' => true option in the FormBuilder, at all levels. It is set to false by default.
  • a callback in TagType settings to set validation group. (You were on the right track.)
  • 'error_bubbling' => false, as it is true by default in collections

and we're done, we can display the form with all errors next to corresponding fields.

in TaskType.php :

class TaskType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
        ->add('name')
        ->add('tags', 'collection', array(
            'type' => 'tag',
            'error_bubbling' => false,
            'allow_add' => true,
            'by_reference' => false,
            'cascade_validation' => true
        ))
    ;
  }

  public function setDefaultOptions(OptionsResolverInterface $resolver)
  {
    $resolver->setDefaults(array(
        'data_class' => 'Acme\TaskBundle\Entity\Task',
        'cascade_validation' => true
    ));
  }
}

in TagType.php :

class TagType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
        ->add('name')
        ->add('description', 'text', array('required' => false));
  }

  public function setDefaultOptions(OptionsResolverInterface $resolver)
  {
    $resolver->setDefaults(array(
        'data_class' => 'Acme\TaskBundle\Entity\Tag',
        'validation_groups' => function(FormInterface $form) {
            if ($form->getData() !== null && null !== $form->getData()->getId())
            {
                return array('Edit');
            }
            return array('Default');
        },
        'error_bubbling' => false,
    ));
  }
}
like image 137
jillro Avatar answered Nov 03 '22 05:11

jillro