Собственные правила и валидаторы
Table of Contents
Иногда встроенных валидаторов и правил недостаточно для реализации специфической бизнес-логики. В таких случаях Bitrix Framework позволяет расширять возможности системы создавая собственные валидаторы и правила.
Валидатор
Валидатор выполняет простую задачу - проверяет значение. Он не определяет, относится ли значение к свойству или классу, и не зависит от атрибутов.
Создать валидатор очень просто: создайте класс реализующий интерфейс \Bitrix\Main\Validation\Validator\ValidatorInterface с публичный методом validate(mixed $value): ValidationResult.
Вы можете добавить в конструктор класса необходимые параметры, если ваш валидатор это подразумевает.
В методе validate создать объект ValidationResult, который будет хранить результаты проверки.
Выполнить необходимые проверки и добавить в ValidationResult ошибки.
Пример валидатора для определения является ли значение UUID версии 4:
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\ValidationResult;
use Bitrix\Main\Validation\Validator\ValidatorInterface;
class UUIDv4Validator implements ValidatorInterface
{
public function validate(mixed $value): ValidationResult
{
$result = new ValidationResult();
if (
!is_string($value)
|| preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $value) !== 1
) {
$result->addError(new ValidationError(
message: Loc::getMessage('FUSION_VALIDATION_VALIDATOR_UUIDV4_NOT_VALID'),
failedValidator: $this
));
return $result;
}
return $result;
}
}
В разработке валидаторов старайтесь придерживаться правила fail fast - не ждите пока будут выполнены все проверки, если хотя бы один из критериев не соответствует добавляйте ошибку и возвращайте не успешный результат валидации.
Правила
В отличии от валидаторов правила не так универсальны и просты, поскольку зависят от применения: к свойствам и к классу. В Bitrix Framework для этого требуется реализация разных интерфейсов.
Правила для свойства
Правило для свойства реализует интерфейс Bitrix\Main\Validation\Rule\PropertyValidationAttributeInterface с публичным методом validateProperty(mixed $propertyValue): ValidationResult;.
Пример простого правила для свойства объекта:
use Attribute;
use Bitrix\Main\Validation\Rule\PropertyValidationAttributeInterface;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\ValidationResult;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class UUIDv4 implements PropertyValidationAttributeInterface
{
public function validateProperty(mixed $propertyValue): ValidationResult
{
$result = new ValidationResult();
if (
!is_string($propertyValue)
|| preg_match('/^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i', $propertyValue) !== 1
) {
$result->addError(new ValidationError('Значение должно быть корректным UUID v4'));
return $result;
}
return $result;
}
}
Что происходит в этом коде?
Мы описывает класс UUIDv4, реализующий интерфейрс PropertyValidationAttributeInterface (валидация свойства), который проверяет что указанное значение явялется UUID версии 4. Мы указали что данный класс может являться php-атрибутом чтобы была возможность использовать его в качестве атрибута на проверяемом объекте.
И хотя этого кода вполне достаточно для расширения системы использовать его все равно не очень удобно. Что не так с этим правилом?
- Отсутствие валидаторов. Без валидаторов мы переносим проблему дублирования кода контроллеров/сервисов в правила.
- Отсутствие возможности изменить текст ошибки.
Для удобства разработчиков был создан абстрактный класс Bitrix\Main\Validation\Rule\AbstractPropertyValidationAttribute позволяющий легко избавиться от этих недостатков.
use Attribute;
use Bitrix\Main\Localization\LocalizableMessageInterface;
use UUIDv4Validator;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class UUIDv4 extends AbstractPropertyValidationAttribute
{
public function __construct(
protected string|LocalizableMessageInterface|null $errorMessage = null
) {
}
protected function getValidators(): array
{
return [
new UUIDv4Validator(),
];
}
}
Наследование
AbstractPropertyValidationAttributeпозволяет вам вернуть одно сообщение об ошибке errorMessage, вместо стандартных ответов валидатора
Правила для класса
Подобно атрибуту, правило для класса так же имеет свой интерфейс Bitrix\Main\Validation\Rule\ClassValidationAttributeInterface с похожим методом public function validateObject(object $object): ValidationResult; и так же имеет абстрактный класс для упрощения создания своих правил - Bitrix\Main\Validation\Rule\AbstractClassValidationAttribute.
Рассмотрим пример создания правила для класса реализующего логику интервала дат
use Attribute;
use Bitrix\Main\Validation\Rule\AbstractClassValidationAttribute;
use Bitrix\Main\Localization\LocalizableMessage;
use Bitrix\Main\Localization\LocalizableMessageInterface;
use Bitrix\Main\Validation\ValidationError;
use Bitrix\Main\Validation\ValidationResult;
use Bitrix\Main\Type\DateTime;
use ReflectionClass;
use ReflectionProperty;
use Bitrix\Main\Localization\Loc;
Loc::loadMessages(__FILE__);
#[Attribute(Attribute::TARGET_CLASS)]
class DateInterval extends AbstractClassValidationAttribute
{
public function __construct(
private readonly string $startDateProperty,
private readonly string $endDateProperty,
private readonly bool $allowSameDate = false,
private readonly bool $allowNullDate = false,
protected string|LocalizableMessageInterface|null $errorMessage = null
) {}
public function validateObject(object $object): ValidationResult
{
$result = new ValidationResult();
$properties = $this->getProperties($object);
if (empty($properties)) {
$result->addError(new ValidationError(
new LocalizableMessage(
"FUSION_INTRANET_VALIDATION_RULE_DATE_INTERVAL_EMPTY_PROPERTIES",
),
));
return $this->replaceWithCustomError($result);
}
$values = $this->getValues($object, ...$properties);
$leftValue = $values[$this->startDateProperty] ?? null;
$rightValue = $values[$this->endDateProperty] ?? null;
if ($leftValue === null) {
if ($this->allowNullDate) {
$leftValue = DateTime::createFromTimestamp(0);
} else {
$result->addError(new ValidationError(
new LocalizableMessage(
"FUSION_INTRANET_VALIDATION_RULE_DATE_INTERVAL_LEFT_BORDER_REQUIRED",
),
));
}
}
if ($rightValue === null) {
if ($this->allowNullDate) {
$rightValue = DateTime::createFromTimestamp(
strtotime("9999-12-31 23:59:59"),
);
} else {
$result->addError(new ValidationError(
new LocalizableMessage(
"FUSION_INTRANET_VALIDATION_RULE_DATE_INTERVAL_RIGHT_BORDER_REQUIRED",
),
));
}
}
if (!$result->isSuccess()) {
return $this->replaceWithCustomError($result);
}
$cmp = $rightValue <=> $leftValue;
if ($cmp < 0) {
$result->addError(new ValidationError(
new LocalizableMessage(
"FUSION_INTRANET_VALIDATION_RULE_DATE_INTERVAL_DATE_INTERVAL_INVALID",
),
));
} elseif ($cmp === 0 && !$this->allowSameDate) {
$result->addError(new ValidationError(
new LocalizableMessage(
"FUSION_INTRANET_VALIDATION_RULE_DATE_INTERVAL_DATE_INTERVAL_SAME_NOT_ALLOWED",
),
));
}
return $this->replaceWithCustomError($result);
}
private function getProperties(object $object): array
{
$reflection = new ReflectionClass($object);
return array_filter(
$reflection->getProperties(),
fn(ReflectionProperty $property): bool => in_array(
$property->getName(),
[$this->startDateProperty, $this->endDateProperty],
true,
),
);
}
private function getValues(
object $object,
ReflectionProperty ...$properties,
): array {
$values = [];
foreach ($properties as $property) {
if ($property->isInitialized($object)) {
$values[$property->getName()] = $property->getValue($object);
} else {
$values[$property->getName()] = null;
}
}
return $values;
}
}
Наследование
AbstractClassValidationAttributeпозволяет вам вернуть одно сообщение об ошибкеerrorMessage, вместо стандартных ответов валидатора