Предыстория
Все началось с того, что мне понадобилось отличать пустые строки от null в апи запросах. Напомню: стандартное поведение Laravel заключается в обрезании у строк начальных и конечных пробелов и преобразовании пустых строк в null. Это актуально для запросов, пришедших из html форм, но в современном мире, где все пуляются по ajax json'ами уже не удобно. Отключается это просто:
Если нужно отключить это поведение для всего приложения, то это делается в файле bootstrap/app.php
(ссылка на документацию):
<?php
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->remove([
ConvertEmptyStringsToNull::class,
TrimStrings::class,
]);
})
->create();
Если это нужно сделать только для какой-то группы маршрутов, то это делается для соответствующего маршрута или группы маршрутов с помощью метода withoutMiddleware
(ссылка на документацию):
<?php
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
Route::->group(function () {
...
})->withoutMiddleware([
ConvertEmptyStringsToNull::class,
TrimStrings::class,
]);
Проблема
После этого пустые строки перестают преобразовываться в null, и кроме правила required
никакие правила валидации на них не срабатывают. Выстрел в ногу произошел с датой (которая у меня должна была быть либо null либо валидной датой). Небольшое копание интернете показало, что не только у меня такая проблема. В таких случаях рекомендуют создавать свое implicit (больше всего, наверное, подходит перевод перевод «безоговорочный») правило командой php artisan make:rule RuleName --implicit
, но мне показалось, что при отключении преобразования пустых строк в null своих правил не напасешься, поэтому я решил изменить поведение валидатора.
Поиск проблемного места
Полазив по коду в папке vendor/laravel/framework/src/Illuminate/Validation в файле Validator.php я нашел процедуру, в которой выполняется эта проверка: цепочка validateAttribute — isValidatable — presentOrRuleIsImplicit. В последней как раз проверяется, является ли входное значение пустой строкой и если да — то является ли применяемое правило implicit.
Найдя проблемное место я даже написал issue, но был послан. Наверное, правильно, так как поведение достаточно сильно меняется, а проблема возникает только при отключении middleware ConvertEmptyStringsToNull.
Дорабатываем валидатор
Зная проблемное место, с этим уже можно что‑то сделать. Я решил написать свой класс‑наследник со своей процедурой проверки необходимости проверки правила. Получилось что‑то такое:
<?php
namespace App\Validator;
use Illuminate\Validation\Validator;
class MyValidator extends Validator
{
/**
* Determine if the field is present, or the rule implies required.
*
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
*/
protected function presentOrRuleIsImplicit($rule, $attribute, $value)
{
if (is_null($value) || (is_string($value) && trim($value) === '')) && !$this->hasRule($attribute, ['Nullable', 'Present', 'Sometimes'])) {
return $this->isImplicit($rule);
}
return $this->validatePresent($attribute, $value) ||
$this->isImplicit($rule);
}
}
Т. е. если есть правила Nullable, Sometimes и Present — все равно запускать проверку, если во входящих данных есть это поле.
Осталось найти, как применить всю силу ООП чтобы везде в приложении использовать класс‑наследник.
Подменяем то, что производит фабрика
Для создания валидаторов в laravel используется фабрика, скрытая за фасадом 'validator' (см. vendor/laravel/framework/src/Illuminate/Support/Facades/Validator.php). Таким образом, заменив то, что создает фабрика на наш класс-наследник с помощью своего сервис-провайдера мы получим желаемое.
Это можно сделать с помощью сервис провайдера, который не предоставляет никаких своих сервисов, но меняет поведение приложения. Такой подход описан в документации здесь.
Добавляем свой сервис-провайдер: php artisan make:provider MyValidatorProvirer
регистрируем его в файле bootstrap/providers.php
(ссылка на документацию):
<?php
return [
App\Providers\AppServiceProvider::class,
// ...
App\Providers\MyValidatorProvider::class,
// ...
];
в функции boot() подставляем свой resolver в фабрику (см. Illuminate\Validation\factory.php, там есть функция-сеттер resolver, которая устанавливает коллбэк для получения нужного класса), т.е. это именно то, что предусмотрено создателями фреймворка:
<?php
namespace App\Providers;
use App\Validator\MyValidator;
use Illuminate\Support\ServiceProvider;
class MyValidatorProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->app['validator'] // фабрика
->resolver( // эта функция устанавливает колбэк для получения нужного экземпляра
function ($translator, $data, $rules, $messages) {
return new MyValidator(
$translator,
$data,
$rules,
$messages
);
});
}
}
Всё, готово. Теперь правила, вызванные на пустые строки будут возвращать ошибки:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class TestController extends Controller
{
public function __invoke(Request $request)
{
$validator = Validator::make(['test' => ''], [
'test' => 'nullable|date'
]);
dump($validator->errors());
}
}
Заключение
Написал эту статью, чтобы структурировать то, что я узнал в процессе решения задачи. Может быть кому-то она тоже будет полезна и поможет немного лучше понять архитектуру фреймворка.
tkovacs
Вообще не понял в чем проблема и для чего все это)
Siddthartha
чтобы реализовать
nullable|date
валидацию?Fragster Автор
По дефолту laravel преобразует пустую (или состоящую только из пробельных символов) к null. Это поведение не всегда подходит. Если же его отключить, то правила валидации не срабатывают на пустые строки (которые до этого автоматически преобразовывались к null).
tkovacs
Почему это не подходит? null это ничего, пустая строка это тоже ничего
Fragster Автор
Например по дефолту (без выключения нормализации), когда приходит json
И мы делаем
$model.fill($validated)
а field при этом не nullable - мы получим ошибку.Да и вообще, ситуации, когда нужно различать пустые строки, null и, например, 0 - существуют.
PiramidHead
И довольно часто встречаются. Как раз по этой причине на собеседованиях нередко можно услышать вопрос: "Что проверяет empty()".