Validating empty fields

Validating an optional field is a snap with CakePHP’s allowEmpty. But what if the field is only optional sometimes and under certain circumstances?

A custom validation method, right? Well, actually no. See, if a field is optional (or, more accurately, 'allowEmpty' => true) then CakePHP will ignore any validation method, custom or otherwise. As it should, because, you know, the field is optional. Optional + empty = ignore.

That said, removing the allowEmpty declaration means the field is no longer optional. And while our custom validation method is now technically in the logic flow, we’ll never reach it because we’ll never get past the field being empty, which it no longer can be because it’s, you know, required.

It’s enough to drive a man to drink.

The source of all this hand-wringing isn’t even CakePHP, it’s HTML5. As of v2.3, CakePHP now adds the HTML5 required property to all input fields for which 'allowEmpty' is not explicitly declared as false. And if that property is there and the field is empty, your browser (assuming you’re using a modern one) will barf out a generic “Please fill out this field” error on form submission, keeping all the action frustratingly client-side and away from your custom validation method.

The solution is simple, if completely unintuitive: Add 'required' => false to the element declaration in its view. CakePHP won’t add the required property and your custom validation method will be invoked when the form is submitted.

You could also turn off all HTML5 client-side validation for a particular form (more info here) but that seems a bit like swatting a fly with an atom bomb.

Time for an example. Let’s assume we’re collecting addresses. They’ll be either domestic US or international. If the former, a zip code is required; if the latter it’s optional. And lastly, all submitted zip codes need to be 5 digits long (i.e. numbers only).

We take our validated optional field and add to it a reference to our custom validation method, like so:

<!-- file: /app/Model/Address.php -->
public $validate = array(
   'zip' => array(
      'validateZip' => array(
         'rule' => array('checkStateZip'), // INVOKE CUSTOM VALIDATION METHOD
         'message' => 'Please provide the zip code for US addresses.'
      ),
      'numeric' => array(
         'rule' => array('numeric'),
         'message' => 'Zip codes must be numbers only.',
         'allowEmpty' => true
      ),
      'length' => array(
         'rule' => array('minLength', 5),
         'message' => 'Zip codes must be at least 5 digits long.'
      )
   )
);

// OUR CUSTOM VALIDATION METHOD INVOKED ABOVE
public function checkStateZip($check) {
   $state = $this->data['Address']['state'];
   if ($state == 'none' || ($state != 'none' && $this->data['Address']['zip'])) {
      return true;
   }
   return false;
}

The validateZip rule is invoked first. Because it does not explicitly declare 'allowEmpty' => false CakePHP will add a required='required' property to the input element unless we tell it not to:

<!-- file: /app/View/Addresses/add.ctp -->
<?php
echo $this->Form->input('zip', array(
   'after' => 'All US-based addresses require a zip code.',
   'required' => false
));
?>

When we reload the form, there’s no longer a red asterisk next to the zip code field and HTML5 permits the form to be submitted with that field empty. And yet, CakePHP will evaluate whether that field should be empty based on the states dropdown, via our checkStateZip custom validation method.

Now it’s time for that drink.