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.
Finally I was able to resolve the problem.
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;
}
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.