Modifiquemos un poco el validador estándar de Laravel o nuestra primera experiencia con fachadas y proveedores de servicios.

Fondo

Todo comenzó con el hecho de que necesitaba distinguir cadenas vacías de nulas en las solicitudes de API. Como recordatorio, el comportamiento estándar de Laravel es recortar los espacios iniciales y finales de las cadenas y convertir cadenas vacías en nulas. Esto es relevante para solicitudes provenientes de formularios html, pero en el mundo moderno, donde todos usan ajax jsons, ya no es conveniente. Es fácil de desactivar:

Si necesita deshabilitar este comportamiento para toda la aplicación, esto se hace en el archivo bootstrap/app.php (enlace a documentación):

<?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();

Si es necesario hacer esto solo para un determinado grupo de rutas, entonces se hace para la ruta o grupo de rutas correspondiente utilizando el método withoutMiddleware (enlace a documentación):

<?php
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\TrimStrings;

Route::->group(function () {
...
})->withoutMiddleware((
  ConvertEmptyStringsToNull::class,
  TrimStrings::class,
));

Problemas

Después de esto, las cadenas vacías ya no se convierten en nulas y, además de la regla required no se les aplican reglas de validación. El disparo en el pie se produjo con la fecha (que tenía que tener fecha nula o válida). Una pequeña investigación en Internet demostró que no soy el único con este problema. En tales casos, se recomienda crear su propia regla implícita (la traducción más adecuada probablemente sea “incondicional”) con el comando php artisan make:rule RuleName --implicit pero me pareció que si desactivas la conversión de cadenas vacías a nulas, no podrás guardar tus reglas, así que decidí cambiar el comportamiento del validador.

Encontrar el área problemática

Después de revisar el código en la carpeta proveedor/laravel/framework/src/Illuminate/Validation en el archivo Validator.php, encontré un procedimiento en el que se realiza esta verificación: la cadena validarAttribute – isValidatable – presentOrRuleIsImplicit. En este último, comprueba si el valor de entrada es una cadena vacía y, de ser así, si la regla aplicada está implícita.

Habiendo encontrado el área problemática, incluso escribí asuntopero fue enviado. Probablemente esto sea correcto, ya que el comportamiento cambia bastante y el problema sólo ocurre cuando el middleware ConvertEmptyStringsToNull está deshabilitado.

Estamos ultimando el validador.

Conociendo el área del problema, ya puedes hacer algo al respecto. Decidí escribir mi propia clase sucesora con mi propio procedimiento para verificar si es necesario verificar una regla. Resultó algo como esto:

<?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);
  }
}

Es decir, si hay reglas que aceptan valores NULL, a veces y presentes, aún así ejecute la verificación si los datos entrantes contienen este campo.

Todo lo que queda es descubrir cómo aplicar todo el poder de la programación orientada a objetos para utilizar la clase descendiente en todas partes de la aplicación.

Reemplazamos lo que produce la fábrica.

Meme para diversificar de alguna manera el artículo.

Meme para diversificar de alguna manera el artículo.

Para crear validadores, laravel utiliza una fábrica oculta detrás de la fachada del 'validador' (consulte seller/laravel/framework/src/Illuminate/Support/Facades/Validator.php). Por lo tanto, al reemplazar lo que crea la fábrica con nuestra clase descendiente con la ayuda de su proveedor de servicios, obtendremos lo que queremos.

Esto se puede hacer utilizando un proveedor de servicios, que no proporciona ninguno de sus propios servicios, pero cambia el comportamiento de la aplicación. Este enfoque se describe en la documentación. Aquí.

Añade tu proveedor de servicios: php artisan make:provider MyValidatorProvirer registrarlo en un archivo bootstrap/providers.php (enlace a documentación):

<?php

return (
    App\Providers\AppServiceProvider::class,
  // ...
    App\Providers\MyValidatorProvider::class,
  // ...
);

en la función boot() sustituimos nuestro solucionador en la fábrica (ver Illuminate\Validation\factory.php, hay una función de resolución que instala una devolución de llamada para obtener la clase requerida), es decir Esto es exactamente lo que proporcionan los creadores del marco:

<?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
          );
        });
  }
}

Todo está listo. Ahora las reglas llamadas en líneas vacías devolverán errores:

<?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());
  }
}
Intente ejecutar el código anterior con el validador estándar y compare.

Intente ejecutar el código anterior con el validador estándar y compare.

Conclusión

Escribí este artículo para estructurar lo que aprendí en el proceso de resolución del problema. Quizás también sea útil para alguien y le ayude a comprender un poco mejor la arquitectura del marco.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *