1

I'm a Symfony noob trying unsuccessfully to visualize how best to validate a form field based on either it or a different field. The case: a form will solicit either a date of birth or an age. If the dob is entered, age is ignored. If age is entered and dob is empty, the dob is said to be today's date less age in years. If neither is entered a validation error is thrown. I've accomplished this with Smarty validation; as a learning exercise I'm trying to reproduce the application in Symfony.

I've looked at this solution where both fields are properties of an entity. In my case age is not, only dob. So it's not clear to me how to apply that solution. I'd greatly appreciate pointers.

Thanks.

George PS: (edited: dreck removed) PPS: (edited: removed to make room for nearly working version) Form:

// src\Mana\AdminBundle\Resources\views\Form\Type\NewClientType.php
namespace Mana\AdminBundle\Form\Type;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Mana\AdminBundle\Validator\Constraints;

class NewClientType extends AbstractType {

    public function setDefaultOptions(OptionsResolverInterface $resolver) {
        $resolver->setDefaults(array('validation_groups' => 'client_new', 
            'validation_constraint' => new DOBorAge(),
            ));
     }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add('fname', null, array('required' => false));
        $builder->add('sname', null, array('required' => false));
        $builder->add('dob', 'birthday', array('widget' => 'single_text', 'required' => false));
        $builder->add('age', null, array('mapped' => false, 'required' => false));
    }

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

services:

services:
  client_new:
    class: Mana\AdminBundle\Validator\Constraints\DOBorAgeValidator
    scope: request
    tags:
      - { name: validator.constraint_validator, alias: dobage_validator}

Validators:

// src\Mana\AdminBundle\Form\Type\DOBorAge.php
namespace Mana\AdminBundle\Form\Type;
use Mana\AdminBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

class DOBorAge extends Constraint {

    public $message = 'Either a date of birth or age must be present';

    public function validatedBy() {
        return 'dobage_validator';
    }

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

}

and

// src\Mana\AdminBundle\Validator\Constraints\DOBorAgeValidator.php
namespace Mana\AdminBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class DOBorAgeValidator extends ConstraintValidator {

    protected $request;

    public function __construct(Request $request) {
        $this->request = $request;
    }

    public function validate($value, Constraint $constraint) {
        var_dump($this->request->request->get('client'));
        die();
        return true;
    }
}
Community
  • 1
  • 1
geoB
  • 4,578
  • 5
  • 37
  • 70

2 Answers2

1

I think that the best way to manage this kind of validation is to add a customized validation constraint to your form.

Just add a class that extend Symfony\Component\Validator\Constraint and override the validatedBy() method. You can then specify a customized validator that you've to create so that it contains all the logic of your form validation.

This customized validator should extend Symfony\Component\Validator\ConstraintValidator class and implement the isValid() method for Symfony 2.0.x or validate() method for Symfony 2.1.x.

You can then Check your values, compare them and do whatever you want inside this method and add eventually add violations if one or many of your validation rules aren't respected.

Also check Symfony2 validation based on two fields.

Community
  • 1
  • 1
Ahmed Siouani
  • 13,701
  • 12
  • 61
  • 72
  • Thanks for your reply. In both Custom Validator and Callback it is not at all obvious how one gets both form values into the validator. Can you clarify? I am sufficiently noob as not to understand how to apply either of those examples. – geoB Nov 14 '12 at 21:35
  • @geoB Try to create a validation constraint step by step according to the [documentation](http://symfony.com/doc/current/cookbook/validation/custom_constraint.html). Also, check the [Class validation constraint](http://symfony.com/doc/current/cookbook/validation/custom_constraint.html#class-constraint-validator) section of the documentation, by using it you can get all the class properties, compare them and apply whatever validation logic you want. – Ahmed Siouani Nov 14 '12 at 22:46
  • Tried that. Ended up with fatal error and no clear indication of how to proceed. One of my concerns is that age is not a member of the Clients class, so where does it's property come from? It's an unmapped field on the form. Does that mean it's still available? Are there any more complete examples than what's in the Cookbook? I'll go ahead and post my code as a PS to my question - that'll give you something to shoot at! g – geoB Nov 14 '12 at 23:13
  • Thanks immensely for this! I'll see what I can do with this. Probably take me a while - I am truly a novice with Symfony. But I have been occasionally successful in cobbling together some usable code. Will report back regardless of success in a day or two. g – geoB Nov 15 '12 at 01:56
  • @geoB I tested the code snippets I wrote for your case, it works fine. So, just let me know if you've some troubles implementing it. – Ahmed Siouani Nov 15 '12 at 09:39
  • Ahmed: I've removed error-generating code and replaced it with code that generates a form and performs some conventional validation. Netbeans IDE breakpoints are not observed when placed in either DOBorAge or DOBorAgeValidator. The code compiles in dev environment but appears not to be used. Now what? And thanks! George – geoB Nov 17 '12 at 14:20
  • These classes are called when you perform a $form->isValid() call. What's the way you tested it? It should work, so if you still have troubles, add me for live chat, I'll help you fixing your implementation > ahmed.siouani@gmail.com – Ahmed Siouani Nov 17 '12 at 16:47
  • With no response to e-mail address above I created the kluge below. – geoB Nov 23 '12 at 19:20
1

A Symfony-esque solution: a single form field that takes either a date or an age, then transform the entry into a date. (Now, if I could only turn this into a custom field...)

namespace Mana\AdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Mana\AdminBundle\Form\DataTransformer\AgeToDOB;

class NewClientType extends AbstractType {

    public function buildForm(FormBuilderInterface $builder, array $options) {

        $transformer = new AgeToDOB();

        $builder->add('fname', null, array('required' => false, 
            'invalid_message' => 'First name not be blank',));
        $builder->add('sname', null, array('required' => false,
            'invalid_message' => 'Last name not be blank',));
        $builder->add(
                $builder->create('dob', 'text', array('required' => false,
            ))->addModelTransformer($transformer));
    }

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

and the transformer:

namespace Mana\AdminBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Mana\AdminBundle\Entity\Client;

class AgeToDOB implements DataTransformerInterface {

    public function reverseTransform($dob) {

        if (null == $dob) {
            return '';
        }
        if ((substr_count($dob, '/') == 2 && strtotime($dob))) {
            $date = new \DateTime($dob);
            return date_format($date, 'Y-m-d');
        }
        if (is_numeric($dob)) {
            $date = new \DateTime();
            $interval = 'P' . $dob . 'Y';
            $date->sub(new \DateInterval($interval));
            return date_format($date, 'Y-m-d');
        }
    }

    public function transform($client) {
        if (null == $client) {
            return '';
        }
        if (is_object($client)) {
            $dob = $client->getDob();
            // date: test for two / characters
            if ((substr_count($dob, '/') == 2 && strtotime($dob))) {
                $date = new \DateTime($dob);
                return date_format($date, 'm/d/Y');
            }
            if (is_numeric($dob)) {
                $date = new \DateTime();
                $interval = 'P' . $dob . 'Y';
                $date->sub(new \DateInterval($interval));
                return date_format($date, 'm/d/Y');
            }
        }
    }

}
geoB
  • 4,578
  • 5
  • 37
  • 70