Предыстория
Все началось с того, что мне понадобилось отличать пустые строки от 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()); } }
Заключение
Написал эту статью, чтобы структурировать то, что я узнал в процессе решения задачи. Может быть кому-то она тоже будет полезна и поможет немного лучше понять архитектуру фреймворка.
ссылка на оригинал статьи https://habr.com/ru/articles/845204/
Добавить комментарий