Symfony Form Framework – Requiring At Least One of Many Fields

In my application there are three places for the user to specify an amount, all of which constitute the final cost. No single one of these fields are required, but at least one needs to be completed for the form to validate. Unfortunately, the Form Framework allows only one field per validator. That is, unless you use the sfValidatorSchema::setPostValidator() option. This method will allow you to set a validator on your entire schema, and is how password validation works. You set the sfValidatorSchemaCompare() validator using this method, and it compares two fields to be equal.

Using this method, I created a class called sfValidatorSchemaVariableRequired that accepts an array in the constructor for all the widgets you are referring to, and also the minimum number required out of that list (default is 1). Here is an example of it used below:

    $this->validatorSchema->setPostValidator(
      new sfValidatorSchemaVariableRequired(array('college_amount', 'capital_amount', 'other_amount')));

This will check the three fields passed in the array and ensure at least 1 of them has been completed. This takes place after other validation, so you can still validate the data beforehand (for instance, I use sfValidatorInteger for all three fields).

Your form by default renders like this:

Validation Screenshot

You can of course change all of the validator messages. You can hide the global message all together. I prefer to pass a list of labels to the constructor like this:

    $this->validatorSchema->setPostValidator(
      new sfValidatorSchemaVariableRequired(
        array('college_amount', 'capital_amount', 'other_amount'),
        array('labels' => array('Annual Fund Gift Amount', 'Capital Gift Amount', 'Other Gift Amount')),
        array('global_invalid' => 'You must fill out at least %num_required% of the following fields: %labels%')));

Which will then render your error in this way:

Validation Screenshot

Here is the code for the class itself, which you can pull from github here


 */
class sfValidatorSchemaVariableRequired extends sfValidatorSchema
{
  /**
   * Constructor.
   *
   * Available options:
   *
   *  * num_required:         minimum amount of fields that must pass validation (default is 1)
   *  * field_labels:         field labels passed to the global_invalid message
   *
   * Available messages:
   *
   *  * global_invalid:       Global error thrown for the form
   *
   * @param string $fields      Array of fields to choose from.  These values are also passed to the global_invalid message
   * @param array  $options     An array of options
   * @param array  $messages    An array of error messages
   *
   * @see sfValidatorBase
   */
  public function __construct(array $fields, $options = array(), $messages = array())
  {
    $this->addOption('num_required', 1);
    $this->addOption('fields', $fields);
    $this->addOption('labels', array());
    $this->addOption('throw_global_error', true);

    $this->addMessage('invalid', 'You must fill out at least %num_required% of these fields');
    $this->addMessage('global_invalid', 'You filled out %num_valid% of %num_required% required fields below');

    parent::__construct(null, $options, $messages);
  }

  /**
   * @see sfValidatorBase
   */
  protected function doClean($values)
  {
    $valid = 0;
    foreach ($this->getOption('fields') as $field) {
      $valid += (isset($values[$field]) && $values[$field]) ? 1:0;
    }

    if ($valid < $this->getOption('num_required'))
    {
      $errorSchema = new sfValidatorErrorSchema($this);

      if ($this->getOption('throw_global_error'))
      {
        // Add global error
        $errorSchema->addError(new sfValidatorError($this, 'global_invalid', array(
            'num_required'  => $this->getOption('num_required'),
            'num_valid'     => $valid,
            'fields'        => implode(', ', $this->getOption('fields')),
            'labels'        => implode(', ', $this->getOption('labels')),
        )));
      }

      $error = new sfValidatorError($this, 'invalid', array('num_required' => $this->getOption('num_required')));

      // Add an error for each of the fields
      foreach ($this->getOption('fields') as $field)
      {
        $errorSchema->addError($error, $field);
      }

      throw $errorSchema;
    }

    return $values;
  }
}

This class only checks that at least one or more fields has a value. I can think of a few other group validators, such as having one or more fields match a passed validator, or passed individual validators. Can you think of any other useful Post Validators?

Add comment