Как добавить нарушение в коллекцию?

Моя форма выглядит следующим образом:

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
    ));
}

У меня есть валидатор класса для объекта, который представляет форму, которая вызывает:

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

Которая добавит только глобальную ошибку в форму, а не ошибку на вспомогательном пути. Я предполагаю, что это потому, что отделы представляют собой набор, встраивающий другой FormType.

Если я изменил departments на одно из других полей, он отлично работает.

Как я могу получить эту ошибку в нужном месте? Я предполагаю, что это будет нормально работать, если моя ошибка была на одном объекте в коллекции и, таким образом, отображается в дочерней форме, но мои критерии заключаются в том, что нарушение происходит, если ни один из объектов в коллекции не помечен как активный, поэтому ему нужно быть на родительском уровне.

Ответ 1

По умолчанию формы имеют параметр "error_bubbling", установленный на true, что приводит к описанному поведению. Вы можете отключить эту опцию для отдельных форм, если хотите, чтобы они сохраняли свои ошибки.

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

Ответ 2

Я боролся с этой проблемой в Symfony 3.3, где я хотел проверить всю коллекцию, но передал ошибку соответствующему элементу/элементу коллекции. Коллекция добавляется к форме таким образом:

        $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(),
                ],
            ]
        );

Тип StudentGradeType:

<?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';
    }


}

и валидатор выглядит так:   

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;

                }
            }
        }
    }
}

Это приводит к добавлению ошибки в поле в элементе коллекции в соответствии с прикрепленным изображением. Ошибка в элементе/поле

Craig

Ответ 3

У меня очень похожий случай. У меня есть CollectionType с пользовательской формой (с DataTransformers внутри и т.д.), Мне нужно проверять один за другим элементы и отмечать, что из них не так, и печатать их на виде.

Я делаю это решение в ConstraintValidator (мой пользовательский валидатор):

Валидатор должен быть нацелен на CLASS_CONSTRAINT для работы или свойство property не работает.

public function validate($value, Constraint $constraint) {
    /** @var Form $form */
    $form = $this->context->getRoot();
    $studentsForm = $form->get("students"); //CollectionType name in the root Type
    $rootPath = $studentsForm->getPropertyPath()->getElement(0);

    /** @var Form $studentForm */
    foreach($studentsForm as $studentForm){
        //Iterate over the items in the collection type
        $studentPath = $studentForm->getPropertyPath()->getElement(0);

        //Get the data typed on the item (in my case, it use an DataTransformer and i can get an User object from the child TextType)
        /** @var User $user */
        $user = $studentForm->getData();

        //Validate your data
        $email = $user->getEmail();
        $user = $userRepository->findByEmailAndCentro($email, $centro);

        if(!$user){
            //If your data is wrong build the violation from the propertyPath getted from the item Type
            $this->context->buildViolation($constraint->message)
                ->atPath($rootPath)
                ->atPath(sprintf("[%s]", $studentPath))
                ->atPath("email") //That last is the name property on the item Type
                ->addViolation();
        }
    }
}

Просто я проверяю снова элементы формы в коллекции и строю нарушение, используя свойствоPath из элемента в коллекции, которая неверна.