Más allá de CRUDs: [02] Trabajando con data cover image

Más allá de CRUDs: [02] Trabajando con data

Kenny Horna Lectura de 13 minutos

laravel php programming

En el núcleo de cada proyecto, encontrarás data. Casi todas las tareas de las aplicaciones se pueden resumir de este modo: proveer, interpretar y manipular data de cualquier modo que el negocio lo requiera.


Este es el artículo #02 de la serie Laravel: Más allá de CRUDs. Fue originalmente publicado por Brent en su blog (puedes encontrar ahí la serie en su idioma original).

La tabla de contenido que conforma esta serie la tienes aquí.

Dicho esto, comencemos 😉.


Probablemente también hayas notado que: al comienzo de un proyecto no comienzas a construir controladores ni jobs, comienzas construyendo, lo que Laravel llama, modelos. Los grandes proyectos se benefician al hacer DER (Diagrama Entidad-Relación) y otros tipos de diagramas para conceptualizar qué datos serán manejados por la aplicación. Solo cuando eso esté claro, puedes comenzar a construir los puntos de entrada y los hooks que funcionan con sus datos.

En este capítulo veremos de cerca cómo trabajar con datos de manera estructurada, de modo que todos los desarrolladores de tu equipo puedan escribir la aplicación para manejar estos datos de una manera predecible y segura.

Puede que estés pensando en modelos en este momento, pero tenemos que retroceder unos pasos más al principio.

Teoría de tipos

Para comprender el uso de los DTOs (Data Transfer Objects) o Objetos de Transferencia de Datos (spoiler: de eso se trata este capítulo), necesitarás tener algunos conocimientos básicos sobre los sistemas de tipos.

No todos están de acuerdo con el vocabulario utilizado cuando se habla de sistemas de tipos. Así que aclaremos algunos términos en la forma en que los usaré aquí.

La fuerza de un sistema de tipos, tipos fuertes o débiles, define si una variable puede cambiar su tipo después de que se define.

Un ejemplo simple: dada una variable de cadena $a = 'prueba';; un sistema de tipo débil te permite reasignar esa variable a otro tipo, por ejemplo $a = 1;, a un entero.

PHP es un lenguaje débilmente tipado. Siento que existe un ejemplo más real:

$id = '1'; // Ejm. un id recibido desde una URL

function find(int $id): Model
{
    // El input '1' será automáticamente casteado a entero
}

find($id);

Para ser claros: tiene sentido que PHP tenga un sistema de tipado débil. Al ser un lenguaje que funciona principalmente con solicitudes HTTP, todo es básicamente un string.

Puedes pensar que en el PHP moderno, puedes evitar este cambio de tipo detrás-de-cámaras (malabarismo de tipos) mediante el uso de la función de tipos estrictos, pero eso no es completamente cierto. Declarar tipos estrictos evita que otros tipos pasen a una función, pero aún puede cambiar el valor de la variable en la función misma.

declare(strict_types=1);

function find(int $id): Model
{
    $id = '' . $id;

    /*
     *  Esto es perfectamente válido en PHP
     * `$id` es un string ahora.
     */

    // …
}

find('1'); // Esto desencadenaría un TypeError.

find(1); // Esto estaría ok.

Incluso con tipos estrictos y sugerencias de tipos, el sistema de tipado de PHP es débil. Las sugerencias de tipo solo aseguran el tipo de una variable en ese instante en el tiempo, sin garantías sobre cualquier valor futuro que pueda tener esa variable.

Como dije antes: tiene sentido que PHP tenga un sistema de tipo débil, ya que todas las entradas con las que tiene que lidiar comienzan siendo strings. Sin embargo, hay una propiedad interesante para los tipos fuertes: vienen con algunas garantías. Si una variable tiene un tipo que no se puede cambiar, un rango completo de comportamiento inesperado simplemente ya no puede suceder.

Verás, es matemáticamente comprobable que si un programa fuertemente tipado se compila, es imposible que ese programa tenga una variedad de errores que puedan existir en lenguajes débilmente tipados. En otras palabras, los tipos fuertes le dan al programador un mejor seguro de que el código realmente se comporta como se supone que debe hacerlo.

Como nota al margen: ¡esto no significa que un lenguaje fuertemente tipado no pueda tener errores! Eres perfectamente capaz de escribir una implementación con errores. Pero cuando un programa fuertemente tipado se compila exitosamente, está seguro de que cierto conjunto de errores y errores no pueden ocurrir en ese programa.

Los sistemas de tipo fuerte permiten a los desarrolladores tener mucha más información sobre el programa al escribir el código, en lugar de tener que ejecutarlo.

Hay un concepto más que debemos considerar: los tipos estáticos y dinámicos, y aquí es donde las cosas comienzan a ponerse interesantes.

Como probablemente sepas, PHP es un lenguaje interpretado. Esto significa que un script PHP se traduce a código de máquina en tiempo de ejecución. Cuando envías una solicitud a un servidor que ejecuta PHP, este tomará esos archivos .php simples y analizará el texto en algo que el procesador pueda ejecutar.

Una vez más, este es uno de los puntos fuertes de PHP: la simplicidad de escribir un script, actualizar la página y todo está ahí. Esa es una gran diferencia en comparación con un lenguaje que debe compilarse antes de que pueda ejecutarse.

Obviamente, existen mecanismos de almacenamiento en caché que optimizan esto, por lo que la declaración anterior es una simplificación excesiva. Sin embargo, es lo suficientemente bueno como para obtener el siguiente punto.

Una vez más, hay un inconveniente: dado que PHP solo verifica sus tipos en tiempo de ejecución, las verificaciones de tipo del programa pueden fallar cuando se ejecutan. Esto significa que puede tener un error más claro para depurar, pero aún así el programa se ha bloqueado.

Esta comprobación de tipos en tiempo de ejecución hace que PHP sea un lenguaje de tipo dinámico. Por otro lado, un idioma escrito de forma estática tendrá todas sus verificaciones de tipo realizadas antes de que se ejecute el código.

A partir de PHP 7.0, su sistema de tipos se ha mejorado bastante. Tanto es así que herramientas como PHPStan, phan y psalm comenzaron a ser muy populares últimamente. Estas herramientas toman el lenguaje dinámico que es PHP, pero ejecutan un montón de análisis estáticos en su código.

Estas librerías opcionales pueden ofrecer una gran cantidad de información sobre su código, sin tener que ejecutarlo o probarlo, un IDE como PhpStorm también tiene muchas de estas comprobaciones estáticas incorporadas.

Con toda esta información de fondo en mente, es hora de volver al núcleo de nuestra aplicación: la data.

Estructurando data no estructurada

¿Alguna vez has tenido que trabajar con una "variedad de cosas" que en realidad era más que una simple lista? ¿Usaste las llaves de los arrays como campos? ¿Sentiste el dolor de no saber exactamente qué había en ese array? ¿No estás seguro de si los datos que contiene son realmente lo que esperas que sean o qué campos están disponibles?

Visualicemos de lo que estoy hablando: trabajar con los requests de Laravel. Piense en este ejemplo como una operación CRUD básica para actualizar un cliente existente:

function store(CustomerRequest $request, Customer $customer) 
{
    $validated = $request->validated();

    $customer->name = $validated['name'];
    $customer->email = $validated['email'];

    // …
}

Es posible que ya veas surgir el problema: no sabemos exactamente qué datos están disponibles en el array $validated. Si bien los arreglos en PHP son una estructura de datos versátil y poderosa, tan pronto como se usan para representar algo diferente a "una lista de cosas", hay mejores maneras de resolver su problema.

Antes de buscar soluciones, esto es lo que puedes hacer para lidiar con esta situación:

  • Leer el código fuente
  • Leer la documentación
  • "Arrojar" (dump) $validated para inspeccionarlo
  • O usar un depurador para inspeccionarlo

Ahora imagina por un minuto que estás trabajando con un equipo de varios desarrolladores en este proyecto, y que tu colega ha escrito este código hace cinco meses: puedo garantizarte que no sabrás con qué datos estás trabajando, sin hacer ninguna de las cosas engorrosas enumeradas anteriormente.

Resulta que los sistemas fuertemente tipados en combinación con el análisis estático pueden ser de gran ayuda para comprender con qué estamos tratando exactamente. Idiomas como Rust, por ejemplo, resuelven este problema limpiamente:

struct CustomerData {
    name: String,
    email: String,
    birth_date: Date,
}

¡Una estructura es lo que necesitamos! Lamentablemente, PHP no tiene estructuras. Tiene arreglos y objetos, y eso es todo.

Sin embargo... los objetos y las clases pueden ser suficientes:

class CustomerData
{
    public string $name;
    public string $email;
    public Carbon $birth_date;
}

Ahora yo sé; las propiedades escritas solo están disponibles a partir de PHP 7.4. Dependiendo de cuándo leas esta serie, es posible que aún no puedas usarlos. Tengo una solución para ti más adelante en este capítulo, sigue leyendo 😉.

Para aquellos que puedan usar PHP 7.4 o superior, pueden hacer cosas como esta:

public function store(CustomerRequest $request, Customer $customer) 
{
    $validated = CustomerData::fromRequest($request);

    $customer->name = $validated->name;
    $customer->email = $validated->email;
    $customer->birth_date = $validated->birth_date;

    // …
}

El analizador estático integrado en tu IDE siempre podría decirnos con qué datos estamos tratando.

Este patrón de envolver datos no estructurados en tipos, para que podamos usar nuestros datos de manera confiable, se llama "objetos de transferencia de datos" (Data Transfer Objects - DTOs). Es el primer patrón concreto que te recomiendo usar en proyectos Laravel más grandes que el promedio.

Cuando discutas esta serie con tus colegas, amigos o dentro de la comunidad de Laravel, puedes toparte con personas que no comparten la misma visión sobre los sistemas de tipos fuertes. De hecho, hay muchas personas que prefieren adoptar el lado dinámico y débil de PHP. Y definitivamente hay algo que decir al respecto.

Sin embargo, según mi experiencia, el enfoque fuertemente tipado ofrece más ventajas cuando se trabaja con un equipo de varios desarrolladores en un proyecto durante mucho tiempo. Tienes que aprovechar todas las oportunidades que puedas para reducir la carga cognitiva. No deseas que los desarrolladores tengan que comenzar a depurar tu código cada vez que quieran saber qué hay exactamente en una variable. La información tiene que estar a la mano para que los desarrolladores puedan centrarse en lo que es importante: crear la aplicación.

Por supuesto, el uso de DTOs tiene un precio: no solo existe la sobrecarga de definir estas clases; también necesita mapear, por ejemplo, solicitar datos en un DTO.

Los beneficios del uso de DTOs definitivamente superan este costo que tiene que pagar. Independientemente del tiempo que pierdas al escribir este código, lo compensará a largo plazo.

Sin embargo, la pregunta sobre la construcción de DTOs a partir de datos "externos" es una pregunta que aún necesita respuesta.

Fábricas de DTOs

¿Cómo construimos DTO? Compartiré dos posibilidades contigo y también explicaré cuál tiene mi preferencia personal.

El primero es el más correcto: usar una fábrica dedicada.

class CustomerDataFactory
{
    public function fromRequest(
       CustomerRequest $request
    ): CustomerData {
        return new CustomerData([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'birth_date' => Carbon::make(
                $request->get('birth_date')
            ),
        ]);
    }
}

Tener una fábrica separada mantiene tu código limpio durante todo el proyecto. Tiene más sentido que esta fábrica viva en la capa de aplicación.

Si bien es la solución correcta, probablemente notaste que usé una taquigrafía en un ejemplo anterior, en la clase DTO en sí:

CustomerData::fromRequest

¿Qué tiene de malo este enfoque? Bueno, para comenzar: agrega lógica específica de la aplicación en el dominio. El DTO que vive en el dominio ahora debe saber sobre la clase CustomerRequest, que vive en la capa de aplicación.

use Spatie\DataTransferObject\DataTransferObject;

class CustomerData extends DataTransferObject
{
    // …

    public static function fromRequest(
        CustomerRequest $request
    ): self {
        return new self([
            'name' => $request->get('name'),
            'email' => $request->get('email'),
            'birth_date' => Carbon::make(
                $request->get('birth_date')
            ),
        ]);
    }
}

Obviamente, mezclar código específico de la aplicación dentro del dominio no es la mejor de las ideas. Sin embargo, tiene mi preferencia. Hay dos razones para eso.

En primer lugar: ya establecimos que los DTO son el punto de entrada para los datos en la base de código. Tan pronto como trabajemos con datos del exterior, queremos convertirlos en un DTO. Necesitamos hacer este mapeo en algún lugar, por lo que podríamos hacerlo dentro de la clase para la que está destinado.

En segundo lugar, y esta es la razón más importante; Prefiero este enfoque porque es una de las limitaciones de PHP: no admite parámetros con nombre.

Verás, no desea que tus DTO terminen teniendo un constructor con un parámetro individual para cada propiedad: esto no escala y es muy confuso cuando se trabaja con propiedades con valores nulos o predeterminados. Es por eso que prefiero el enfoque de pasar un arreglo al DTO, y hacer que se construya en función de los datos de este arreglo. Como una nota adicional: utilizamos el paquete spatie/data-transfer-object para hacer exactamente esto.

Debido a que los parámetros con nombre no son compatibles, tampoco hay un análisis estático disponible, lo que significa que no sabes qué datos se necesitan cada vez que construye un DTO. Prefiero mantener este "estar en la oscuridad" dentro de la clase DTO, para que pueda usarse sin un pensamiento adicional desde el exterior.

Sin embargo, si PHP admitiera algo como parámetros con nombre, diría que el patrón de fábrica es el camino a seguir:

public function fromRequest(CustomerRequest $request): CustomerData
{
    return new CustomerData(
        'name' => $request->get('name'),
        'email' => $request->get('email'),
        'birth_date' => Carbon::make(
            $request->get('birth_date')
        ),
    );
}

Nota la falta del arreglo al construir CustomerData.

Hasta que PHP soporte esto, elegiría la solución pragmática sobre la correcta teórica. Sin embargo, depende de ti. Siéntase libre de elegir lo que mejor se adapte a su equipo.

Una alternativa a las propiedades tipificadas

Como mencioné anteriormente, existe una alternativa al uso de propiedades escritas para admitir DTO: docblocks. El paquete DTO que vinculé anteriormente también los admite.

use Spatie\DataTransferObject\DataTransferObject;

class CustomerData extends DataTransferObject
{
    /** @var string */
    public $name;

    /** @var string */
    public $email;

    /** @var \Carbon\Carbon */
    public $birth_date;
}

Sin embargo, de forma predeterminada, los docblocks no dan ninguna garantía de que los datos sean del tipo que dicen que son. Afortunadamente, PHP tiene su API de reflexión, y con ella, es posible mucho más.

La solución proporcionada por este paquete puede considerarse como una extensión del sistema de tipo PHP. Si bien hay mucho que se puede hacer en "terreno del usuario" y en tiempo de ejecución, aún así agrega valor. Si no puede usar PHP 7.4 y desea tener un poco más de certeza de que sus tipos de docblock son realmente respetados, este paquete lo tiene cubierto.


Debido a que los datos se encuentran en el núcleo de casi todos los proyectos, es uno de los bloques de construcción más importantes. Los objetos de transferencia de datos le ofrecen una forma de trabajar con datos de forma estructurada, segura y predecible.

Notarás a lo largo de este libro que los DTO se usan con mayor frecuencia. Es por eso que era tan importante mirarlos en profundidad al principio. Del mismo modo, hay otro elemento fundamental que necesita nuestra atención: las acciones. Ese es el tema para el próximo capítulo, stay tuned 😉.