Валидация строк и чисел в Laravel

Валидация пользовательского ввода - первый рубеж защиты приложения. В Laravel для строк и чисел предусмотрен обширный набор правил: от проверки длины и диапазона до форматов email, URL и регулярных выражений. Здесь разбираем каждое из них с рабочими примерами.

Если вы только начинаете работать с валидацией, загляните в основы валидации - там описаны способы запуска валидатора и обработка ошибок.

Строки

Правило string

Базовая проверка - убедиться, что поле содержит строку, а не массив или число:

$validated = $request->validate([
    'city' => ['required', 'string'],
]);

Если поле может отсутствовать, добавьте nullable:

$request->validate([
    'middle_name' => ['nullable', 'string'],
]);

Длина строки: min, max, between, size

Для строковых полей min и max проверяют количество символов:

$request->validate([
    'username' => ['required', 'string', 'min:3', 'max:30'],
    'bio'      => ['nullable', 'string', 'max:500'],
]);

Запись between:min,max заменяет пару min + max:

$request->validate([
    'nickname' => ['required', 'string', 'between:2,20'],
]);

Правило size требует точное количество символов:

$request->validate([
    'postal_code'  => ['required', 'string', 'size:6'],
    'currency_iso' => ['required', 'string', 'size:3', 'uppercase'],
]);

Fluent-строитель Rule::string()

Цепочка вызовов вместо массива строк:

use Illuminate\Validation\Rule;

$request->validate([
    'title' => [
        'required',
        Rule::string()
            ->min(5)
            ->max(200)
            ->alphaDash(ascii: true),
    ],
]);

Доступные методы: alpha, alphaDash, alphaNumeric, ascii, between, doesntEndWith, doesntStartWith, endsWith, exactly, lowercase, max, min, startsWith, uppercase. Через when и unless можно подключать условные ограничения.

starts_with, ends_with

Проверка начала или конца строки:

$request->validate([
    'sku'      => ['required', 'string', 'starts_with:SKU-,PRD-'],
    'filename' => ['required', 'string', 'ends_with:.csv,.xlsx'],
]);

Принимают несколько вариантов через запятую. Обратные правила: doesnt_start_with и doesnt_end_with.

lowercase и uppercase

Оба правила корректно работают с мультибайтовыми строками - кириллица, умлауты и прочие Unicode-символы проверяются наравне с латиницей:

$request->validate([
    'slug'         => ['required', 'string', 'lowercase'],
    'country_code' => ['required', 'string', 'uppercase', 'size:2'],
]);

alpha, alpha_dash, alpha_num

Ограничение допустимых символов:

$request->validate([
    'first_name' => ['required', 'alpha'],
    'handle'     => ['required', 'alpha_dash:ascii'],
    'promo_code' => ['required', 'alpha_num:ascii'],
]);

Параметр ascii сужает проверку до ASCII-диапазона: alpha:ascii принимает только a-z и A-Z, alpha_num:ascii добавляет цифры 0-9, alpha_dash:ascii - еще дефис и подчеркивание. Без ascii правила принимают любые Unicode-символы.

Пустые строки, filled и trim

Laravel подключает middleware TrimStrings и ConvertEmptyStringsToNull. Пробельная строка " " придет в валидатор как null. Для необязательных полей используйте nullable вместе со string.

Правило filled проверяет, что поле не пустое, но только если оно присутствует в запросе. В отличие от required, оно не требует само наличие поля:

$request->validate([
    // Если передано - не должно быть пустым. Если не передано - ок
    'nickname' => ['filled', 'string', 'max:30'],
]);

Отдельного правила trim нет - обрезка пробелов на уровне middleware. Для кастомной предобработки:

protected function prepareForValidation(): void
{
    $this->merge([
        'sku'   => trim($this->sku),
        'email' => strtolower(trim($this->email)),
    ]);
}

ascii

Проверяет, что строка содержит только ASCII-символы (коды 0-127). Полезно для полей, которые попадут в системы, не поддерживающие Unicode - имена файлов, идентификаторы, HTTP-заголовки:

$request->validate([
    'api_key'  => ['required', 'string', 'ascii'],
    'filename' => ['required', 'string', 'ascii', 'max:255'],
]);

Не путайте с alpha:ascii - правило ascii допускает цифры, пробелы, знаки препинания и спецсимволы. Правило alpha:ascii принимает только буквы.

Валидация JSON-строки

Правило json проверяет, что строка содержит валидный JSON. Распространенный кейс - поля с настройками или метаданными, которые хранятся как JSON-текст:

$request->validate([
    'metadata' => ['required', 'json'],
    'filters'  => ['nullable', 'json'],
]);

Правило пропустит "{}", "[1,2,3]", "\"hello\"" и отклонит "{broken", "undefined". Для валидации структуры JSON (типы полей, обязательные ключи) понадобятся кастомные правила или декодирование + валидация вложенных данных.

Проверка кодировки: encoding

Правило проверяет кодировку через mb_check_encoding. Работает и для строк, и для файлов:

$request->validate([
    // Строковое поле
    'xml_payload' => ['required', 'string', 'encoding:utf-8'],
]);

Через fluent-строитель файлов:

use Illuminate\Validation\Rules\File;

$request->validate([
    'import_file' => [
        'required',
        File::types(['csv'])->encoding('utf-8'),
    ],
]);

Если кодировка не совпадает, валидация падает без попытки конвертации.

Числа

integer и numeric

Правило integer использует PHP-фильтр FILTER_VALIDATE_INT. Строка "42" пройдет, "42.5" - нет.

Правило numeric шире - принимает целые, дробные (float) и числовые строки:

$request->validate([
    'quantity' => ['required', 'integer'],
    'price'    => ['required', 'numeric'],
]);

Строгая проверка типа: strict

Для JSON API, где важен тип данных, а не строковое представление:

$request->validate([
    'age'    => ['required', 'integer:strict'],
    'weight' => ['required', 'numeric:strict'],
]);

Строка "42" не пройдет integer:strict - нужен настоящий int. В HTML-формах strict бесполезен: браузер всегда отправляет строки.

nullable и числовые поля

Когда числовое поле необязательно, недостаточно просто убрать required. Без nullable пустое значение (которое middleware превратит в null) не пройдет проверку integer или numeric:

// Ошибка: null не пройдет integer
'age' => ['integer', 'min:0']

// Правильно: nullable разрешает null
'age' => ['nullable', 'integer', 'min:0']

С nullable остальные правила (min, max, between) применяются только если значение не null. Если передан null - валидация поля считается пройденной.

Положительные числа и запрет нуля

$request->validate([
    // Больше нуля
    'amount' => ['required', 'numeric', 'gt:0'],

    // >= 0 (ноль допустим)
    'discount' => ['required', 'numeric', 'min:0'],

    // Запрет нуля без ограничения знака
    'offset' => ['required', 'integer', 'not_in:0'],
]);

Диапазоны: between, min, max

Для числовых полей проверяют значение, а не длину:

$request->validate([
    'rating'      => ['required', 'integer', 'between:1,5'],
    'temperature' => ['required', 'numeric', 'min:-40', 'max:60'],
    'latitude'    => ['required', 'numeric', 'between:-90,90'],
    'longitude'   => ['required', 'numeric', 'between:-180,180'],
]);

Сравнение полей: gt, gte, lt, lte

Сравнение значения с другим полем формы:

$request->validate([
    'min_price' => ['required', 'numeric'],
    'max_price' => ['required', 'numeric', 'gt:min_price'],

    'start_weight' => ['required', 'numeric'],
    'end_weight'   => ['required', 'numeric', 'gte:start_weight'],
]);

Если поле с указанным именем не найдено, Laravel трактует параметр как числовое значение:

$request->validate([
    'score' => ['required', 'integer', 'gte:0', 'lte:100'],
]);

Десятичные и дробные числа: decimal

Контроль количества знаков после точки:

$request->validate([
    // Ровно два знака (19.99 - ок, 19.9 - нет)
    'price' => ['required', 'numeric', 'decimal:2'],

    // От двух до четырех знаков
    'exchange_rate' => ['required', 'numeric', 'decimal:2,4'],
]);

Для валидации дробных чисел без требований к точности достаточно numeric - оно принимает и 3.14, и 42.

Распространенная задача - цена с копейками. Комбинация для денежного поля:

$request->validate([
    'price'    => ['required', 'numeric', 'decimal:2', 'gt:0', 'max:999999.99'],
    'shipping' => ['required', 'numeric', 'decimal:2', 'min:0'],
    'tax_rate' => ['required', 'numeric', 'decimal:2,4', 'between:0,100'],
]);

Количество цифр: digits, digits_between, min_digits, max_digits

Проверка длины числа в цифрах (не значения):

$request->validate([
    'inn'            => ['required', 'digits:10'],
    'card_last4'     => ['required', 'digits:4'],
    'otp'            => ['required', 'digits_between:4,6'],
    'account_number' => ['required', 'integer', 'min_digits:8', 'max_digits:20'],
]);

multiple_of

Проверка кратности. Типичное применение - шаг количества в интернет-магазине, где товар продается упаковками:

$request->validate([
    'pallet_count' => ['required', 'integer', 'multiple_of:12'],
    'ticket_count' => ['required', 'integer', 'multiple_of:2'],
]);

Только цифры (без точек и знаков)

Когда длина неизвестна, а нужны только цифры - digits не подходит. Используйте regex:

$request->validate([
    'document_number' => ['required', 'regex:/^\d+$/'],
]);

Пропустит "007" и "42", отклонит "-5" и "3.14".

Что пройдет, а что нет

Несколько неочевидных случаев, на которых ломаются правила:

Правило integer:

Правило numeric:

Правило string + max:5:

Правило decimal:2:

Правило email:

Правило url:http,https:

Правило boolean:

Email

Базовая и расширенная проверка

$request->validate([
    'email' => ['required', 'email'],

    // RFC + MX-запись домена + защита от спуфинга
    'corporate_email' => ['required', 'email:rfc,dns,spoof'],
]);

Доступные стили проверки:

Fluent-вариант Rule::email()

use Illuminate\Validation\Rule;

$request->validate([
    'email' => [
        'required',
        Rule::email()
            ->rfcCompliant(strict: false)
            ->validateMxRecord()
            ->preventSpoofing(),
    ],
]);

Проверки dns и spoof требуют расширения PHP intl.

URL, IP, UUID, ULID

URL

$request->validate([
    'website'  => ['required', 'url:http,https'],
    'callback' => ['required', 'url:https'],
]);

Без параметров url принимает любые протоколы. Указание http,https защищает от javascript: и прочих нежелательных схем.

Правило active_url идет дальше - проверяет, что домен в URL реально существует (DNS-запрос). Работает как url + проверка домена:

$request->validate([
    'homepage' => ['required', 'active_url'],
]);

Учитывайте, что active_url делает DNS-запрос при каждой валидации, поэтому для высоконагруженных форм лучше обойтись url:http,https.

IP-адреса

$request->validate([
    'server_ip' => ['required', 'ip'],
    'ipv4_only' => ['required', 'ipv4'],
    'ipv6_only' => ['required', 'ipv6'],
]);

Для MAC-адресов: mac_address.

UUID и ULID

$request->validate([
    'transaction_id' => ['required', 'uuid'],
    'session_token'  => ['required', 'uuid:4'],
    'event_ref'      => ['required', 'uuid:7'],
    'event_id'       => ['required', 'ulid'],
]);

Hex-цвет

$request->validate([
    'brand_color' => ['required', 'hex_color'],
]);

Принимает форматы #fff, #ffffff, #ffffffff.

Регулярные выражения

regex и not_regex

$request->validate([
    'document_number' => ['required', 'regex:/^\d+$/'],
    'display_name'    => ['required', 'string', 'not_regex:/[<>&"\']/'],
]);

Если regex содержит |, передавайте правила массивом:

// Правильно
$request->validate([
    'code' => ['required', 'regex:/^(ABC|DEF)-\d{4}$/'],
]);

// Ошибка - символ | сломает парсинг
// 'code' => 'required|regex:/^(ABC|DEF)-\d{4}$/'

Валидация номера телефона

Встроенного правила нет. Базовая проверка через regex:

$request->validate([
    'phone' => ['required', 'regex:/^\+?[1-9]\d{6,14}$/'],
]);

Для серьезных проектов - пакет propaganistas/laravel-phone на основе libphonenumber.

Компромисс - очистить номер от форматирования перед валидацией:

protected function prepareForValidation(): void
{
    if ($this->phone) {
        $this->merge([
            'phone' => preg_replace('/[\s\-\(\)]+/', '', $this->phone),
        ]);
    }
}

Валидация base64

Встроенного правила нет. Regex проверяет только допустимые символы, но не структурную корректность. Для полной проверки надежнее base64_decode в кастомном правиле:

// Быстрая проверка формата
$request->validate([
    'avatar_data' => ['required', 'string', 'regex:/^[A-Za-z0-9+\/=]+$/'],
]);

// Полная проверка через декодирование
$request->validate([
    'payload' => [
        'required',
        'string',
        function (string $attr, mixed $value, Closure $fail) {
            if (false === base64_decode($value, strict: true)) {
                $fail("Поле {$attr} содержит невалидный base64.");
            }
        },
    ],
]);

Булевы значения и чекбоксы

boolean и boolean:strict

Принимает true, false, 1, 0, "1", "0":

$request->validate([
    'is_active' => ['required', 'boolean'],
]);

Параметр strict - только true и false:

$request->validate([
    'enabled' => ['required', 'boolean:strict'],
]);

Чекбоксы и accepted

Правило accepted проверяет, что поле содержит "yes", "on", 1, "1", true или "true":

$request->validate([
    'terms' => ['required', 'accepted'],
]);

Для обратной логики - declined. Для JSON API, где нужно убедиться, что булево поле равно true, accepted тоже подходит.

Строковые булевы значения

JavaScript-фронтенды иногда отправляют булевы значения как строки "true" и "false". Стандартное правило boolean их не примет - оно ждет true/false (тип bool), 1/0 или "1"/"0".

Если изменить фронтенд нельзя, конвертируйте в prepareForValidation:

protected function prepareForValidation(): void
{
    if (is_string($this->is_active)) {
        $this->merge([
            'is_active' => filter_var($this->is_active, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
        ]);
    }
}

После этого "true" станет true, "false" станет false, а "maybe" станет null (и не пройдет required|boolean).

Форматированный ввод

Пользователи вводят числа с разделителями тысяч, символами валют, пробелами. Валидатор их отклонит - "1 500,00" не пройдет numeric. Очищайте ввод до валидации:

protected function prepareForValidation(): void
{
    if ($this->price) {
        $cleaned = $this->price;
        $cleaned = str_replace([' ', "\xC2\xA0"], '', $cleaned); // пробелы и неразрывные пробелы
        $cleaned = str_replace(',', '.', $cleaned);               // запятая -> точка
        $cleaned = preg_replace('/[^\d.\-]/', '', $cleaned);      // убрать символы валют
        $this->merge(['price' => $cleaned]);
    }
}

После этого "1 500,00 ₽" превратится в "1500.00" и пройдет numeric|decimal:2.

Тот же подход работает для телефонов, ИНН с пробелами, номеров карт с дефисами - очистите форматирование в prepareForValidation, а валидируйте уже чистые данные.

Числа из формы vs JSON API

HTML-форма всегда отправляет строки. Поле <input type="number" value="42"> на сервере станет строкой "42". Правила integer и numeric это учитывают и принимают строковые числа.

В JSON API клиент может передать настоящий тип: {"age": 42} вместо {"age": "42"}. Если API должен принимать только корректные типы, используйте strict:

// HTML-форма: принимает строку "42"
'quantity' => ['required', 'integer'],

// JSON API: принимает только int 42
'quantity' => ['required', 'integer:strict'],

Это различие часто приводит к багам при тестировании: тесты отправляют JSON с числовыми типами, а в проде приходит строка из формы. Если API принимает оба формата, не используйте strict.

Валидация числовых массивов

Когда форма или API принимает список чисел (идентификаторы, координаты, рейтинги), валидация каждого элемента делается через wildcard *:

$request->validate([
    'product_ids'   => ['required', 'array', 'min:1'],
    'product_ids.*' => ['required', 'integer', 'gt:0'],

    'scores'   => ['required', 'array', 'between:1,10'],
    'scores.*' => ['required', 'numeric', 'between:0,100'],

    'coordinates'     => ['required', 'array', 'size:2'],
    'coordinates.0'   => ['required', 'numeric', 'between:-90,90'],
    'coordinates.1'   => ['required', 'numeric', 'between:-180,180'],
]);

Подробнее о массивах - в валидации массивов и JSON.

Для вложенных объектов с числовыми полями используйте dot-нотацию:

$request->validate([
    'items'           => ['required', 'array', 'min:1'],
    'items.*.name'    => ['required', 'string', 'max:200'],
    'items.*.price'   => ['required', 'numeric', 'gt:0', 'decimal:2'],
    'items.*.qty'     => ['required', 'integer', 'between:1,9999'],
]);

Контроль по типу данных: size, min, max

Поведение зависит от контекста:

$request->validate([
    'name'     => ['required', 'string', 'max:255'],
    'points'   => ['required', 'integer', 'max:1000'],
    'document' => ['required', 'file', 'max:5120'],
]);

Всегда указывайте тип (string, integer, numeric) перед правилами размера, иначе Laravel угадывает по данным.

Подсчет слов

Встроенного правила нет. Кастомное через замыкание:

use Closure;

$request->validate([
    'review' => [
        'required',
        'string',
        function (string $attribute, mixed $value, Closure $fail) {
            if (str_word_count($value) > 300) {
                $fail("Поле {$attribute} не должно превышать 300 слов.");
            }
        },
    ],
]);

Regex-рецепты

Несколько готовых паттернов для типичных задач в рунете:

$request->validate([
    // Российский мобильный: +7 и 10 цифр
    'phone_ru' => ['required', 'regex:/^\+7\d{10}$/'],

    // ИНН физлица (12 цифр) или юрлица (10 цифр)
    'inn' => ['required', 'regex:/^\d{10}(\d{2})?$/'],

    // Серия и номер паспорта РФ: 4 цифры пробел 6 цифр
    'passport' => ['required', 'regex:/^\d{4}\s\d{6}$/'],

    // Российский почтовый индекс
    'zip_ru' => ['required', 'regex:/^\d{6}$/'],

    // Номер банковской карты (16 цифр, можно с пробелами)
    'card' => ['required', 'regex:/^[\d\s]{13,19}$/'],

    // HH:MM формат времени
    'time' => ['required', 'regex:/^([01]\d|2[0-3]):[0-5]\d$/'],

    // Hex-цвет без решетки (для API)
    'color' => ['required', 'regex:/^[0-9a-fA-F]{6}$/'],
]);

Для дат и времени обычно лучше использовать встроенные правила date, date_format - подробнее в валидации дат.

Для проверки кириллических строк (ФИО, город) регулярные выражения тоже работают:

$request->validate([
    // Кириллица, пробелы и дефисы
    'full_name' => ['required', 'regex:/^[\p{Cyrillic}\s\-]+$/u'],

    // Город: кириллица, пробелы, дефисы, точки (для "г." и "ст.")
    'city' => ['required', 'regex:/^[\p{Cyrillic}\s\.\-]+$/u'],
]);

Модификатор u обязателен для работы с Unicode-классами вроде \p{Cyrillic}.

Полные примеры форм

Карточка товара

$request->validate([
    'name'        => ['required', 'string', 'min:3', 'max:200'],
    'slug'        => ['required', 'string', 'lowercase', 'alpha_dash:ascii', 'max:100'],
    'sku'         => ['required', 'string', 'ascii', 'max:50'],
    'description' => ['nullable', 'string', 'max:10000'],
    'price'       => ['required', 'numeric', 'gt:0', 'max:999999.99', 'decimal:2'],
    'old_price'   => ['nullable', 'numeric', 'gt:price'],
    'quantity'    => ['required', 'integer', 'min:0'],
    'weight'      => ['nullable', 'numeric', 'gt:0', 'decimal:0,2'],
    'barcode'     => ['nullable', 'digits_between:8,13'],
]);

Обратите внимание на old_price - правило gt:price гарантирует, что зачеркнутая цена всегда выше текущей.

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

$request->validate([
    'name'     => ['required', 'string', 'min:2', 'max:100'],
    'email'    => ['required', 'email:rfc,dns', 'max:255', 'unique:users,email'],
    'phone'    => ['nullable', 'regex:/^\+?[1-9]\d{6,14}$/'],
    'age'      => ['nullable', 'integer', 'between:13,120'],
    'website'  => ['nullable', 'url:http,https', 'max:500'],
    'username' => ['required', 'string', 'alpha_dash:ascii', 'between:3,30', 'unique:users,username'],
    'bio'      => ['nullable', 'string', 'max:1000'],
    'terms'    => ['required', 'accepted'],
]);

API-эндпоинт настроек

JSON API со строгой типизацией:

$request->validate([
    'notifications_enabled' => ['required', 'boolean:strict'],
    'items_per_page'        => ['required', 'integer:strict', 'between:10,100', 'multiple_of:10'],
    'timezone'              => ['required', 'string', 'timezone'],
    'locale'                => ['required', 'string', 'size:2', 'lowercase'],
    'theme_color'           => ['nullable', 'hex_color'],
    'api_key'               => ['nullable', 'string', 'ascii', 'size:32'],
    'webhook_url'           => ['nullable', 'url:https'],
    'metadata'              => ['nullable', 'json'],
]);

Форма обратной связи

Минимальная форма с защитой от мусорных сообщений:

$request->validate([
    'name'    => ['required', 'string', 'min:2', 'max:100'],
    'email'   => ['required', 'email:rfc'],
    'subject' => ['required', 'string', 'min:5', 'max:200'],
    'message' => ['required', 'string', 'min:20', 'max:5000'],
    'phone'   => ['nullable', 'regex:/^\+?[1-9]\d{6,14}$/'],
    'budget'  => ['nullable', 'numeric', 'gt:0', 'max:99999999'],
]);

Правило min:20 для поля message отсекает сообщения из одного слова.

Платежная форма

$request->validate([
    'amount'          => ['required', 'numeric', 'gt:0', 'decimal:2', 'max:999999.99'],
    'currency'        => ['required', 'string', 'size:3', 'uppercase'],
    'card_number'     => ['required', 'digits_between:13,19'],
    'card_expiry'     => ['required', 'regex:/^(0[1-9]|1[0-2])\/\d{2}$/'],
    'cvv'             => ['required', 'digits_between:3,4'],
    'cardholder_name' => ['required', 'string', 'ascii', 'min:2', 'max:100'],
    'billing_email'   => ['required', 'email:rfc'],
]);

Приведение типов после валидации

Валидация проверяет формат, но не меняет тип данных. После $request->validated() числовые поля из HTML-формы остаются строками:

$data = $request->validated();
// $data['price'] === "19.99" (строка, не float)

Для приведения типов до попадания в модель используйте касты Eloquent:

protected function casts(): array
{
    return [
        'price'     => 'decimal:2',
        'quantity'  => 'integer',
        'is_active' => 'boolean',
    ];
}

Или приведите вручную при работе с данными напрямую:

$data = $request->validated();
$price = (float) $data['price'];
$qty   = (int) $data['quantity'];

Тестирование валидации

Проверить, что правила работают как ожидается, можно через assertSessionHasErrors в feature-тестах или через Validator напрямую в unit-тестах:

// Feature-тест: невалидные данные должны вернуть ошибку
public function test_price_must_be_positive(): void
{
    $response = $this->post('/products', [
        'name'  => 'Test',
        'price' => '-5',
    ]);

    $response->assertSessionHasErrors('price');
}

// Feature-тест: валидные данные проходят
public function test_valid_product_is_accepted(): void
{
    $response = $this->post('/products', [
        'name'  => 'Test Product',
        'price' => '29.99',
        'quantity' => '10',
    ]);

    $response->assertSessionHasNoErrors();
}

Unit-тест через Validator::make позволяет проверить правила без HTTP-запроса:

use Illuminate\Support\Facades\Validator;

public function test_inn_must_be_10_or_12_digits(): void
{
    $rules = ['inn' => ['required', 'regex:/^\d{10}(\d{2})?$/']];

    $valid = Validator::make(['inn' => '7707083893'], $rules);
    $this->assertTrue($valid->passes());

    $invalid = Validator::make(['inn' => '12345'], $rules);
    $this->assertTrue($invalid->fails());
}

Частые ошибки

numeric vs integer для дробных чисел. integer отклонит 3.14. Для цен и весов: numeric или numeric + decimal.

Строка "0" и required. "0" проходит required. Пустая строка "" доедет как null (middleware), и required ее забракует.

digits и ведущие нули. "007" пройдет digits:3, но при приведении к int станет 7.

between включает границы. between:1,10 пропустит и 1, и 10. Для строгих границ: gt и lt.

max:255 без типа. Без string Laravel может решить, что "100" - число, и проверит значение вместо длины.

email без дополнительных проверок. Правило email по умолчанию проверяет только формат по RFC. Адрес [email protected] пройдет валидацию. Если критично отсечь мертвые домены, добавляйте dns.

numeric и запятая как разделитель. Число "12,5" не пройдет numeric - PHP считает запятую недопустимым разделителем. Если форма принимает ввод с запятой, конвертируйте в prepareForValidation:

protected function prepareForValidation(): void
{
    if ($this->price) {
        $this->merge([
            'price' => str_replace(',', '.', $this->price),
        ]);
    }
}

gt:0 и строки. Если у поля нет правила numeric или integer, gt:0 сравнит длину строки с нулем - и любая непустая строка пройдет. Всегда ставьте numeric или integer перед gt.

Подробнее о правилах unique, exists и проверках по базе данных - в статье валидация по базе данных. О кастомных правилах через Rule и замыкания - в пользовательские правила.