doremi
doremi

Reputation: 15329

Building Symfony 2 Custom Validator that Uses Multiple Fields

I'm building a custom validator that needs to validate the value from TWO form fields in the db in order to get this constraint to pass.

My question is this: the ContractValidator's validate method only has one $value in it's signature so how do I get access to the values from more than just a single field to do the validation?

Here is a typical custom validator:

namespace Acme\WebsiteBundle\Validator\Constraints;

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

class MyCustomValidator extends ConstraintValidator
{
  public function validate($value, Constraint $constraint)
  {
    // check $value and return an error
    // but in my case, i want the value from more than one form field to do a validation
    // why? i'm checking that two pieces of information (ssn + dob year) match
    // the account the user is registering for
  }
}

Here's an example of a form class with some validations set:

namespace ACME\WebsiteBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\MinLength;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use ACME\WebsiteBundle\Validator\Constraints\UsernameAvailable;

class AccountRegistration extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder->add('ssn', 'number', array(
      'max_length' => 9, 
      'required' => true,
      'error_bubbling' => true)
    );

    $builder->add('year_of_birth', 'choice', array(
      'choices'  => range(date("Y") - 100, date("Y")),
      'required' => true,
      'empty_value' => 'Select ...',
      'label'    => 'Year of Birth',
      'error_bubbling' => true)
    );

    $builder->add('username', 'text', array(
      'required' => true,
      'error_bubbling' => true)
    );

    $builder->add('password', 'password', array(
      'max_length' => 25,
      'required' => true,
      'error_bubbling' => true)
    );

    $builder->add('security_question', 'choice', array(
      'empty_value' => 'Select ...',
      'choices' => array(),
      'label' => 'Security Question',
      'required' => true,
      'error_bubbling' => true)
    );

    $builder->add('security_question_answer', 'text', array(
      'label' => 'Answer',
      'required' => true,
      'error_bubbling' => true)
    );
  }

  public function getName()
  {
    return 'account_registration';
  }

  public function getDefaultOptions(array $options)
  {

    $collectionConstraint = new Collection(array(
      'allowExtraFields' => true, 
      'fields' => array(
        'ssn'  => array(new MinLength(array('limit' => 9, 'message' => 'too short.')), new NotBlank()),
        'year_of_birth' => array(new NotBlank()),
        'username' => array(new NotBlank(), new UsernameAvailable()),
        'password' => array(new NotBlank(), new Regex(array(
          'message' => 'password must be min 8 chars, contain at least 1 digit',
          'pattern' => "((?=.*\d)(?=.*[a-z]).{8,25})"))
        ),
        'security_question' => array(new NotBlank()),
        'security_question_answer' => array(new NotBlank()))
      )
    );

    return array(
        'csrf_protection' => true,
        'csrf_field_name' => '_token',
        'intention'       => 'account_registration',
        'validation_constraint' => $collectionConstraint
    );
  }  
}

Upvotes: 21

Views: 12253

Answers (2)

b.b3rn4rd
b.b3rn4rd

Reputation: 8830

Any custom validator that extends ConstraintValidator has access to the $context property. $context is an instance of ExecutionContext that gives you access to submitted data:

Example:

<?php

namespace My\Bundle\MyBundle\Validator\Constraints;

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


class AppointorRoleValidator extends ConstraintValidator
{

    public function validate($value, Constraint $constraint)
    {
        $values = $this->context->getRoot()->getData();
        /* ... */
    }
}

Upvotes: 33

Sgoettschkes
Sgoettschkes

Reputation: 13189

You need to use CLASS_CONSTRAINT as described in the cookbooks. You then get passed the whole class and can use any property of this class. In your example above, this would mean that instead of the $value beeing one string/integer, it would be the whole object you want to validate.

The only thing you need to change is getTargets() functions, which has to return self::CLASS_CONSTRAINT.

Also make sure that you define your validator on class level, not on property level. If you use annotations this means that the validator must be described above the class defnition, not above one specific attribute definition:

/**
  * @MyValidator\SomeValidator
  */
class MyClass {

}

Upvotes: 4

Related Questions