Создание правил валидации
Imhotep позволяет создавать собственные правила валидации, и у вас есть целых три способа сделать это. Какой выбрать? Зависит от ваших задач: хотите ли вы быстрое решение для одного случая, простое переиспользуемое правило или что-то мощное с расширенными возможностями. Давайте разберем каждый вариант.
- Интерфейс IValidationRule - простой, для базовых проверок
- Замыкания - быстрый, для одноразовых проверок
- Наследование AbstractRule - продвинутый, с расширенными возможностями
Используя интерфейс IValidationRule
namespace App\Rules;
use Closure;
use Imhotep\Contracts\Validation\IValidationRule;
use Imhotep\Facades\Validator;
class CustomRule implements IValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($value === 'custom') {
// :attribute, автоматически заменится на имя поля.
$fail('The :attribute must be custom.');
}
}
}
Теперь вы можете использовать правило двумя способами. Первый — передать экземпляр класса прямо в массив правил:
use App\Rules\CustomRule;
$request->validate([
'name' => ['required', 'string', new CustomRule],
]);
Второй способ — зарегистрировать правило глобально, чтобы использовать его как строку в любом месте приложения. Добавьте это в AppServiceProvider:
namespace App\Providers;
use App\Rules\CustomRule;
use Imhotep\Facades\Validator;
use Imhotep\Framework\Providers\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// ...
}
public function boot(): void
{
Validator::extend('custom', CustomRule::class);
}
}
И использовать собственное правило более удобным способом:
$request->validate([
'name' => 'required|string|custom',
]);
Доступ к дополнительным данным
Иногда для проверки одного поля нужно посмотреть на значения других полей. Например, вы хотите убедиться, что дата окончания больше даты начала. В таких случаях реализуйте интерфейс Imhotep\Contracts\Validation\DataAwareRule — он даст вашему правилу доступ ко всем данным формы через метод setData:
namespace App\Rules;
use Closure;
use Imhotep\Contracts\Validation\IValidationRule;
use Imhotep\Contracts\Validation\DataAwareRule;
class CustomRule implements IValidationRule, DataAwareRule
{
protected array $data = [];
public function setData(array $data): void
{
$this->data = $data;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// ...
}
}
Так же, вы можете получить доступ к экземпляру валидатора:
namespace App\Rules;
use Closure;
use Imhotep\Contracts\Validation\IValidationRule;
use Imhotep\Contracts\Validation\ValidatorAwareRule;
use Imhotep\Validation\Validator;
class CustomRule implements IValidationRule, ValidatorAwareRule
{
protected Validator $validator;
public function setValidator(Validator $validator): void
{
$this->validator = $validator;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// ...
}
}
Используя замыкания
Бывает, что правило нужно всего один раз — для конкретного поля в конкретном запросе. Создавать для этого отдельный класс часто излишне. Вместо этого вы можете передать замыкание прямо в массив правил. Оно получит три аргумента: имя поля, его значение и замыкание $fail, которое нужно вызвать, если проверка не пройдена:
use Imhotep\Facades\Validator;
use Closure;
$request->validate([
'title' => [
'required',
'max:255',
function (string $attribute, mixed $value, Closure $fail) {
if ($value === 'foo') {
// :attribute, автоматически заменится на имя поля.
$fail("The :attribute is invalid.");
}
},
],
]);
Есть и более чистый способ — зарегистрировать замыкание через Validator::extend() и использовать правило как обычную строку:
use Imhotep\Facades\Validator;
Route::get('/imhotep', function () {
Validator::extend('imhotep', function (string $attribute, mixed $value, Closure $fail) {
if ($value === 'foo') {
$fail("The :attribute is invalid.");
}
});
$request->validate([
'title' => 'required|max:255|imhotep'
]);
});
А если вы хотите, чтобы замыкание работало во всем приложении, зарегистрируйте его в AppServiceProvider. Так правило будет доступно из любой точки вашего кода:
<?php declare(strict_types=1);
namespace App\Providers;
use App\Rules\CustomRule;
use Imhotep\Facades\Validator;
use Imhotep\Framework\Providers\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Validator::extend('imhotep', function (string $attribute, mixed $value, Closure $fail) {
if ($value === 'foo') {
$fail("The :attribute is invalid.");
}
});
}
}
Используя наследование от AbstractRule
Если вам нужна максимальная гибкость и контроль над процессом валидации, обратите внимание на наследование от AbstractRule. Именно так устроены все встроенные правила Imhotep — от required до email. Этот подход дает вам доступ к мощным возможностям, которых нет у предыдущих двух способов:
namespace App\Rules;
use Imhotep\Validation\Rules\AbstractRule;
class UppercaseRule extends AbstractRule
{
protected string $message = 'Поле :attribute должно быть в верхнем регистре';
public function check(mixed $value): bool
{
if (!is_string($value)) {
return false;
}
return strtoupper($value) === $value;
}
}
Параметры правила
Многие правила требуют дополнительных параметров. Например, правило min:18 требует число 18, а contains:hello,world — список подстрок. Параметры настраиваются через метод setParameters. Он получает массив значений в том порядке, в котором они переданы в строке правила. Ваша задача — перенести их в свойство parameters с удобными для вас ключами.
В методе check вы можете использовать requireParameters(), чтобы указать, какие параметры обязательны. Если их не передадут, валидатор выбросит понятное исключение — не придется писать проверки вручную.
namespace App\Rules;
use Imhotep\Validation\Rules\AbstractRule;
class ContainsRule extends AbstractRule
{
public function setParameters(array $parameters): static
{
// Заполняем параметр "substrings"
$this->parameters['substrings'] = $parameters;
return $this;
}
public function check(mixed $value): bool
{
$this->requireParameters(['substrings']);
if (!is_string($value)) {
return false;
}
foreach ($this->parameters['substrings'] as $substring) {
if (!str_contains($value, $substring)) {
return false;
}
}
return true; // проверка успешна
}
}
Пример использования:
$request->validate([
'name' => 'contains:hello,world'
]);
Модификация значения
Иногда данные нужно привести к единому формату до проверки. Например, обрезать пробелы, привести телефон к стандартному виду или преобразовать строку в число. Для этого реализуйте интерфейс IModifyValue. Метод modifyValue будет вызван до check, и его результат отправится на проверку. Это очень удобно, когда вы хотите не только проверить, но и нормализовать данные.
namespace App\Rules;
use Imhotep\Contracts\Validation\IModifyValue;
use Imhotep\Validation\Rules\AbstractRule;
class TrimRule extends AbstractRule implements IModifyValue
{
public function check(mixed $value): bool
{
return is_string($value);
}
public function modifyValue(mixed $value): mixed
{
return is_string($value) ? trim($value) : $value;
}
}
Неявные правила
Неявные правила выполняются даже для отсутствующих или пустых полей. Это полезно для правил типа required_if или required_with. Установите $implicit = true для проверки полей:
namespace App\Rules;
use Imhotep\Validation\Rules\AbstractRule;
class RequiredIfNotEmptyRule extends AbstractRule
{
protected bool $implicit = true;
public function setParameters(array $parameters): static
{
$this->parameters['field'] = array_shift($parameters);
return $this;
}
public function check(mixed $value): bool
{
$field = $this->parameters['field'];
// Проверка выполняется даже если поле отсутствует
if ($this->data->get($field) !== null) {
return !empty($value);
}
return true;
}
}
Типизированные сообщения
Установите $typed = true для использования разных сообщений для разных типов данных:
<?php
namespace App\Rules;
use Imhotep\Validation\Rules\AbstractRule;
use Imhotep\Http\UploadedFile;
class SizeRule extends AbstractRule
{
use Traits\UtilsTrait;
protected bool $typed = true;
public function setParameters(array $parameters): static
{
$this->parameters['size'] = array_shift($parameters);
return $this;
}
public function check(mixed $value): bool
{
$this->requireParameters(['size']);
$valueSize = $this->getValueSize($value);
$requiredSize = $this->getBytesSize($this->parameters['size']);
return $valueSize === $requiredSize;
}
}
Теперь можно использовать разные сообщения в языковых файлах:
// resources/lang/ru/validation.php
return [
'size' => [
'string' => 'Поле :attribute должно содержать :size символов',
'numeric' => 'Поле :attribute должно быть :size',
'array' => 'Поле :attribute должно содержать :size элементов',
'file' => 'Поле :attribute должно быть :size КБ',
],
];
Доступ к данным
Через свойство $this->data можно получить доступ к другим полям:
namespace App\Rules;
use Imhotep\Validation\Rules\AbstractRule;
class GreaterThanFieldRule extends AbstractRule
{
public function setParameters(array $parameters): static
{
$this->parameters['field'] = array_shift($parameters);
return $this;
}
public function check(mixed $value): bool
{
$this->requireParameters(['field']);
$otherValue = $this->data->get($this->parameters['field']);
if (!is_numeric($value) || !is_numeric($otherValue)) {
return false;
}
return $value > $otherValue;
}
}
Регистрация правила
Правила, наследующие AbstractRule, регистрируются через сервис-провайдер:
namespace App\Providers;
use App\Rules\GreaterThanFieldRule;
use Imhotep\Facades\Validator;
use Imhotep\Framework\Providers\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// ...
}
public function boot(): void
{
Validator::extend('custom', GreaterThanFieldRule::class);
}
}