Callback
- a wrapper around callable
This rule allows validation of the current attribute value (but not limited to it) with an arbitrary condition within a
callable. The benefit is that there is no need to create a separate custom rule and handler.
A condition can be within:
- Standalone callable function.
- Callable class.
- DTO (data transfer object) method.
The downside of using standalone functions and DTO methods is a lack of reusability. So they are mainly useful for some specific non-repetitive conditions. Reusability can be achieved with callable classes, but depending on other factors (the need for additional parameters for example), it might be a good idea to create a full-fledged custom rule with a separate handler instead.
The callback function signature is the following:
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\ValidationContext;
function (mixed $value, Callback $rule, ValidationContext $context): Result;
where:
$value
is the validated value;$rule
is a reference to the originalCallback
rule;$context
is a validation context;- returned value is a validation result instance with or without errors.
Using as a function
An example of passing a standalone callable function to a Callback
rule:
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\ValidationContext;
new Callback(
static function (mixed $value, Callback $rule, ValidationContext $context): Result {
// The actual validation code.
return new Result();
},
);
Examples
Value validation
Callback
rule can be used to add validation missing in built-in rules for a single attribute's value. Below is the example verifying that a value is a valid YAML string (additionally requires yaml
PHP extension):
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
new Callback(
static function (mixed $value): Result {
if (!is_string($value)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($value);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
},
);
Note:
Processing untrusted user input with yaml_parse()
can be dangerous with certain settings. Please refer to yaml_parse docs for more details.
Usage of validation context for validating multiple attributes depending on each other
In the example below, the 3 angles are validated as degrees to form a valid triangle:
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\Rule\Integer;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\ValidationContext;
$rules = [
'angleA' => [
new Required(),
new Integer(),
],
'angleB' => [
new Required(),
new Integer(),
],
'angleC' => [
new Required(),
new Integer(),
],
new Callback(
static function (mixed $value, Callback $rule, ValidationContext $context): Result {
$angleA = $context->getDataSet()->getAttributeValue('angleA');
$angleB = $context->getDataSet()->getAttributeValue('angleB');
$angleC = $context->getDataSet()->getAttributeValue('angleC');
$sum = $angleA + $angleB + $angleC;
if ($sum <= 0) {
return (new Result())->addError('The angles\' sum can\'t be negative.');
}
if ($sum > 180) {
return (new Result())->addError('The angles\' sum can\'t be greater than 180 degrees.');
}
return new Result();
}
),
];
Replacing boilerplate code with separate rules and when
However, some cases of using validation context can lead to boilerplate code:
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\ValidationContext;
static function (mixed $value, Callback $rule, ValidationContext $context): Result {
if ($context->getDataSet()->getAttributeValue('married') === false) {
return new Result();
}
$spouseAge = $context->getDataSet()->getAttributeValue('spouseAge');
if ($spouseAge === null) {
return (new Result())->addError('Spouse age is required.');
}
if (!is_int($spouseAge)) {
return (new Result())->addError('Spouse age must be an integer.');
}
if ($spouseAge < 18 || $spouseAge > 100) {
return (new Result())->addError('Spouse age must be between 18 and 100.');
}
return new Result();
};
They can be rewritten using multiple rules and conditional validation making code more intuitive. We can use built-in rules where possible:
use Yiisoft\Validator\Rule\BooleanValue;
use Yiisoft\Validator\Rule\Integer;
use Yiisoft\Validator\ValidationContext;
$rules = [
'married' => new BooleanValue(),
'spouseAge' => new Integer(
min: 18,
max: 100,
when: static function (mixed $value, ValidationContext $context): bool {
return $context->getDataSet()->getAttributeValue('married') === true;
},
),
];
Using as an object's method
For property
When using as a PHP attribute, set an object's method as a callback instead:
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
final class Config {
public function __construct(
#[Callback(method: 'validateYaml')]
private string $yaml,
) {
}
private function validateYaml(mixed $value): Result
{
if (!is_string($value)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($value);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
}
}
The signature is the same as in a regular function. Note that there are no restrictions on visibility levels and static modifiers, all of them can be used (public
, protected
, private
, static
).
Using a callback
argument instead of method
with PHP attributes is prohibited due to current PHP language restrictions (a callback can't be inside a PHP attribute).
For the whole object
It's also possible to check the whole object:
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
#[Callback(method: 'validate')]
final class Config {
public function __construct(
private int $yaml,
) {
}
private function validate(): Result
{
if (!is_string($this->yaml)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($this->yaml);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
}
}
Note the use of property value ($this->yaml
) instead of method argument ($value
).
Using a callable class
A class that implements __invoke()
can also be used as a callable:
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\ValidationContext;
final class YamlCallback
{
public function __invoke(mixed $value): Result
{
if (!is_string($value)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($value);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
}
}
The signature is the same as in a regular function.
Using in rules (note that a new instance must be passed, not a class name):
use Yiisoft\Validator\Rule\Callback;
$rules = [
'yaml' => new Callback(new YamlCallback()),
];
Shortcut for use with validator
When using with the validator and default Callback
rule settings, a rule declaration can be omitted, so just including a callable is enough. It will be normalized automatically before validation:
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Validator;
$data = [];
$rules = [
'yaml' => static function (mixed $value): Result {
if (!is_string($value)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($value);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
},
];
$result = (new Validator())->validate($data, $rules);
Or it can be set within an array of other rules:
use Exception;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\Validator;
$data = [];
$rules = [
'yaml' => [
new Required(),
static function (mixed $value): Result {
if (!is_string($value)) {
return (new Result())->addError('The value must be a string.');
}
$notYamlMessage = 'This value is not a valid YAML.';
try {
$data = yaml_parse($value);
} catch (Exception $e) {
return (new Result())->addError($notYamlMessage);
}
if ($data === false) {
return (new Result())->addError($notYamlMessage);
}
return new Result();
},
],
];
$result = (new Validator())->validate($data, $rules);
Back to Yii Validator