Modelos DTO en Laravel 8

Cómo implementar un DTO que modele la petición (request) en Laravel

Los DTO o Data Transfer Object son los tipos de objetos utilizados para modelar la comunicación entre sistemas, lo que en una API REST significa que son los modelos de las peticiones (requests) y respuestas (responses). En la implementación de la API en Laravel, los controladores reciben un objeto genérico Request:

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        // your code here! 
    }

Lo que implica que toda la validación del modelo de datos se debe implementar en el controlador. Buscando referencias y librerías, encontré esta librería para Data Transfer Objects (DTOs): https://github.com/spatie/data-transfer-object y vamos a ver cómo aplicarla para modelar la petición o request.

Solución clásica: Request genérica

Vamos a ver un poco más en detalle la solución clásica. Como indicaba al comienzo del artículo, el controlador recibe una Request genérica y por tanto es parte de la implementación validar el DTO recibido. Por ejemplo, vamos a tener un modelo llamado “Book”

class Book extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'author',
    ];
}

Ahora vamos a implementar un endpoint que reciba un nuevo libro. En la implementación en el controlador, validaremos la petición y se mostrará, o bien los errores de la validación en caso de que el modelo de la petición no sea correcto, o bien los datos recibidos:

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' =>  'required',
            'author' => 'required',
        ]);
 
        if($validator->fails()){
            return response()->json($validator->errors(), 400); 
        }

        $data=[
            'name' => $request['name'],
            'author' => $request['author']
        ];

        return response()->json($data, 200); 

    }

Es un endpoint muy simple, pero la mitad del código es para validar la petición, es decir, para validar un DTO.

Vamos a ver ahora una solución más optimizada utilizando la librería https://github.com/spatie/data-transfer-object.

Implementar una DTO de petición (request) en Laravel

Utilizando la librería de DTO (https://github.com/spatie/data-transfer-object), vamos a definir una clase que gestione la petición (request) y además se encargue de validar el modelo:

<?php

namespace App\Models\Requests;

use Spatie\DataTransferObject\DataTransferObject;
use Illuminate\Http\Request;
use Validator;

class BookStoreRequest extends DataTransferObject
{

    public string $name;

    public string $author;


    public static function fromRequest(Request $request): self
    {
        return BookStoreRequest::fromArray($request->all());
    }

    public static function fromArray($request): self
    {

        $validator = Validator::make($request, [
            'name' =>  'required',
            'author' =>  'required',
        ]);
 
        if($validator->fails()){
            return response()->json($validator->errors(), 400); 
        }

        return new self([
            'name' => $request['name'],
            'author' => $request['author'],
        ]);
    }

}

La clase que modela el DTO de la request dispone de dos métodos estáticos que devuelven un objeto de la clase, una parte de la Request genérica de Laravel (“fromRequest”) y una segunda que tiene como entrada un array (“fromArray”).

Ahora podemos modificar el controller, que quedaría:

use App\Models\Requests\BookStoreRequest;

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $bookStoreRequest = BookStoreRequest::fromRequest($request);

        $data=[
            'name' => $request['name'],
            'author' => $request['author']
        ];

        return response()->json($data, 200); 

    }

De esta manera, la clase BookStoreRequest se encarga de modelar y validar el DTO de la petición, dejando únicamente en el controlador la implementación de la lógica de negocio. En mi opinión, una aproximación mucho más adecuada.

Pero aún podemos mejorarlo un poco más 🙂

Publicidad

Inyección del DTO como Request

Ahora vamos a sustituir el tipo de objeto recibido por el controlador, cambiando la Request genérica:

    public function store(Request $request)
    {
        // your code here!
    }

y sustituirla por el DTO que hemos implementado:

    public function store(BookStoreRequest $request)
    {
        // your code here!
    }

En este caso, cuando Laravel intenta inyectar “BookStoreRequest “, intenta crear la instancia del objeto a partir de la “Request”. Por tanto, para ello tenemos que añadir un constructor a la clase “BookStoreRequest” que tenga como argumento de entrada la “Request” e incluya la ´lógica de validación:

<?php

namespace App\Models\Requests;

use Spatie\DataTransferObject\DataTransferObject;
use Illuminate\Http\Request;
use Validator;

class BookStoreRequest extends DataTransferObject
{

    public string $name;

    public string $author;


    public function __construct(Request $request)
    {
        $validator = Validator::make($request, [
            'name' =>  'required',
            'author' =>  'required',
        ]);
 
        if($validator->fails()){
            return response()->json($validator->errors(), 400); 
        }

        // everything OK, we set the attributes
        $this->name = $request['name'];
        $this->author = $request['author];
    }

}

Por tanto, ya podemos hacer:

use App\Models\Requests\BookStoreRequest;

   /**
     * Store a newly created resource in storage.
     *
     * @param  App\Models\Requests\BookStoreRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(BookStoreRequest $request)
    {

        $data=[
            'name' => $request->name,
            'author' => $request->author
        ];

        return response()->json($data, 200); 

    }

E increíblemente, esto… funciona!

DTO Request en Laravel 8

Como puedes ver, el código del controlador queda muy limpio y únicamente tiene lógica de negocio.

Aun así, la solución todavía no está completa, ya que si bien es cierto que valida la entrada, en caso que la petición no sea correcta obtenemos el siguiente error:

DTO Request excepción

Este error nos indica que está esperando un objeto de tipo “BookStoreRequest” y le estamos devolviendo un “JsonResponse”, que es la salida del validador:

class BookStoreRequest extends DataTransferObject
{

...
    public function __construct(Request $request)
    {

.. 
        if($validator->fails()){
            return response()->json($validator->errors(), 400); 
        }

...
    }

}

En este punto, lo realmente interesante sería que no llegara a ejecutarse nada en el controlador sino que simplemente se devolviera una excepción, y para ello necesitamos añadir una nueva funcionalidad a la API: la gestión de excepciones o Exception Handling en Laravel 8.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.