简体   繁体   中英

Symfony unable to validate collection form

I am working on a collection form which is called goals, a user can add as many goals as they want, this part is working fine, i am able to show/add/edit/delete goals just fine

在此处输入图片说明

Problem I am having is how to validate the data. On a form there is a goal target (integer) field and saved to date (integer) field.

The rule is the value of saved to date cannot be more than goal target and for this I have created the custom validation and that class is being picked when a form is submitted.

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

From reading the symfony documentation it seems I need to add the constraint Valid which I have inside validation.yml .

goals:
    - Valid:

Problem 1

Suppose when I enter saved to date which is greater than goal target against the first goal, instead of getting the error only against that goal i get the error against both goals.

NOTE The second error should not be there as 8000 is less than 20000 在此处输入图片说明

Problem 2

Suppose against both goals I give saved to date greater than goal target I then see 2 errors against each field.

在此处输入图片说明

This is my view template

{% 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 %}

This is my Action

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

At this point I am pretty clueless as I have spent hours trying to resolve this, I will really appreciate any help in this.

That's a class level constraint and it will fire for every instance of your goal class you persist from your form.

Because you're iterating through all your objects in the validator (why?) for each instance of your goal class you will check all of your goal entities, which isn't ideal (for 2x entity you will check each entity 2x, for 3x entity you will check each entity 3x, etc).

Note that $value here is your class object so there is no need to look at other entities in the validator.

public function validate($value, Constraint $constraint)

You should write the validator something like (I have not checked syntax):

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

Have a read of the documentation for class constraint validators

Finally I was able to resolve the problem.

  1. When creating a custom validations and you need access to the entire class you need to add the following piece of code in your Constraint class. In my case this is SavedToDate and I was adding it in SavedToDateValidator which was wrong.

     public function getTargets() { return self::CLASS_CONSTRAINT; } 
  2. To make sure the validation errors appear correctly against the fields while working with the collection form , I had to improve my validate() function of custom Validator SavedToDateValidator , thanks to @Richard for the tip.

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

    One of the important part of above function is ->atPath('reached') this atPath() sticks the error to the field where the violation is, I did not have this earlier and that was leading to displaying of error messages against all fields rather the only against the field where the error actually belonged to. The parameter inside the atpath('fieldname') is property name that you want to link the error with. But in order to get this to work you also need to turn off error_bubbling so the errors are not passed to the parent form.

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

This solution worked for me and I must admit it was really fun working on it, kept me excited.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM