How can I add a violation to a collection?

Viewed 9904

My form looks like this:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $factory = $builder->getFormFactory();

    $builder->add('name');

    $builder->add('description');

    $builder->add('manufacturers', null, array(
        'required' => false
    ));

    $builder->add('departments', 'collection', array(
        'type' => new Department
    ));
}

I have a class validator on the entity the form represents which calls:

    if (!$valid) {
        $this->context->addViolationAtSubPath('departments', $constraint->message);
    }

Which will only add a 'global' error to the form, not an error at the sub path. I assume this is because departments is a collection embedding another FormType.

If I changed departments to one of the other fields it works fine.

How can I get this error to appear in the right place? I assume it would work fine if my error was on a single entity within the collection, and thus rendered in the child form, but my criteria is that the violation occur if none of the entities in the collection are marked as active, thus it needs to be at the parent level.

3 Answers

I have been wrestling with this issue in Symfony 3.3, where I wished to validate an entire collection, but pass the error to the appropriate collection element/field. The collection is added to the form thus:

        $form->add('grades', CollectionType::class,
            [
                'label'         => 'student.grades.label',
                'allow_add'     => true,
                'allow_delete'  => true,
                'entry_type'    => StudentGradeType::class,
                'attr'          => [
                    'class' => 'gradeList',
                    'help'  => 'student.grades.help',
                ],
                'entry_options'  => [
                    'systemYear' => $form->getConfig()->getOption('systemYear'),
                ],
                'constraints'    => [
                    new Grades(),
                ],
            ]
        );

The StudentGradeType is:

<?php

namespace Busybee\Management\GradeBundle\Form;

use Busybee\Core\CalendarBundle\Entity\Grade;
use Busybee\Core\SecurityBundle\Form\DataTransformer\EntityToStringTransformer;
use Busybee\Core\TemplateBundle\Type\SettingChoiceType;
use Busybee\Management\GradeBundle\Entity\StudentGrade;
use Busybee\People\StudentBundle\Entity\Student;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class StudentGradeType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * StaffType constructor.
     *
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('status', SettingChoiceType::class,
                [
                    'setting_name' => 'student.enrolment.status',
                    'label'        => 'grades.label.status',
                    'placeholder'  => 'grades.placeholder.status',
                    'attr'         => [
                        'help' => 'grades.help.status',
                    ],
                ]
            )
            ->add('student', HiddenType::class)
            ->add('grade', EntityType::class,
                [
                    'class'         => Grade::class,
                    'choice_label'  => 'gradeYear',
                    'query_builder' => function (EntityRepository $er) {
                        return $er->createQueryBuilder('g')
                            ->orderBy('g.year', 'DESC')
                            ->addOrderBy('g.sequence', 'ASC');
                    },
                    'placeholder'   => 'grades.placeholder.grade',
                    'label'         => 'grades.label.grade',
                    'attr'          => [
                        'help' => 'grades.help.grade',
                    ],
                ]
            );

        $builder->get('student')->addModelTransformer(new EntityToStringTransformer($this->om, Student::class));

    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setDefaults(
                [
                    'data_class'         => StudentGrade::class,
                    'translation_domain' => 'BusybeeStudentBundle',
                    'systemYear'         => null,
                    'error_bubbling'     => true,
                ]
            );
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'grade_by_student';
    }


}

and the validator looks like:

namespace Busybee\Management\GradeBundle\Validator\Constraints;

use Busybee\Core\CalendarBundle\Entity\Year;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class GradesValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {

        if (empty($value))
            return;

        $current = 0;
        $year    = [];

        foreach ($value->toArray() as $q=>$grade)
        {
            if (empty($grade->getStudent()) || empty($grade->getGrade()))
            {

                $this->context->buildViolation('student.grades.empty')
                    ->addViolation();

                return $value;
            }

            if ($grade->getStatus() === 'Current')
            {
                $current++;

                if ($current > 1)
                {
                    $this->context->buildViolation('student.grades.current')
                        ->atPath('['.strval($q).']')  // could do a single atPath with a value of "[".strval($q)."].status"
                        ->atPath('status')      //  full path = children['grades'].data[1].status
                        ->addViolation();

                    return $value;

                }
            }

            $gy = $grade->getGradeYear();

            if (! is_null($gy))
            {
                $year[$gy] = empty($year[$gy]) ? 1 : $year[$gy]  + 1 ;

                if ($year[$gy] > 1)
                {
                    $this->context->buildViolation('student.grades.year')
                        ->atPath('['.strval($q).']')
                        ->atPath('grade')
                        ->addViolation();

                    return $value;

                }
            }
        }
    }
}

This results in the error being added to the field in the element of the collection as per the attach image. Error at Element/Field

Craig

Related