Or Nothing Is Better,
The Rationale For An "Or" Validator And An "Empty" Validator

Jim Moser, June 14, 2016 (Revised September 4, 2016)

In Zend Framework 2 the Zend\InputFilter\Input class has three properties related to the validation of non-existent (unset) and empty values. These properties are named required, allowEmpty, and continueIfEmpty. These three properties have setter and getter methods (isRequired, setRequired, etc.) so I will refer to them as the required, allow empty, and continue if empty attributes.

Based upon their names I expected the attributes to affect the Input validation results as follows:

The required attribute should affect only whether an Input is required to have a value set. If required is true then the Input's isValid() method should return true only if a value is set for the Input and the validator chain is valid. If required is false, the isValid() method should return true only if either the value is not set for the Input or the validator chain is valid. The required attribute should not affect the validity of set but empty values.

The allow empty attribute should affect whether empty (but set) values are allowed, and if they are allowed then continue if empty should determine whether empty values still need to be validated by the original validator chain or are considered valid solely for being empty.

If allow empty is false then for the Input to be valid the set value must be both not empty (Zend\Validator\NotEmpty validator returns true) and valid for the original validator chain. Continue if empty should have no effect whenever allow empty is false.

If allow empty is true and continue if empty is false then for the Input to be valid the set value must either be empty or valid according to the original validator chain.

If allow empty and continue if empty are both true then for the Input to be valid the set value must simply be valid according to the original validator chain regardless of whether the value is considered empty or not.

I summarized these expectations in the table below.

Table 1. Expected validation results.
Required True True True True False False False False
Allow Empty True True False False True True False False
Continue If Empty True False True False True False True False
Value Not Set Invalid Invalid Invalid Invalid Valid Valid Valid Valid
Empty Value, Chain Invalid Invalid Valid Invalid Invalid Invalid Valid Invalid Invalid
Empty Value, Chain Valid Valid Valid Invalid Invalid Valid Valid Invalid Invalid
Non-empty Value, Chain Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid
Non-empty Value, Chain Valid Valid Valid Valid Valid Valid Valid Valid Valid

To determine whether this is actually how these attributes affect the validation of Input objects I first read the Zend\InputFilter sections of the Zend Framework 2 Reference Guide. This did not tell me much as the guide sets these attributes in the examples but does not explain in detail how they work and interact.

To get a more definitive answer I examined the source code and saw that these attributes did not seem to work exactly as I thought. To verify this I created some tests to generate a table of Input validation results to compare against my expectations shown in Table 1.

Table 2. ZF2 version 2.3.9 Input class actual validation results.
Required True True True True False False False False
Allow Empty True True False False True True False False
Continue If Empty True False True False True False True False
Value Not Set Invalid Valid Invalid Invalid Valid Valid Valid Valid
Empty Value, Chain Invalid Invalid Valid Invalid Invalid Valid Valid Valid Valid
Empty Value, Chain Valid Valid Valid Valid Invalid Valid Valid Valid Invalid
Non-empty Value, Chain Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid
Non-empty Value, Chain Valid Valid Valid Valid Valid Valid Valid Valid Valid

In Table 2 I highlighted the results that were different from what I originally expected. There were 6 results that I would have expected to be invalid that were valid.

I then searched online for an explanation. What I learned was that some of these results were unintentional and that changes were made to more recent versions of the Input class to try to resolve this.

So I installed the relevant Zend Framework 2 Composer packages for version 2.4.8, 2.5.1, and 2.73 which include changes to the Input class since version 2.3.9, and re-ran the validation tests. The results for both version 2.4.8 and 2.7.3 were the same but the results for 2.5.1 were different as shown in the following tables.

Table 3. ZF2 versions 2.4.8 and 2.7.3 Input class actual validation results.
Required True True True True False False False False
Allow Empty True True False False True True False False
Continue If Empty True False True False True False True False
Value Not Set Invalid Invalid Invalid Invalid Valid Valid Valid Valid
Empty Value, Chain Invalid Invalid Valid Invalid Invalid Invalid Valid Invalid Valid
Empty Value, Chain Valid Valid Valid Valid Invalid Valid Valid Valid Valid
Non-empty Value, Chain Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid
Non-empty Value, Chain Valid Valid Valid Valid Valid Valid Valid Valid Valid

Table 4. ZF2 version 2.5.1 Input class actual validation results.
Required True True True True False False False False
Allow Empty True True False False True True False False
Continue If Empty True False True False True False True False
Value Not Set Invalid Valid Invalid Invalid Invalid Valid Invalid Valid
Empty Value, Chain Invalid Invalid Valid Invalid Invalid Invalid Valid Invalid Valid
Empty Value, Chain Valid Valid Valid Valid Invalid Valid Valid Valid Valid
Non-empty Value, Chain Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid
Non-empty Value, Chain Valid Valid Valid Valid Valid Valid Valid Valid Valid

I think the results for versions 2.4.8 and 2.7.3 are improved with only 4 results differing from what I originally expected and the logic appearing slightly more consistent. The "Value Not Set" results are an important improvement with the results now consistently invalid if "required" is true and consistently valid if "required" is false.

I find the results for version 2.5.1 even worse than for version 2.3.9. I do not know why there was a regression there but by version 2.7.3 the results are back to where they were with version 2.4.8.

However I still do not like the results for versions 2.4.8 and 2.7.3. There are still results that appear logically incorrect to me. For example, if required = true, allow empty = false, continue if empty = true, and the value is an empty value that causes the raw original validator chain to validate, then the Input object validates. But it seems to me that if allow empty is false and the value is considered empty then the Input should be invalid regardless of what continue if empty is set to.

The source code documentation for the Input class in version 2.4.8 has the note @deprecated 2.4.8 Add Zend\Validator\NotEmpty to the ValidatorChain. added to the comments for the $allowEmpty property. The comments for the $continueIfEmpty property have the note @deprecated 2.4.8 Add Zend\Validator\NotEmpty to the ValidatorChain. Should always return true. I think this indicates that even the developers are not happy with the results and are recommending setting allow empty and continue if empty to true and manually adding the Zend\Validator\NotEmpty validator to the validator chain as needed.

However, it is not possible to duplicate all combinations of allow empty and continue if empty by setting them both to true and manually adding the NotEmpty validator. In particular it is not possible to achieve the same results I would expect when allow empty = true, and continue if empty = false. The reason is that if allow empty = true, and continue if empty = false then the Input class should in essence perform a logical OR type operation. The Input is valid if the value is empty OR if it causes the validator chain to validate. This cannot be duplicated by adding the NotEmpty validator to the validator chain because the validator chain performs a logical AND operation on its validators.

So what do I recommend as a solution?

The solution I recommend has four parts.

1. Use version 2.7.3 or higher of Zend\InputFilter. Version 2.4.8 is okay as well but due to the different results for version 2.5.1 I do not know about the other versions between 2.4.8 and 2.7.3 as I have not tested them yet.

2. Always set allow empty and continue if empty to true. This is useful because if both allow empty and continue if empty are true then the Input will validate as if these attributes did not exist. Note I only know this is true for versions 2.4.8 and 2.7.3. This will allow you to focus on adding the validators that will deliver the desired results without having to worry about the convoluted logic behind allow empty and continue if empty interfering with the validation results.

3. Use the combination of validators needed to achieve the desired validation results.

However, you will not be able to duplicate the results of allow empty = true and continue if empty = false just using combinations of existing validators within the current version of the Zend\Validator module (as of version 2.7.3). So if you want the validation results to match those of column 3 (Required=True, Allow Empty=True, Continue If Empty=False) or more likely column 7 (False, True, False) of Table 1 or 3, the third part of my solution requires you to use two validators not included in the Zend\Validator module.

4. If needed use an Empty validator in conjunction with an Or validator chain to create a validator chain that is valid if either the value is empty OR the value is valid according to the base validator.

An Empty validator is one which is valid only if the value is empty.

An Or validator chain is a validator which accepts multiple validators just like a Zend\Validator\ValidatorChain does but during validation joins them with a logical OR such that the chain is valid only if one or more of the component validators is valid.

I have created implementations of these validators and made them available on GitHub. The first one I named EmptyValidator. The second I named OrChain. I also created a VerboseOrChain validator which has the same validation logic as OrChain but allows adding validation failure messages to the chain.

If allow empty and continue if empty are both set to true you can use the OrChain validator to combine the EmptyValidator with your desired base validator or validator chain to achieve the results that would otherwise require setting allow empty = true and continue if empty = false.

In Table 5 below I show the various allow empty and continue if empty combinations versus the combinations of validators that can achieve the same results with both allow empty and continue if empty set to true. In the headings || represents joining two validators with the OrChain validator. && represents joining two validators using the Zend\Validator\ValidatorChain which joins the individual validators using a logical AND operation.

Table 5. Equivalent validator combinations.
Allow Empty True True False False
Continue If Empty True False True False
Equivalent Validator Combination Chain Empty || Chain NotEmpty && Chain NotEmpty && Chain

In Table 6 below I show the validation results for various combinations of the required attribute and validators with both allow empty and continue if empty set to true. Note that the results are the same as my original expected results listed in Table 1 and not the unexpected results shown in Tables 2 and 3.

Table 6. Validation results vs. various validation combinations with both allow empty and continue if empty true.
Required True True True True False False False False
Validator Combination Chain Empty || Chain NotEmpty && Chain NotEmpty && Chain Chain Empty || Chain NotEmpty && Chain NotEmpty && Chain
Value Not Set Invalid Invalid Invalid Invalid Valid Valid Valid Valid
Empty Value, Chain Invalid Invalid Valid Invalid Invalid Invalid Valid Invalid Invalid
Empty Value, Chain Valid Valid Valid Invalid Invalid Valid Valid Invalid Invalid
Non-empty Value, Chain Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid Invalid
Non-empty Value, Chain Valid Valid Valid Valid Valid Valid Valid Valid Valid

Example Usage

The code below uses an OrChain validator chain to join a StringLength validator with an EmptyValidator. The Input that is created will be valid if either it does not contain a value (required=false), the value is empty, OR the value is more than 5 characters long.


use JimMoser\Validator\EmptyValidator;
use JimMoser\Validator\OrChain;
use Zend\InputFilter\Input;
use Zend\Validator\StringLength;
use Zend\Validator\ValidatorChain;

$validator = new StringLength(array('min' =>5));
$emptyValidator = new EmptyValidator();

$orChain = new OrChain();
$orChain->attach($emptyValidator)->attach($validator);

$chain = new ValidatorChain();
$chain->attach($orChain); 

$input = new Input();
$input->setName('nick_name')
      ->setRequired(false)
      ->setAllowEmpty(true)
      ->setContinueIfEmpty(true)
      ->setValidatorChain($chain);

Conclusion

The "required", "allow empty", and "continue if empty" attributes of the Zend\InputFilter\Input class in older versions of Zend Framework 2 affected validation results in unexpected ways. In more recent versions the "required" attribute works as expected. The "allow empty" and "continue if empty" attributes affect validation results differently in the new versions than in the older versions but I still think they affect validation results in manners inconsistent with their names. Fortunately in the newer versions, if both "allow empty" and "continue if empty" are set to true then these two attributes do not affect the validation results which allows a workaround.

I think the best solution to avoid unexpected validation results is to:

  1. Use a newer version of Zend\InputFilter\Input. (Version 2.4.8 or 2.7.3 or later.)
  2. Set both "allow empty" and "continue if empty" to true.
  3. Use combinations of the Zend\Validator\NotEmpty, Zend\Validator\ValidatorChain, JimMoser\Validator\EmptyValidator, and JimMoser\Validator\OrChain validators to get the validation results you previously might have attempted to use the "allow empty" and "continue if empty" attributes to achieve.

In most cases you will not need the JimMoser\Validator\OrChain or JimMoser\Validator\EmptyValidator classes but they allow you to specify that a value is to be either empty OR valid according to some validator. This fills in a gap allowing you to use validators to achieve the results of all combinations of "allow empty" and "continue if empty".

Even without the unexpected and inconsistent results from the "allow empty" and "continue if empty" attributes, I believe using validators to "replace" these two attributes is a cleaner solution because it essentially removes validation rules from the Input class and places them in validators where they belong.