Создание правил валидации

Imhotep позволяет создавать собственные правила валидации, и у вас есть целых три способа сделать это. Какой выбрать? Зависит от ваших задач: хотите ли вы быстрое решение для одного случая, простое переиспользуемое правило или что-то мощное с расширенными возможностями. Давайте разберем каждый вариант.

  1. Интерфейс IValidationRule - простой, для базовых проверок
  2. Замыкания - быстрый, для одноразовых проверок
  3. Наследование 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.');
        }
    }
}
PHP

Теперь вы можете использовать правило двумя способами. Первый — передать экземпляр класса прямо в массив правил:

use App\Rules\CustomRule;

$request->validate([
    'name' => ['required', 'string', new CustomRule],
]);
PHP

Второй способ — зарегистрировать правило глобально, чтобы использовать его как строку в любом месте приложения. Добавьте это в 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);
    }
}
PHP

И использовать собственное правило более удобным способом:

$request->validate([
    'name' => 'required|string|custom',
]);
PHP

Доступ к дополнительным данным

Иногда для проверки одного поля нужно посмотреть на значения других полей. Например, вы хотите убедиться, что дата окончания больше даты начала. В таких случаях реализуйте интерфейс 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
    {
        // ...
    }
}
PHP

Так же, вы можете получить доступ к экземпляру валидатора:

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
    {
        // ...
    }
}
PHP

Используя замыкания

Бывает, что правило нужно всего один раз — для конкретного поля в конкретном запросе. Создавать для этого отдельный класс часто излишне. Вместо этого вы можете передать замыкание прямо в массив правил. Оно получит три аргумента: имя поля, его значение и замыкание $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.");
            }
        },
    ],
]);
PHP

Есть и более чистый способ — зарегистрировать замыкание через 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'
    ]);
});
PHP

А если вы хотите, чтобы замыкание работало во всем приложении, зарегистрируйте его в 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.");
            }
        });
    }
}
PHP

Используя наследование от 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;
    }
}
PHP

Параметры правила

Многие правила требуют дополнительных параметров. Например, правило 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; // проверка успешна
    }
}
PHP

Пример использования:

$request->validate([
    'name' => 'contains:hello,world'
]);
PHP

Модификация значения

Иногда данные нужно привести к единому формату до проверки. Например, обрезать пробелы, привести телефон к стандартному виду или преобразовать строку в число. Для этого реализуйте интерфейс 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;
    }
}
PHP

Неявные правила

Неявные правила выполняются даже для отсутствующих или пустых полей. Это полезно для правил типа 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;
    }
}
PHP

Типизированные сообщения

Установите $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;
    }
}
PHP

Теперь можно использовать разные сообщения в языковых файлах:

// resources/lang/ru/validation.php

return [
    'size' => [
        'string' => 'Поле :attribute должно содержать :size символов',
        'numeric' => 'Поле :attribute должно быть :size',
        'array' => 'Поле :attribute должно содержать :size элементов',
        'file' => 'Поле :attribute должно быть :size КБ',
    ],
];
PHP

Доступ к данным

Через свойство $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;
    }
}
PHP

Регистрация правила

Правила, наследующие 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);
    }
}
PHP