在Symfony 3中为集合的每个元素应用特定的验证组

7
我不得不将我的一个项目从Symfony 2.8升级到Symfony 3.4,并注意到验证过程发生了巨大变化。
为了简化,假设我有一个用户实体,其中包含许多地址实体。当我创建/更新用户时,我希望能够添加/删除/更新任意数量的地址。因此,在Symfony 2.8中,我遇到了这种情况: 用户 我使用注解验证器。
src/AppBundle/Entity/User.php

//...
class User
{
    //...
    /** 
     * @Assert\Count(min=1, max=10)
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Address", mappedBy="user", cascade={"persist", "remove"})
     */
    protected $addresses;
    //...
}

UserForm

src/AppBundle/Form/UserForm.php

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        ->add('addresses', CollectionType::class, [
            'type' => AddressType::class,
            'cascade_validation' => true,
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false,
        ])
    ;
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults([
        'data_class' => User::class,
        'cascade_validation' => true,
        'validation_groups' => // User's logic
    ]);
}

地址

src/AppBundle/Entity/Address.php

//...
class Address
{
    //...
    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="user")
     */
    protected $user;

    /**
     * @Assert\NotBlank(groups={"zipRequired"})
     * @ORM\Column(type="text", nullable="true")
     */
    protected $zipCode;
    //...
}

地址表单

src/AppBundle/Form/AddressForm.php

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        ->add('zipCode', TextType::class)
    ;
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults([
        'data_class' => Address::class,
        'cascade_validation' => true,
        'validation_groups' => function(FormInterface $form) {
            /** @var Address $data */
            $data = $form->getData();
            $validation_groups = [];

            // Simplified here, it's a service call with heavy logic
            if ($data->doesRequireZip()) {
                $validation_groups[] = 'zipRequired';
            }

            return $validation_groups;
        },
    ]);
}

在Symfony 2.8中

添加了3个地址,其中两个必须验证zipRequired组,一个不需要。我做到了!

在Symfony 3.4中

我在User::$zipCode声明中添加了@Assert\Valid()并删除了'cascade_validation' => true(不在configureOptions方法中,但似乎未使用,因为它已过时)。

但是现在添加了3个地址,其中两个应该验证zipRequired组,而另一个则不需要:仅使用User类的验证器组,所以我可以使用不连贯的数据验证表单

我使用xdebug进行了检查,并且AddressForm中的validator_groups回调被调用,但验证器没有被调用。

我尝试了此处描述的解决方案:Specify different validation groups for each item of a collection in Symfony 2?,但在symfony 3.4中无法工作,因为cascade_validation在属性上会抛出错误。

在我的情况下,涉及的逻辑太重,不能像Specify different validation groups for each item of a collection in Symfony 3?中描述的那样使用解决方案,因为重写整个validation_groups回调并将其应用于所有子实体非常低效。

@Assert\Validcascade_validation的行为不同,有没有一种方法来处理在Symfony 3.4中嵌入表单的各个实体验证组,或该功能已经消失了?


你已经解决了吗?我在使用 SF 5.1,遇到了完全相同的问题。 - Dreanmer
刚刚发现他们将尊重添加在根表单上的任何验证组,或许子表单上的回调函数运行并返回正确的组列表,但它们会被根表单的验证组覆盖。 - Dreanmer
1
遗憾的是,他们称之为一个特性: https://github.com/symfony/symfony/issues/31441 我能找到的唯一方法就是添加一个回调。 - Dreanmer
1个回答

3

由于这是一种预期的行为(整个表单应该从根验证组进行验证,您可以在此处查看说明:https://github.com/symfony/symfony/issues/31441),我找到解决此问题的唯一方法是在集合项(实体)中使用回调验证:

src/AppBundle/Entity/Address.php

//...
class Address
{
    //...
    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="user")
     */
    protected $user;

    /**
     * @ORM\Column(type="text", nullable="true")
     */
    protected $zipCode;
    //...

    /**
     * @Assert\Callback()
     */
    public function validate(): void {
        if ($data->doesRequireZip()) {
            // validate if $zipCode isn't null
            // other validations ...  
        }
    }
}

更多关于Symfony回调断言的信息:https://symfony.com/doc/current/reference/constraints/Callback.html

这个解决方案是在Symfony 5.1中创建的,但是这可能从2.8+版本都可以使用。

更新

只是为了补充,我的最终解决方案是通过验证器添加验证,强制每个属性与特定组进行匹配(如果您的表单已经进行了默认设置,则无需强制执行默认设置)。

/**
 * @Assert\Callback()
 */
public function validate(ExecutionContext $context): void
{
    $validator = $context->getValidator();
    $groups = $this->doesRequireZip() ? ['requiredZipGroup'] : [];
    $violations = $validator->validate($this, null, $groups);

    /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
    foreach ($violations as $violation) {
        $context->buildViolation($violation->getMessage(), $violation->getParameters())
            ->atPath($violation->getPropertyPath())
            ->addViolation();
    }
}

$validator->validate()的第二个参数传递为null将强制Assert\Valid通过对象$this运行,因此,$groups中的所有约束和回调也将运行。

我有一段时间没有使用这个特定的解决方案了。但是你的解决方案看起来很棒。 不管怎样,他们改变了这个行为还是太糟糕了。 - Jihel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接