Использование валидации
Введение
Иногда необходимо проверить, что данные корректны, не привязываясь к бизнес-логике. Например, идентификатор пользователя не должен быть меньше 1.
public function __construct(int $userId)
{
if ($userId <= 0)
{
throw new \Exception();
}
$this->userId = $userId;
}
Такие проверки могут увеличивать объем кода, поэтому для упрощения используется валидация через атрибуты.
Установка правил валидации
Рассмотрим пример класса пользователя.
final class User
{
private ?int $id;
private ?string $email;
private ?string $phone;
// getters & setters ...
}
Ограничения:
idбольше 0,email— валидный адрес,phone— валидный телефон,- заполнен
emailилиphone.
Добавим атрибуты валидации.
use Bitrix\Main\Validation\Rule\AtLeastOnePropertyNotEmpty;
use Bitrix\Main\Validation\Rule\Email;
use Bitrix\Main\Validation\Rule\Phone;
use Bitrix\Main\Validation\Rule\PositiveNumber;
#[AtLeastOnePropertyNotEmpty(['email', 'phone'])]
final class User
{
#[PositiveNumber]
private ?int $id;
#[Email]
private ?string $email;
#[Phone]
private ?string $phone;
// getters & setters ...
}
Теперь можно валидировать через \Bitrix\Main\Validation\ValidationService, который доступен через локатор по ключу main.validation.service. Это позволяет валидировать класс в том месте, где нужно. Например, при сохранении в базу данных.
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Validation\ValidationService;
class UserService
{
private ValidationService $validation;
public function __construct()
{
$this->validation = ServiceLocator::getInstance()->get('main.validation.service');
}
public function create(?string $email, ?string $phone): Result
{
$user = new User();
$user->setEmail($email);
$user->setPhone($phone);
$result = $this->validation->validate($user);
if (!$result->isSuccess())
{
return $result;
}
// save logic ...
}
}
ValidationService предоставляет метод validate(), возвращающий ValidationResult. Результат валидации содержит ошибки всех сработавших валидаторов.
- Модификаторы доступа у свойств не учитываются, валидация происходит через рефлексию.
- Если атрибут
nullableи его значение не установлено, он будет пропущен при валидации.
Валидация вложенных объектов
Если объект сложный и содержит вложенные объекты, можно использовать атрибут \Bitrix\Main\Validation\Rule\Recursive\Validatable. Это укажет, что объект также должен быть провалидирован.
use Bitrix\Main\Validation\Rule\Composite\Validatable;
use Bitrix\Main\Validation\Rule\NotEmpty;
use Bitrix\Main\Validation\Rule\PositiveNumber;
class Buyer
{
#[PositiveNumber]
public ?int $id;
#[Validatable]
public ?Order $order;
}
class Order
{
#[PositiveNumber]
public int $id;
#[Validatable]
public ?Payment $payment;
}
class Payment
{
#[NotEmpty]
public string $status;
#[NotEmpty(errorMessage: 'Custom message error')]
public string $systemCode;
}
// validation
/** @var \Bitrix\Main\Validation\ValidationService $validationService */
$validationService = \Bitrix\Main\DI\ServiceLocator::getInstance()->get('main.validation.service');
$buyer = new Buyer();
$buyer->id = 0;
$result1 = $validationService->validate($buyer);
// "id: Значение поля меньше допустимого"
foreach ($result1->getErrors() as $error)
{
echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}
echo PHP_EOL;
$buyer->id = 1;
$order = new Order();
$order->id = -1;
$buyer->order = $order;
$result2 = $validationService->validate($buyer);
// "order.id: Значение поля меньше допустимого"
foreach ($result2->getErrors() as $error)
{
echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}
echo PHP_EOL;
$buyer->order->id = 123;
$payment = new Payment();
$payment->status = '';
$payment->systemCode = '';
$buyer->order->payment = $payment;
$result3 = $validationService->validate($buyer);
// "order.payment.status: Значение поля не может быть пустым"
// "order.payment.systemCode: Custom message error"
foreach ($result3->getErrors() as $error)
{
echo $error->getCode() . ': ' . $error->getMessage(). PHP_EOL;
}
Валидация в контроллерах
Пример валидации в контроллере.
use Bitrix\Main\Validation\Rule\NotEmpty;
use Bitrix\Main\Validation\Rule\PhoneOrEmail;
final class CreateUserDto
{
public function __construct(
#[PhoneOrEmail]
public ?string $login,
#[NotEmpty]
public ?string $password,
#[NotEmpty]
public ?string $passwordRepeat,
)
{}
}
В коде класс будет выглядеть так:
class UserController extends Controller
{
private ValidationService $validation;
protected function init()
{
parent::init();
$this->validation = ServiceLocator::getInstance()->get('main.validation.service');
}
public function createAction(): Result
{
$dto = new CreateUserDto();
$dto->login = (string)$this->getRequest()->get('login');
$dto->password = (string)$this->getRequest()->get('password');
$dto->passwordRepeat = (string)$this->getRequest()->get('passwordRepeat');
$result = $this->validation->validate($dto);
if (!$result->isSuccess())
{
$this->addErrors($result->getErrors());
return false;
}
// create logic ...
}
}
Чтобы избежать повторения кода, создайте фабричный метод в DTO:
final class CreateUserDto
{
public function __construct(
#[PhoneOrEmail]
public ?string $login = null,
#[NotEmpty]
public ?string $password = null,
#[NotEmpty]
public ?string $passwordRepeat = null,
)
{}
public static function createFromRequest(\Bitrix\Main\HttpRequest $request): self
{
return new static(
login: (string)$request->getRequest()->get('login'),
password: (string)$request->getRequest()->get('password'),
passwordRepeat: (string)$request->getRequest()->get('passwordRepeat'),
);
}
}
Воспользуемся автоварингом контроллера и специальным классом Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter, который спрячет повторяющуюся логику валидации.
class UserController extends Controller
{
public function getAutoWiredParameters()
{
return [
new \Bitrix\Main\Validation\Engine\AutoWire\ValidationParameter(
CreateUserDto::class,
fn() => CreateUserDto::createFromRequest($this->getRequest()),
),
];
}
public function createAction(CreateUserDto $dto): Result
{
// create logic ...
}
}
Если объект CreateUserDto не валиден, метод createAction не выполнится. Контроллер вернет ошибку.
{
data: null,
errors:
[
{
code: "name",
customData: null,
message: "Значение поля не должно быть пустым",
},
],
status: "error"
}
Валидаторы без атрибутов
Валидаторы можно использовать и без атрибутов.
use Bitrix\Main\Validation\Validator\EmailValidator;
$email = 'bitrix@bitrix.com';
$validator = new EmailValidator();
$result = $validator->validate($email);
if (!$result->isSuccess())
{
// ...
}
Собственное сообщение об ошибке
Можно указать свой текст ошибки, который будет возвращен после валидации.
use Bitrix\Main\Validation\Rule\PositiveNumber;
class User
{
public function __construct(
#[PositiveNumber(errorMessage: 'Invalid ID!')]
public readonly int $id
)
{
}
}
$user = new User(-150);
/** @var \Bitrix\Main\Validation\ValidationService $service */
$result = $service->validate($user);
foreach ($result->getErrors() as $error)
{
echo $error->getMessage();
}
// output: 'Invalid ID!'
Стандартная ошибка валидатора:
use Bitrix\Main\Validation\Rule\PositiveNumber;
class User
{
public function __construct(
#[PositiveNumber]
public readonly int $id
)
{
}
}
$user = new User(-150);
/** @var \Bitrix\Main\Validation\ValidationService $service */
$result = $service->validate($user);
foreach ($result->getErrors() as $error)
{
echo $error->getMessage();
}
// output: 'Значение поля меньше допустимого'
Получить сработавший валидатор
Результат валидации хранит ошибки \Bitrix\Main\Validation\ValidationError, которые содержат свойство $this->failedValidator.
$errors = $service->validate($dto)->getErrors();
foreach ($errors as $error)
{
$failedValidator = $error->getFailedValidator();
// ...
}
Список доступных атрибутов
Свойства:
ElementsType— все элементы перечисляемого свойства должны быть заданного типаEmailInArray— значение свойства является одним из элементов массива (для случаев, когда по какой-то причине не удалось использоватьEnum)LengthMaxMinNotEmptyPhonePhoneOrEmail— свойство является либо телефоном, либо почтойPositiveNumberRangeRegExpUrlJson— свойство (строка) является Json
Классы:
AtLeastOnePropertyNotEmpty— проверяет, что хотя бы одно свойство из заданных не пустое (названия свойств прокидываются в конструктор)
Список доступных валидаторов
AtLeastOnePropertyNotEmpty— проверяет, что хотя бы одно свойство из заданных не пустое (названия свойств прокидываются в конструктор)EmailInArray— значение свойства является одним из элементов массива (для случаев, когда по какой-то причине не удалось использоватьEnum)LengthMaxMinNotEmptyPhoneRegExpUrlJson— значение переменной (строка) является Json