Validate input using zend-validator
In our article Filter input using zend-filter, we covered filtering data. The filters in zend-filter are generally used to pre-filter or normalize incoming data. This is all well and good, but we still don't know if the data is valid. That's where zend-validator comes in.
Installation
To install zend-validator, use Composer:
$ composer require zendframework/zend-validator
Like zend-filter, the only required dependency is zend-stdlib. However, a few other components are suggested, based on which filters and/or features you may want to use:
- zendframework/zend-servicemanager is used by the
ValidatorPluginManager
andValidatorChain
to look up validators by their short name (versus fully qualified class name), as well as to allow usage of validators with dependencies. - zendframework/zend-db is used by a pair of validators that can check if a matching record exists (or does not!).
- zendframework/zend-uri is used by the
Uri
validator. - The CSRF validator requires both zendframework/zend-math and zendframework/zend-session.
- zendframework/zend-i18n and zendframework/zend-i18n-resources can be installed in order to provide translation of validation error messages.
For our examples, we'll be using the ValidatorChain
functionality with a
ValidatorPluginManager
, so we will also want to install zend-servicemanager:
$ composer require zendframework/zend-servicemanager
ValidatorInterface
The current incarnation of zend-validator is stateful; validation error
messages are stored in the validator itself. As such, validators must implement
the ValidatorInterface
:
namespace Zend\Validator;
interface ValidatorInterface
{
/**
* @param mixed $value
* @return bool
*/
public function isValid($value);
/**
* @return array
*/
public function getMessages();
}
The $value
can be literally anything; a validator examines it to see if it is
valid, and returns a boolean result. If it is invalid, a subsequent call to
getMessages()
should return an associative array with the keys being message
identifiers, and the values the human-readable message strings.
As such, usage looks like the following:
if (! $validator->isValid($value)) {
// Invalid value
echo "Failed validation:\n";
foreach ($validator->getMessages() as $message) {
printf("- %s\n", $message);
}
return false;
}
// Valid value!
return true;
Stateless validations are planned
At the time of writing, we have proposed1 a new validation component to work in parallel with zend-validator; this new component will implement a stateless architecture. Its proposed validation interface will no longer return a boolean, but rather a
ValidationResult
. That instance will provide a method for determining if the validation was successful, encapsulate the value that was validated, and, for invalid values, provide access to the validation error messages. Doing so will allow better re-use of validators within the same execution process.This proposal also includes code for adapting existing zend-validator implementations to work with the stateless design.
zend-validator provides a few dozen filters for common operations, including things like:
- Common conditionals like
LessThan
,GreaterThan
,Identical
,NotEmpty
,IsInstanceOf
,InArray
, andBetween
. - String values, such as
StringLength
,Regex
. - Network-related values such as
Hostname
,Ip
,Uri
, andEmailAddress
. - Business values such as
Barcode
,CreditCard
,GpsPoint
,Iban
, andUuid
. - Date and time related values such as
Date
,DateStep
, andTimezone
.
Any of these validators may be used by themselves.
In many cases, though, your validation may be related to a set of validations: as an example, the value must be non-empty, a certain number of characters, and fulfill a regular expression. Like filters, zend-validator allows you to do this with chains.
ValidatorChain
Usage of a validator chain is similar to filter chains: attach validators you want to execute, and then pass the value to the chain:
use Zend\Validator;
$validator = new Validator\ValidatorChain();
$validator->attach(new Validator\NotEmpty());
$validator->attach(new Validator\StringLength(['min' => 6]));
$validator->attach(new Validator\Regex('/^[a-f0-9]{6,12}$/');
if (! $validator->isValid($value)) {
// Failed validation
var_dump($validator->getMessages());
}
The above uses validator instances, eliminating the need for
ValidatorPluginManager
, and thus avoids usage of zend-servicemanager. However,
if we have zend-servicemanager installed, we can replace usage of attach()
with attachByName()
:
use Zend\Validator;
$validator = new Validator\ValidatorChain();
$validator->attachByName('NotEmpty');
$validator->attachByName('StringLength', ['min' => 6]);
$validator->attachByName('Regex', ['pattern' => '/^[a-f0-9]{6,12}$/']);
if (! $validator->isValid($value)) {
// Failed validation
var_dump($validator->getMessages());
}
Breaking the chain
If you were to run either of these examples with $value = ''
, you may discover
something unexpected: you'll get validation error messages for every single
validator! This seems wasteful; there's no need to run the StringLength
or
Regex
validators if the value is empty, is there?
To solve this problem, when attaching a validator, we can tell the chain to break execution if the given validator fails. This is done by passing a boolean flag:
- as the second argument to
attach()
- as the third argument to
attachByName()
(the second argument is an array of constructor options)
Let's update the second example:
use Zend\Validator;
$validator = new Validator\ValidatorChain();
$validator->attachByName('NotEmpty', [], $breakChainOnFailure = true);
$validator->attachByName('StringLength', ['min' => 6], true);
$validator->attachByName('Regex', ['pattern' => '/^[a-f0-9]{6,12}$/']);
if (! $validator->isValid($value)) {
// Failed validation
var_dump($validator->getMessages());
}
The above adds a boolean true
as the $breakChainOnFailure
argument to the
attachByName()
method calls of the NotEmpty
and StringLength
validators
(we had to provide an empty array of options for the NotEmpty
validator so we
could pass the flag). In these cases, if the value fails validation, no further
validators will be executed.
Thus:
$value = ''
will result in a single validation failure message, produced by theNotEmpty
validator.$value = 'test'
will result in a single validation failure message, produced by theStringLength
validator.$value = 'testthis'
will result in a single validation failure message, produced by theRegex
validator.
Prioritization
Validators are executed in the same order in which they are attached to the
chain by default. However, internally, they are stored in a PriorityQueue
;
this allows you to provide a specific order in which to execute the validators.
Higher values execute earlier, while lower values (including negative values)
execute last. The default priority is 1.
Priority values may be passed as the third argument to attach()
and fourth
argument to attachByName()
.
As an example:
$validator = new Validator\ValidatorChain();
$validator->attachByName('StringLength', ['min' => 6], true, 1);
$validator->attachByName('Regex', ['pattern' => '/^[a-f0-9]{6,12}$/'], false, -100);
$validator->attachByName('NotEmpty', [], true, 100);
In the above, when executing the validation chain, the order will still be
NotEmpty
, followed by StringLength
, followed by Regex
.
Why prioritize?
Why would you use this feature? The main reason is if you want to define validation chains via configuration, and cannot guarantee the order in which the items will be present in configuration. By adding a priority value, you can ensure that recreation of the validation chain will preserve the expected order.
Context
Sometimes we may want to vary how we validate a value based on whether or not
another piece of data is present, or based on that other piece of data's value.
zend-validator offers an unofficial API for that, via an optional $context
value you can pass to isValid()
. The ValidatorChain
accepts this value, and,
if present, will pass it to each validator it composes.
As an example, let's say you want to capture an email address (form field "contact"), but only if the user has selected a radio button allowing you to do so (form field "allow_contact"). We might write that validator as follows:
use ArrayAccess;
use ArrayObject;
use Zend\Validator\EmailAddress;
use Zend\Validator\ValidatorInterface;
class ContactEmailValidator implements ValidatorInterface
{
const ERROR_INVALID_EMAIL = 'contact-email-invalid';
/** @var string */
private $contextVariable;
/** @var EmailAddress */
private $emailValidator;
/** @var string[] */
private $messages = [];
/** @var string[] */
private $messageTemplates = [
self::ERROR_INVALID_EMAIL => 'Email address "%s" is invalid',
];
public function __construct(
EmailAddress $emailValidator = null,
string $contextVariable = 'allow_contact'
) {
$this->emailValidator = $emailValidator ?: new EmailAddress();
$this->contextVariable = $contextVariable;
}
public function isValid($value, $context = null)
{
$this->messages = [];
if (! $this->allowsContact($context)) {
// Value will be discarded, so always valid.
return true;
}
if ($this->emailValidator->isValid($value)) {
return true;
}
$this->messages[self::ERROR_INVALID_EMAIL] = sprintf(
$this->messageTemplates[self::ERROR_INVALID_EMAIL],
var_export($value, true)
);
return false;
}
public function getMessages()
{
return $this->messages;
}
private function allowsContact($context) : bool
{
if (! $context ||
! (is_array($context)
|| $context instanceof ArrayObject
|| $context instanceof ArrayAccess)
) {
return false;
}
$allowsContact = $context[$this->contextVariable] ?? false;
return (bool) $allowsContact;
}
}
We would then add it to the validator chain, and call it like so:
$validator->attach(new ContactEmailValidator());
if (! $validator->isValid($data['contact'], $data)) {
// Failed validation!
}
This approach can allow for some quite complex validation routines, particularly if you nest validation chains within custom validators!
Registering your own validators.
If you write your own validators, chances are you'll want to use them with the
ValidatorChain
. This class composes a ValidatorPluginManager
, which is a
plugin manager built on top of zend-servicemanager. As such, you can register
your validators with it:
$plugins = $validator->getPluginManager();
$plugins->setFactory(ContactEmailValidator::class, ContactEmailValidatorFactory::class);
$plugins->setService(ContactEmailValidator::class, $contactEmailValidator);
Alternately, if using zend-mvc or Expressive, you can provide configuration via
the validators
configuration key:
return [
'validators' => [
'factories' => [
ContactEmailValidator::class => ContactEmailValidatorFactory::class,
],
],
];
If you want to use a "short name" to identify your validator, we recommend using an alias, aliasing the short name to the fully qualified class name.
Wrapping up
Between using zend-filter to normalize and pre-filter values, and zend-validator to validate the values, you can start locking down the input your users submit to your application.
That said, what we've demonstrated so far is how to work with single values. Most forms submit sets of values; using the approaches so far can lead to a lot of code!
We have a solution for this as well, via our zend-inputfilter component. Read the article Validate data using zend-inputfilter for more information.
Footnotes
1. https://discourse.zendframework.com/t/rfc-new-validation-component/208/ ↩