phpsymfonysymfony-formsformcollectionsymfony-2.7

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

enter image description here

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 enter image description here

Problem 2

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

enter image description here

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.


Solution

  • 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.