Немного курочим стандартный валидатор Laravel или первый опыт с фасадами и сервис провайдерами

от автора

Предыстория

Все началось с того, что мне понадобилось отличать пустые строки от 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());   } }
Попробуйте выполнить код выше со стандартным валидатором и сравните.

Попробуйте выполнить код выше со стандартным валидатором и сравните.

Заключение

Написал эту статью, чтобы структурировать то, что я узнал в процессе решения задачи. Может быть кому-то она тоже будет полезна и поможет немного лучше понять архитектуру фреймворка.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Писать про другие проблемы laravel и их решения?

80% Да, это полезно16
20% Не интересно4

Проголосовали 20 пользователей. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/845204/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *