Symfony 无法验证集合表单

Symfony unable to validate collection form

我正在研究一个叫做目标的 collection form,用户可以根据需要添加任意数量的目标,这部分工作正常,我能够 show/add/edit/delete 目标很好

我遇到的问题是如何验证数据。在表单上有一个 goal target(整数)字段和 saved to date(整数)字段。

规则是 saved to date 的值不能超过 goal target 为此,我创建了 custom validation 并且 class 在选择表单时已提交。

SavedToDate.php

namespace MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class SavedToDate extends Constraint
{
    public $message = '"%string%" Saved to date cannot be greater than target date.';
}

SavedToDateValidator.php

namespace MyBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class SavedToDateValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        $values = $this->context->getRoot()->getdata()->getGoals()->getValues();
        foreach($values as $item ){
            $target = $item->getTarget();
            $savedToDate = $item->getReached();
           if ($savedToDate > $target) {
                $this->context->buildViolation($constraint->message)
                    ->setParameter('%string%', $value)
                    ->addViolation();
            }
        }
    }

    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
}

通过阅读 symfony 文档,我似乎需要添加 validation.yml.

中的约束 Valid
goals:
    - Valid:

Problem 1

假设当我针对第一个目标输入大于 goal targetsaved to date 时,我得到的不是仅针对该目标的错误,而是针对这两个目标的错误。

注意 第二个错误不应该出现,因为 8000 小于 20000

Problem 2

假设我给的两个目标 saved to date 大于 goal target 然后我看到每个字段有 2 个错误。

这是我的视图模板

{% for goals in form.goals %}      
        <div class="container-fluid">
            <div class="row">
                <div class="col-lg-12">
                    {% if(form_errors(goals.target))  %}
                        <div class="alert alert-danger" role="alert">{{ form_errors(goals.target) }}</div>
                    {% endif %}
                    {% if(form_errors(goals.reached))  %}
                        <div class="alert alert-danger" role="alert">{{ form_errors(goals.reached) }}</div>
                    {% endif %}
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-xs-2" style="padding-top: 5%">
                <label class="" for="exampleInputEmail2">Goal target</label>
                <div class="form-group input-group">
                    {{ form_widget(goals.target, {'attr': {'class': 'form-control'}}) }}
                </div>


            </div>
            <div class="col-xs-2" style="padding-top: 5%">
                <label class="" for="exampleInputEmail2">Saved to date</label>

                <div class="form-group input-group">
                    {{ form_widget(goals.reached, {'attr': {'class': 'form-control'}}) }}
                </div>
            </div>
            <div class="col-xs-2" style="padding-top: 5%">
                <label class="" for="exampleInputEmail2">Goal deadline</label>

                <div class="form-group input-group">
                    {{ form_widget(goals.deadline, {'attr': {'class': 'form-control dp'}}) }}
                </div>
            </div>
            <div class="col-xs-2" style="padding-top: 5%">
                <label class="" for="exampleInputEmail2">Savings</label>

                <div class="form-group input-group">
                    {{ form_widget(goals.allocated, {'attr': {'class': 'form-control'}}) }}
                </div>

            </div>
        </div>
{% endfor %}

这是我的动作

public function prioritiseGoalsAction(Request $request)
{

    $em = $this->getDoctrine()->getManager();
    //get user id of currently logged in user
    $userId = $this->getUser()->getId();

    //get survey object of currently logged in user
    $userGoalsInfo = $em->getRepository('MyBundle:survey')->findOneByuserID($userId);

    //create the form
    $form = $this->createForm(new GoalsType(), $userGoalsInfo);
    $form->handleRequest($request);

    if ($request->isMethod('POST')) {
        if ($form->isValid()) {
            $em->persist($userGoalsInfo);
            $em->flush();
            $this->get('session')->getFlashBag()->add(
                'notice',
                'Your Goals information has been saved'
            );
            return $this->render('MyBundle:Default/dashboard:prioritise-my-goals.html.twig', array(
                'form' => $form->createView(),
            ));
        }
    }


    return $this->render('MyBundle:Default/dashboard:prioritise-my-goals.html.twig', array(
        'form' => $form->createView(),
    ));
}

在这一点上我很无能,因为我花了几个小时试图解决这个问题,我将非常感谢这方面的任何帮助。

这是一个 class 级别限制,它会在您的目标 class 的每个实例中触发,您坚持使用您的表单。

因为您要针对目标的每个实例遍历验证器中的所有对象(为什么?)class您将检查所有目标实体,这并不理想(对于 2x 实体您将检查每个实体 2x,对于 3x 实体,您将检查每个实体 3x,等等)。

请注意,此处的 $value 是您的 class 对象,因此无需查看验证器中的其他实体。

public function validate($value, Constraint $constraint)

你应该像这样写验证器(我没有检查语法):

class SavedToDateValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
            // value should already be an instance of Goal but you could put in a sanity check like

            if (!$value instanceof Goal) {

                // throw an exception or whatever
            }                

            $target = $value->getTarget();
            $savedToDate = $value->getReached();
            if ($savedToDate > $target) {
                $this->context->buildViolation($constraint->message)
                    ->setParameter('%string%', $value)
                    ->addViolation();
            }
        }
    }
}

阅读 class constraint validators

的文档

终于解决了这个问题。

  1. 创建自定义验证时 您需要访问 整个 class 您需要在您的 Constraintclass。就我而言,这是 SavedToDate 而我是 将其添加到 SavedToDateValidator 中是错误的。

    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
    
  2. 确保验证错误正确显示 在使用 collection form 时,我不得不 改进我的 validate() 自定义功能 Validator SavedToDateValidator,感谢@Richard 的提示。

    public function validate($value, Constraint $constraint)
    {
        if ($value instanceof Goals) {
            $target = $value->getTarget();
            $savedToDate = $value->getReached();
            if ($savedToDate > $target) {
                $this->context->buildViolation($constraint->message)
                    ->setParameter('%goalname%', $value->getName())
                    ->setParameter('%reached%', $value->getReached())
                    ->setParameter('%targetamount%', $value->getTarget())
                    ->atPath('reached')
                    ->addViolation();
            }
        }
    }
    

    上述函数的一个重要部分是->atPath('reached')atPath() 将错误粘贴到违规的字段 是的,我之前没有这个,这导致显示 针对所有字段的错误消息,而不是唯一针对 错误实际所属的字段。 atpath('fieldname') 中的参数是 属性 您想要 link 错误的名称。但是为了得到这个 工作你还需要关闭 error_bubbling 所以错误不会传递给父表单。

        $builder 
            ->add('goals', 'collection', array(
                  'type' => new GoalType(),
                  'allow_add' => true,
                  'by_reference' => false,
                  'error_bubbling' => false
                    ));
    

这个解决方案对我很有效,我必须承认它真的很有趣,让我很兴奋。