3. Trabajando con DTOs (Data Transfer Objects)
3.1. ¿Qué son los DTOs y por qué son importantes?
Los DTOs (Data Transfer Objects) son objetos que transportan datos entre procesos, ayudando a desacoplar la estructura interna de los datos de su representación externa. En nuestro framework, los DTOs cumplen dos funciones esenciales:
- Transformar datos de entrada: Convierten los datos recibidos en la API a la estructura que espera nuestro sistema interno.
- Transformar datos de salida: Convierten los datos internos a la estructura que queremos exponer en nuestra API.
Los DTOs son cruciales porque:
- Separan preocupaciones: La lógica de transformación de datos está aislada del resto del código.
- Ocultan detalles internos: No exponemos directamente estructuras de base de datos.
- Permiten evolucionar la API: Podemos cambiar la estructura interna sin afectar la API pública.
- Facilitan la documentación: Definen claramente qué datos se envían y reciben.
- Evitan problemas de seguridad: Control explícito sobre qué datos se exponen.
3.2. DTOs de petición (ReqDTO)
Los DTOs de petición se encargan de transformar los datos que recibimos del cliente al formato interno que espera nuestra aplicación.
3.2.1. Estructura y propósito
Todos los ReqDTO extienden la clase base ReqDto e implementan la interfaz InterfaceReqDto:
namespace ApiLayer\Domains\Products\DTOs;
use App\Dtos\Contracts\InterfaceReqDto;
use App\Dtos\ReqDto;
class ReqMaesartiProductosDTO extends ReqDto implements InterfaceReqDto
{
// Propiedades que almacenarán los datos transformados
protected $nombre;
protected $codigo;
protected $precio;
// Mapa de transformación para filtros
protected $mapFilters = [];
// Método obligatorio para transformar datos
public function handle(array $data): InterfaceReqDto
{
// Transformación de datos aquí
return $this;
}
// Método obligatorio para convertir a array
public function toArray(): array
{
// Estructura interna que se usará en la aplicación
return [];
}
}
3.2.2. Personalizando el mapeo de campos
El mapeo de campos se realiza en dos lugares principales:
- En el método
handle(): Para transformar datos de creación/actualización - En la propiedad
$mapFilters: Para transformar campos en filtros
Ejemplo completo:
class ReqProductosDTO extends ReqDto implements InterfaceReqDto
{
// Propiedades internas (nombres de BD)
protected $nombre;
protected $codigo_articulo;
protected $precio_venta;
protected $categoria_id;
// Mapeo para filtros (API -> BD)
protected $mapFilters = [
'name' => 'nombre',
'code' => 'codigo_articulo',
'price' => 'precio_venta',
'category' => 'categoria_id'
];
// Transforma datos de entrada
public function handle(array $data): InterfaceReqDto
{
// Convertimos nombres de API a nombres internos
$this->nombre = $data['name'] ?? null;
$this->codigo_articulo = $data['code'] ?? null;
$this->precio_venta = $data['price'] ?? null;
$this->categoria_id = $data['category'] ?? null;
return $this;
}
// Convierte a estructura interna
public function toArray(): array
{
return [
'nombre' => (string) $this->nombre,
'codigo_articulo' => (string) $this->codigo_articulo,
'precio_venta' => (float) $this->precio_venta,
'categoria_id' => (int) $this->categoria_id
];
}
}
3.2.3. Transformando datos de entrada
El método handle() es donde realizamos toda la lógica de transformación. Aquí podemos:
- Renombrar campos: De nombres amigables a nombres internos
- Convertir tipos: Asegurar que los datos tengan el tipo correcto
- Calcular valores: Derivar campos a partir de otros
- Aplicar formato: Normalizar fechas, textos, etc.
Ejemplo avanzado con transformaciones:
public function handle(array $data): InterfaceReqDto
{
// Transformación de nombre (convertir a mayúsculas)
$this->nombre = strtoupper($data['name'] ?? '');
// Formateo de código con prefijo
$this->codigo_articulo = 'PRD-' . ($data['code'] ?? '');
// Cálculo de precio con IVA
$precioBase = $data['price'] ?? 0;
$this->precio_venta = $precioBase * 1.21; // Añadir 21% IVA
// Conversión de categoría (de string a ID)
$categoriaNombre = $data['category'] ?? '';
$this->categoria_id = $this->obtenerCategoriaId($categoriaNombre);
return $this;
}
private function obtenerCategoriaId($nombre)
{
// Aquí iría la lógica para traducir un nombre a ID
$mapaCategorias = [
'electronics' => 1,
'furniture' => 2,
'clothing' => 3
];
return $mapaCategorias[strtolower($nombre)] ?? 0;
}
3.3. DTOs de respuesta (ResDTO)
Los DTOs de respuesta transforman los datos internos de la aplicación al formato que queremos exponer en nuestra API.
3.3.1. Estructura y propósito
Todos los ResDTO extienden la clase base ResDto e implementan la interfaz InterfaceResDto:
namespace ApiLayer\Domains\Products\DTOs;
use App\Dtos\ResDto;
use App\Dtos\Contracts\InterfaceResDto;
class ResProductosDTO extends ResDto
{
// Define qué campos seleccionar de la BD
public static $selectables = [];
// Propiedades internas
protected $id;
protected $nombre;
protected $codigo;
// Convierte datos internos a DTO
public function handle(array $data): InterfaceResDto
{
// Asignar valores de la BD a propiedades
return $this;
}
// Convierte DTO a estructura para API
public function toArray(): array
{
// Estructura externa para API
return [];
}
}
3.3.2. Selección de campos para respuestas
La propiedad estática $selectables define qué campos deben seleccionarse de la base de datos:
class ResProductosDTO extends ResDto
{
// Campos a seleccionar de la BD
public static $selectables = [
'id',
'nombre',
'codigo_articulo',
'precio_venta',
'stock_actual',
'fecha_alta',
'categoria_id'
];
// Resto del código...
}
El framework usará automáticamente esta lista para optimizar las consultas SQL, seleccionando solo los campos necesarios.
3.3.3. Transformando datos de salida
La transformación de datos ocurre en dos métodos principales:
handle(): Convierte los datos de la BD a propiedades del DTOtoArray(): Convierte las propiedades a la estructura final de la API
Ejemplo completo:
class ResProductosDTO extends ResDto
{
public static $selectables = [
'id',
'nombre',
'codigo_articulo',
'precio_venta',
'stock_actual',
'fecha_alta',
'categoria_id'
];
// Propiedades internas
protected $id;
protected $nombre;
protected $codigo_articulo;
protected $precio_venta;
protected $stock_actual;
protected $fecha_alta;
protected $categoria_id;
// Asignar datos de BD a propiedades
public function handle(array $data): InterfaceResDto
{
$this->id = $data['id'];
$this->nombre = $data['nombre'];
$this->codigo_articulo = $data['codigo_articulo'];
$this->precio_venta = $data['precio_venta'];
$this->stock_actual = $data['stock_actual'];
$this->fecha_alta = $data['fecha_alta'];
$this->categoria_id = $data['categoria_id'];
return $this;
}
// Convertir a estructura API
public function toArray(): array
{
return [
'id' => (string) $this->id,
'name' => (string) $this->nombre,
'code' => (string) $this->codigo_articulo,
'price' => (float) $this->precio_venta,
'stock' => (int) $this->stock_actual,
'isAvailable' => $this->stock_actual > 0,
'createdAt' => $this->toDateObject($this->fecha_alta),
'category' => $this->getCategoryName($this->categoria_id)
];
}
// Método auxiliar para convertir ID a nombre
private function getCategoryName($categoryId)
{
$categories = [
1 => 'Electronics',
2 => 'Furniture',
3 => 'Clothing'
];
return $categories[$categoryId] ?? 'Unknown';
}
}
3.4. Ejemplos prácticos de DTOs eficaces
Veamos algunos escenarios comunes donde los DTOs son especialmente útiles:
Ejemplo 1: Transformación de fechas
// En ReqDTO (recibir fecha en formato amigable)
public function handle(array $data): InterfaceReqDto
{
// Convertir fecha de formato dd/mm/yyyy a formato interno yyyymmdd
if (isset($data['birthDate'])) {
$dateObj = \DateTime::createFromFormat('d/m/Y', $data['birthDate']);
$this->fecha_nacimiento = $dateObj ? $dateObj->format('Ymd') : null;
}
return $this;
}
// En ResDTO (devolver fecha en formato amigable)
public function toArray(): array
{
return [
'id' => (string) $this->id,
'name' => (string) $this->nombre,
'birthDate' => $this->formatBirthDate()
];
}
private function formatBirthDate()
{
if (!$this->fecha_nacimiento) return null;
// Convertir de formato interno yyyymmdd a dd/mm/yyyy
$year = substr($this->fecha_nacimiento, 0, 4);
$month = substr($this->fecha_nacimiento, 4, 2);
$day = substr($this->fecha_nacimiento, 6, 2);
return "{$day}/{$month}/{$year}";
}
Ejemplo 2: Unir campos relacionados
// En ReqDTO (separar nombre completo)
public function handle(array $data): InterfaceReqDto
{
// Separar nombre completo en nombre y apellidos
if (isset($data['fullName'])) {
$parts = explode(' ', $data['fullName'], 2);
$this->nombre = $parts[0] ?? '';
$this->apellidos = $parts[1] ?? '';
}
return $this;
}
// En ResDTO (unir nombre y apellidos)
public function toArray(): array
{
return [
'id' => (string) $this->id,
'fullName' => trim($this->nombre . ' ' . $this->apellidos),
'email' => (string) $this->email
];
}
Ejemplo 3: Datos calculados
// En ResDTO (calcular campos adicionales)
public function toArray(): array
{
// Calcular precio con descuento
$precioFinal = $this->precio_venta;
$descuento = 0;
if ($this->stock_actual > 20) {
$descuento = 10; // 10% de descuento para productos con mucho stock
$precioFinal = $this->precio_venta * 0.9;
}
return [
'id' => (string) $this->id,
'name' => (string) $this->nombre,
'regularPrice' => (float) $this->precio_venta,
'finalPrice' => (float) $precioFinal,
'discount' => $descuento,
'stock' => (int) $this->stock_actual
];
}
Ejemplo 4: Formato condicional
// En ResDTO (formato condicional según tipo de usuario)
public function toArray(): array
{
$data = [
'id' => (string) $this->id,
'name' => (string) $this->nombre,
'email' => (string) $this->email
];
// Si es persona física, incluir DNI
if ($this->tipo_cliente === 'F') {
$data['documentType'] = 'DNI';
$data['documentNumber'] = $this->documento;
}
// Si es empresa, incluir CIF
else if ($this->tipo_cliente === 'J') {
$data['documentType'] = 'CIF';
$data['documentNumber'] = $this->documento;
$data['companyName'] = $this->nombre_fiscal;
}
return $data;
}
Consejos para trabajar con DTOs
-
Sé explícito con los tipos: Usa type casting para garantizar el tipo correcto de datos
'price' => (float) $this->precio -
Nombres descriptivos: Usa nombres claros que indiquen el propósito
'isAvailable' en vez de 'avail' -
Utiliza métodos auxiliares: Extrae lógica compleja a métodos privados
'fullAddress' => $this->formatFullAddress() -
Sé consistente: Mantén el mismo estilo en todos tus DTOs
// Usa siempre camelCase en la API
'firstName', 'lastName', 'emailAddress' -
Documenta campos especiales: Añade comentarios para campos no obvios
// 'status': 1=active, 2=pending, 3=blocked
'status' => (int) $this->estado -
Valida en el controlador, transforma en el DTO: Los DTOs no son para validación
-
Haz los DTOs verbosos: Es mejor tener más información que menos
// Incluye más información si es útil
'product' => [
'id' => $this->id,
'name' => $this->nombre,
'summary' => $this->getShortDescription(),
'details' => $this->getFullDescription()
]
Los DTOs son una parte crucial del framework que permiten separar claramente la representación interna de los datos de su representación externa, proporcionando una capa de abstracción que facilita la evolución de la API y el mantenimiento del código.