El patrón de diseño de Action en Laravel

Cómo usar el patrón Action en Laravel para sacar la lógica de los controladores, crear clases reutilizables y facilitar tests unitarios.

El patrón de diseño de Action en Laravel

El patrón de diseño Action en Laravel ayuda a organizar mejor el código al mover la lógica de negocio fuera de los controladores. Este enfoque consiste en crear clases específicas, llamadas Actions, que realizan una única operación, como crear un usuario o procesar un pago. Estas clases son reutilizables, fáciles de probar y mejoran la claridad del proyecto.

Beneficios principales:

  • Controladores más simples: Solo validan peticiones y llaman a la Action correspondiente.
  • Código modular: Cada Action se enfoca en una tarea específica.
  • Reutilización: Útil en controladores, comandos Artisan o pruebas.
  • Testing eficiente: Las Actions se prueban de forma aislada.

Cómo implementarlo:

  1. Crea un directorio app/Actions para organizar las clases.
  2. Nombra las Actions con verbos claros (ej. CreateUserAction).
  3. Usa métodos como handle() o execute() para definir la lógica.
  4. Integra las Actions en controladores mediante inyección de dependencias.

Este patrón mejora la estructura de aplicaciones Laravel, haciéndolas más claras y fáciles de mantener.

Laravel Controller to Action/Service with Unit Tests: Practical Example

Laravel

Entender las clases Action y sus beneficios

Comparación entre Controladores Tradicionales y Patrón Action en Laravel

Comparación entre Controladores Tradicionales y Patrón Action en Laravel

¿Qué son las clases Action en Laravel?

Las clases Action en Laravel son componentes diseñados para encapsular operaciones de negocio específicas en una unidad reutilizable. A diferencia de los Jobs o Controladores, no están sujetas a la implementación de interfaces obligatorias y, generalmente, ofrecen un único método para ejecutar su funcionalidad.

Su propósito principal es servir como un puente entre la capa de transporte (como controladores o comandos CLI) y la capa de datos, aliviando la sobrecarga de lógica en estos controladores. Estas clases son independientes del contexto, lo que significa que puedes utilizarlas tanto en aplicaciones web como en la línea de comandos o en pruebas, sin necesidad de duplicar código.

Esta estructura no solo organiza mejor el código, sino que también establece una base sólida para mejorar la separación de responsabilidades.

Principales beneficios de las clases Action

Uno de los mayores beneficios de las clases Action es que centralizan la lógica de negocio en un solo lugar, lo que facilita su localización, comprensión y modificación. En lugar de tener fragmentos de lógica dispersos por toda la aplicación, cada Action se enfoca en una tarea específica.

Además, son fáciles de probar porque se pueden evaluar de forma aislada, sin depender de un entorno HTTP o de otros componentes externos. También fomentan la reutilización: una vez que defines una Action, como ProcessPaymentAction, puedes invocarla desde cualquier parte de tu aplicación, ya sea en un controlador, un Job o incluso en un comando CLI, sin necesidad de duplicar código.

Controladores vs. Actions: una comparación

Para entender mejor las ventajas de las clases Action, aquí tienes una tabla que compara un controlador tradicional con el enfoque basado en Actions:

Característica Controlador tradicional Patrón Action
Tamaño del archivo A menudo extenso, con cientos de líneas Más pequeño y enfocado en un único propósito
Legibilidad Puede volverse confuso con lógica compleja Alta, con un propósito claramente definido
Dificultad de testing Requiere simulación HTTP, lo que complica las pruebas Fácil de probar con tests unitarios simples
Reutilización Limitada, casi inexistente Alta, usable en diferentes contextos
Acoplamiento Fuertemente ligado a HTTP Request Desacoplado del origen que lo ejecuta

"Las Actions son independientes del contexto; no importa si estás usando HTTP, consola o algún trabajo en segundo plano, el código permanece consistente" - Tegos

Cómo organizar y nombrar tus Actions

Estructura de directorios para Actions

La práctica común es colocar todas las Actions dentro del directorio app/Actions para que sean fáciles de encontrar. Este enfoque ayuda a mantener el código ordenado y predecible desde el inicio del proyecto.

En proyectos de mayor envergadura, es útil organizar las Actions en subdirectorios según el dominio o el recurso. Por ejemplo, puedes usar carpetas como app/Actions/Orders, app/Actions/Users o app/Actions/Billing. Este tipo de organización evita que los archivos se acumulen de manera desordenada a medida que el proyecto crece, lo que facilita el mantenimiento y maximiza la reutilización del código.

Nombrar tus clases Action

El nombre de una clase Action debe ser claro y empezar con un verbo que describa exactamente su propósito. Algunos patrones comunes incluyen:

  • {Verbo}{Recurso}: Ejemplos como CreateUser o UpdateProfile.
  • {Dominio}{Objeto}{Verbo}Action: Ejemplos como OrderItemCancelAction o ProcessOrderPaymentAction.

"Nombra tus clases Action como pequeñas frases explícitas que empiezan con un verbo." - Loris Leiva

Añadir el sufijo "Action" a los nombres ayuda a diferenciarlas fácilmente. Además, es buena práctica estandarizar el nombre del método público principal. Los métodos más comunes son handle() (alineado con Jobs y Listeners en Laravel) o execute(), ya que ambos son ampliamente utilizados y reconocidos.

Con una nomenclatura consistente y descriptiva, las Actions se integran sin problemas en la arquitectura MVC, complementando controladores y modelos.

Dónde encajan las Actions en la estructura MVC de Laravel

Como se destacó anteriormente, las Actions simplifican los controladores al mover la lógica de negocio a una capa separada. Sirven como un puente entre los controladores y los modelos, permitiendo que los controladores se limiten a recibir peticiones, ejecutar una Action y devolver una respuesta.

Gracias a que son clases PHP independientes, las Actions pueden usarse en varios contextos: controladores web, APIs, comandos de Artisan o incluso en tests unitarios. Esta flexibilidad asegura que el código sea consistente, ya sea que se use en solicitudes HTTP, tareas en consola o procesos en segundo plano, integrándose perfectamente en toda la arquitectura de la aplicación.

Cómo implementar el patrón Action en Laravel

Crear tu primera clase Action

Para mantener los controladores más ligeros y modularizar el código, el patrón Action se presenta como una solución efectiva. Aunque Laravel no incluye un comando específico como make:action, puedes crear la clase manualmente o, si usas Laravel 11 o superior, ejecutar el comando php artisan make:class Actions/YourActionName. Este comando genera una clase básica en la carpeta Actions.

El núcleo de una Action es un método público que ejecuta la lógica de negocio. Este método puede llamarse __invoke(), handle() o execute(), según prefieras. Además, puedes inyectar dependencias como servicios o repositorios directamente en el constructor de la clase para simplificar el proceso.

"An action is usually a class designed to perform a single operation. For example, recording a users login." - Chris, Developer, christalks.dev

Usar Actions en tus controladores

La integración de una Action en un controlador es sencilla y aprovecha la inyección de dependencias de Laravel. Puedes declarar la clase Action en el constructor del controlador o directamente en el método que la necesita. Laravel se encargará de resolver la instancia automáticamente a través de su contenedor de servicios.

El controlador debe enfocarse únicamente en gestionar la petición HTTP: validar los datos, delegar la lógica a la Action y devolver una respuesta adecuada. Si utilizas el método __invoke() en tu Action, incluso puedes registrarla directamente en las rutas como un controlador invocable, eliminando la necesidad de crear una clase controladora adicional. Esto simplifica aún más la estructura de tu aplicación.

Añadir validación, eventos y transacciones a las Actions

Una vez que la Action está integrada en el controlador, puedes envolver operaciones críticas en transacciones utilizando DB::transaction(). Para la validación, tienes dos opciones: usar Form Requests en el controlador antes de llamar a la Action o realizarla dentro de la propia Action con Validator::make($data, $rules)->validate(). Esto último es especialmente útil si necesitas invocar la Action desde diferentes contextos, como CLI, API o Web.

Las Actions también son responsables de gestionar efectos secundarios como ejecutar eventos, enviar notificaciones o encolar trabajos. Estos procesos deben ejecutarse al final del método principal (handle() o execute()), lo que mantiene el código desacoplado y organizado. Además, en lugar de manejar todas las excepciones dentro de la Action, permite que las excepciones de negocio se propaguen al manejador global de Laravel. Esto asegura respuestas consistentes y un manejo centralizado de errores.

"Actions are context-free; doesn't matter if you're hitting HTTP, console, or some background job, the code stays consistent." - Tegos, Developer

Testing Actions y buenas prácticas

Escribir tests unitarios para Actions

Una vez que hayas desarrollado tus Actions, es crucial comprobar su funcionamiento mediante tests unitarios. La ventaja de trabajar con clases aisladas es que facilita mucho este proceso. Para estructurar tus tests, utiliza el patrón AAA: Arrange (prepara el escenario), Act (ejecuta la acción) y Assert (verifica el resultado). Además, organiza el directorio tests/ de manera que refleje la estructura de app/, lo que hará más sencillo localizar y gestionar cada test.

Asegúrate de que los nombres de tus tests sean claros y específicos. Por ejemplo, un nombre como create_user_when_email_is_valid_then_user_is_persisted describe tanto la condición como el resultado esperado. Cada test debe enfocarse en un único comportamiento, lo que facilita identificar problemas rápidamente. Para mantener las pruebas independientes, inyecta dependencias a través del constructor y utiliza mocks para simular comportamientos externos.

"I want my tests to serve as documentation to my controllers, services, and overall business logic behind it." - Dmitry Khorev, Software Engineer

Con una base sólida de tests, puedes aplicar buenas prácticas para garantizar que tus Actions sean fáciles de mantener y escalar.

Buenas prácticas para Actions mantenibles

Para que tus Actions sean efectivas y fáciles de gestionar, cada una debe cumplir con una única responsabilidad. Por ejemplo, una clase como CreateUserAction debe limitarse exclusivamente a la tarea de crear usuarios. En lugar de instanciar dependencias directamente con new, inyecta estas dependencias a través del constructor, lo que facilita el uso de mocks en los tests. Además, permite que las excepciones de negocio se propaguen al manejador global en lugar de capturarlas dentro de la Action.

Mantén las dependencias bajo control: una buena regla es limitarte a entre 5 y 8 dependencias por Action. Esto evita crear clases "Dios" que son difíciles de leer, probar y mantener. También es recomendable usar Data Transfer Objects (DTOs) para manejar datos tipados en lugar de arrays, lo que aporta claridad y reduce errores. Por último, asegúrate de que tus Actions sean stateless, es decir, que no almacenen datos en propiedades entre ejecuciones. Esto minimiza los efectos secundarios y mejora la fiabilidad de las pruebas.

Buenas prácticas vs. errores comunes

Para reforzar la calidad de tus Actions, aquí tienes una comparación entre prácticas recomendadas y errores frecuentes:

Práctica recomendada Error común
Inyectar dependencias por constructor para facilitar el mocking Hardcodear new Class() o usar Facades estáticas dentro de la Action
Testear casos felices, casos límite y entradas inválidas Probar solo el "happy path" donde todo funciona correctamente
Mantener cada aserción enfocada en un resultado específico Mezclar validaciones de respuesta, estado de la BD y estructura JSON en un solo test
Usar nombres descriptivos para los tests Usar nombres genéricos como test_handle() o test_action()
Una Action realiza una tarea específica (por ejemplo, CreateInvoice) Crear Actions que manejan múltiples tareas no relacionadas
Usar DB::transaction para operaciones multi-paso No usar transacciones, lo que puede causar datos inconsistentes si ocurre un fallo

"Actions are standalone classes, they can be easily tested in isolation without relying on the full controller or service environment." - Alex Beygi

Conclusión

Como hemos repasado en las secciones anteriores, el patrón de diseño Action mejora significativamente la organización y el mantenimiento del código en Laravel. Al mover la lógica de negocio fuera de los controladores y delegarla a clases específicas, se consigue un código más limpio, fácil de reutilizar y mucho más sencillo de probar. En lugar de controladores que asumen múltiples tareas, cada Action se centra en una función particular, lo que hace que la aplicación sea más comprensible y escalable.

Uno de los mayores beneficios de este enfoque es la reutilización. Una misma Action puede ser utilizada desde distintos puntos: un controlador web, un comando de Artisan, un trabajo en cola o incluso en pruebas unitarias. Esto elimina la duplicación de código y asegura que la lógica de negocio sea coherente, sin importar desde dónde se ejecute. Como bien explica Loris Leiva, creador de Laravel Actions:

"Actions provide a more intuitive structure that focuses on your domain whilst embracing the features of the framework it relies upon"

Además, el patrón Action simplifica la creación de pruebas unitarias, ya que permite probar cada clase de forma aislada, sin necesidad de simular peticiones HTTP o configurar entornos complejos. Al tratarse de clases independientes, escribir pruebas se vuelve más directo, lo que aporta una mayor confianza al implementar cambios.

Mantener el directorio app/Actions organizado por dominios también ayuda a que cualquier desarrollador pueda entender rápidamente las capacidades de la aplicación con solo explorar su estructura de archivos.

Para implementar este patrón, basta con seguir algunos principios básicos: crear clases con una única responsabilidad, inyectar las dependencias necesarias a través del constructor y mantener las Actions pequeñas y enfocadas. Con estas prácticas, no solo lograrás un código Laravel más fácil de mantener y escalar, sino también más profesional y eficiente.

FAQs

¿Qué impacto tienen las Actions en la arquitectura MVC de Laravel?

Las Actions en Laravel ofrecen una forma práctica de trasladar la lógica de negocio a clases específicas con un propósito claro y definido. Esto permite que los controladores se enfoquen exclusivamente en tareas como gestionar las peticiones HTTP, manejar la validación, la autorización y generar las respuestas adecuadas. Mientras tanto, tareas como la creación de usuarios o el envío de correos se encapsulan en estas Actions reutilizables.

Esta estructura no solo mejora la organización del código, sino que también lo hace más fácil de mantener y probar. Al estar desacopladas del ciclo de vida de la petición, las Actions pueden utilizarse en diferentes contextos: controladores, comandos o pruebas, sin necesidad de duplicar lógica. Además, ofrecen la flexibilidad de ejecutar tareas de forma sincrónica o mediante colas. En esencia, las Actions trabajan en armonía con el patrón MVC, ayudando a mantener una arquitectura más limpia y modular.

¿Cuáles son las ventajas de usar Actions en Laravel frente a la lógica en los controladores?

Usar Actions en Laravel es una forma práctica de mantener el código más organizado al separar la lógica de negocio de los controladores. En lugar de que los controladores manejen tanto la lógica como las peticiones HTTP, las Actions se encargan de las operaciones más complejas, como registrar un usuario o generar una factura, dejando a los controladores enfocados en tareas como validaciones, redirecciones y respuestas.

¿Por qué son útiles las Actions? Aquí tienes algunas razones clave:

  • Código más claro y fácil de mantener: Cada Action sigue el principio de responsabilidad única (SRP), lo que significa que cada clase tiene un propósito específico, haciendo que el código sea más limpio y sencillo de modificar.
  • Reutilización en diferentes partes del proyecto: Puedes usar las Actions en controladores, comandos de consola, pruebas o incluso en jobs, sin necesidad de duplicar lógica.
  • Pruebas más simples: Al estar aisladas, las Actions son mucho más fáciles de probar con tests unitarios, asegurándote de que su comportamiento es el esperado.

En definitiva, las Actions ayudan a estructurar mejor los proyectos, reducen el acoplamiento entre componentes y hacen que las aplicaciones sean más fáciles de escalar y mantener.

¿Cuál es la mejor forma de probar las Actions en Laravel?

En Laravel, probar una Action es bastante directo, ya que puedes tratarla como una clase independiente. Esto significa que puedes realizar tests unitarios directamente sobre sus métodos, sin necesidad de involucrar controladores o solicitudes HTTP. Al seguir el principio de responsabilidad única, las Actions permiten inyectar o simular dependencias de manera sencilla, lo que facilita probar su lógica de forma aislada.

Aquí tienes un ejemplo básico de cómo hacerlo:

  • Prepara los datos necesarios: Crea un array o los objetos requeridos con los campos que la Action necesita para ejecutarse.
  • Simula dependencias externas: Si tu Action interactúa con APIs, servicios externos o envía correos electrónicos, utiliza mocks para simular ese comportamiento.
  • Ejecuta la Action: Llama al método principal, por ejemplo: (new MiAction())->handle($datos);.
  • Verifica los resultados: Comprueba que la lógica funciona como se espera, revisando cosas como la creación de un modelo en la base de datos o la activación de un evento.

Este enfoque asegura que los tests sean rápidos y fáciles de mantener, garantizando que cada Action cumpla su propósito de forma autónoma y sin dependencias innecesarias.

Publicaciones de blog relacionadas