Implementando DDD y Arquitectura Hexagonal en PHP con Laravel

Fabio Schettino
14 min readAug 22, 2020

--

Desde los comienzos de mi experiencia profesional como desarrollador (y también como diseñador UX, pero esta es otra historia), he intentado buscar y encontrar soluciones que en el medio y largo plazo me permitieran trabajar de la manera más agradable y rápida posible, tanto a mí como a mis compañeros de equipo, garantizando a la vez la entrega de productos de calidad.

Esto, desde luego, incluye la posibilidad de reutilizar el código en diferentes proyectos.

Por estos motivos, por mi pasión por las arquitecturas y los patrones de diseño de software, por el código limpio y legible, y porque en el último periodo he tenido la oportunidad de trabajar en proyectos desarrollados en PHP que utilizaban como framework Laravel, he decidido escribir este artículo.

Existe mucha información que trata de los aspectos tácticos del Domain Driven Design (DDD) y también mucha relativa a la implementación de clean architectures o arquitecturas limpias en PHP, pero muy poca que lo reúna todo y lo aplique a Laravel que como framework PHP está teniendo cada vez más auge debido a la facilidad con la que puedes desarrollar aplicaciones en tiempos muy rápidos y con un código elegante.

Ahora bien, la primera pregunta que podría surgir es:

¿Pero porque si un proyecto tiene una perspectiva a medio/largo plazo debería utilizar Laravel y no un framework como Symfony por ejemplo?

Una de las posibles respuestas podría ser porque no todos los proyectos tienen esa perspectiva al principio, a menudo empiezan siendo pequeños y por eso necesitan ser desarrollados rápidamente y Laravel es perfecto para esos casos.

Pero con el tiempo, pueden llegar a ser cada vez más grandes y convertirse en un negocio que funciona y necesita de mantenimiento y escalabilidad a medio/largo plazo y claro, no todo el mundo se puede permitir refactorizar todo de nuevo con los costes de tiempo, know-how y mano de obra que eso implica.

Es el caso de muchas start-ups que nacen de una idea, un producto mínimo viable (MVP) y terminan convirtiéndose en un negocio rentable que hay que cuidar y hacer crecer.

Eso si, en cuanto las cosas empiecen a crecer mucho en algún momento habrá que plantearse un cambio en la infraestructura y esto por supuesto, entre otras cosas, podría incluir pasar a un framework más robusto.

La segunda pregunta que podría surgir es:

¡Ok! ¿Pero porque debería implementar una arquitectura limpia como la arquitectura hexagonal, que sin duda añade complejidad accidental, cuando uno de los puntos de fuerza de Laravel es precisamente la sencillez y rapidez con la que nos permite desarrollar?
Con muy pocas lineas de código y abastecido de la suficiente cantidad de comida y bebida la aplicación ya está hecha.

Y la respuesta es porque cuando llegue el momento de cambiar nuestra infraestructura, y por alguna u otra razón este momento antes o después llegará si el proyecto empieza a crecer, te lo garantizo, el haber implementado una arquitectura limpia será lo que te permitirá ahorrar muchísimo tiempo, dinero y quebraderos de cabeza.

Además recuerda, que lo que no estás haciendo tú como desarrollador, lo está haciendo Laravel detrás del telón, y esto está bién si desarrollas aplicaciones pequeñas, si pero necesitas evolucionar tu producto, o tu infraestructura cambia, si necesitas más control sobre tu código porque el proyecto está empezando a tener éxito y pasas de tener 4 peticiones (Tu, tu novia y tus padres) a tener 100.000 o más, puede que tu base de datos o el mismo framework, o hasta la misma versión de framework ya no sean suficientes y tengas que orientarte hacia otras soluciones que te proporcionen más robustez, nuevas funcionalidades o más niveles de seguridad y fiabilidad.

Es en ese momento que entran en juego las arquitecturas limpias en todo su esplendor.

Estas, tienen el objetivo entre otros, de desacoplarnos de la infraestructura o, como en el caso de Laravel cuyo ORM es Eloquent que utiliza el patrón active record, de reducir al mínimo los llamados leaks de infraestructura a los cuales Laravel, por su naturaleza, está más expuesto como veremos más adelante.

…aquí ya empezamos a entrar en lo técnico, pero vamos por pasos.

¿Cuales son las principales características de una clean architecture?

Desde el punto de vista técnico:

  • Muy bajo o nulo nivel de acoplamiento y dependencia de los detalles de implementación (framework, database, etc.).
  • Facilidad de mantenimiento y flexibilidad al cambio (introducir nuevas funcionalidades resulta ser una tarea mucho más liviana y rápida).
  • El software se vuelve intrínsecamente testeable (¡Si! Hay que hacerlo, si no quieres empezar a rezar cada vez que haces una subida a producción tienes que escribir los tests).
  • Estabilidad, robustez y escalabilidad.

Desde el punto de vista del negocio eso se traduce en:

  • Un producto de mayor calidad al que se les pueden añadir mas fácilmente todas las funcionalidades que nuestros clientes demandan (…bueno todas no, antes hay que filtrar las que si y las que no).
  • Tiempos de desarrollo reducidos debido a la menor incidencia de deuda técnica (…y como bien sabemos, el tiempo es dinero).
  • Y por último, pero no menos importante, un equipo de profesionales felices y orgullosos de trabajar en un entorno en el que todo fluye de manera más ágil y organizada.

Y ahora, después de esta extensa pero necesaria introducción, vamos a entrar más en detalles.

Antes de pasar al código es importante definir algunos conceptos.

DDD — Domain Driven Design

En DDD uno de los primeros conceptos con los que nos encontramos es el de bounded context.

La definición de bounded context y su identificación en los procesos de negocio puede llegar a ser amplia y compleja, pero en este caso, para no extendernos mucho y simplificar la tarea, utilizaré la definición que Eric Evans, autor del libro Domain-Driven Design: Tackling Complexity in the Heart of Software y uno de los personajes más influyentes del panorama DDD, da de este concepto.

“Un bounded context es una parte bien definida de un software en la cual términos, definiciones y reglas concretas pueden aplicarse de manera consistente”.

…ok… suena bien, pero puede que todavía no nos haya quedado muy claro, y entonces, ya que a menudo se dice que un ejemplo vale más de mil palabras intentaré explicarlo mejor con un ejemplo.

Imaginemos que nuestro proyecto prevé el desarrollo de una aplicación para los clientes de una cadena de talleres de auto, y de un sistema de gestión para los empleados del servicio de atención al cliente.

Es muy probable que, aunque en parte compartan según que terminología o concepto, estos dos elementos tengan reglas de negocio que se aplican de manera diferente y por eso la aplicación para clientes y el software de gestión del centro de atención pueden ser identificados como dos bounded context diferentes, cada uno con su lógica.

Otro concepto del DDD alrededor del cual todo se mueve es el de dominio y este puede resumirse como el campo de actividad en el cual se desarrolla nuestra lógica de negocio, en él podemos encontrar los modelos de dominio que son la representación de las entidades involucradas en los procesos.

Tomando como ejemplo el de la cadena de talleres de auto, algunas de las entidades podrían ser: Cliente, Taller, Coche etc.

Y por terminar con esta breve disertación tenemos los value objects que pueden ser descritos como los componentes fundamentales que nos permiten modelar las entidades. Vendrían a ser las caracteristicas que las definen dicho muy superficialmente.

En el caso de una entidad Cliente por ejemplo, sus value objects podrían ser el id, su nombre, el email etc.

Ahora ya debería haber quedado más claro, si pero así no fuese, en internet puedes encontrar toneladas de material al respecto que sin duda podrán ayudarte a entenderlo mejor.

Arquitectura Hexagonal

La arquitectura hexagonal es un tipo de arquitectura limpia que se estructura en tres capas o niveles de profundidad.

La primera y más externa es la capa de infraestructura, la segunda es la de aplicación y la tercera, la más profunda, la que se encuentra en el corazón de nuestra arquitectura por así decirlo, es la capa de dominio.

Podéis imaginárosla como círculos concéntricos uno dentro de otro cuyos elementos tienen que respectar una regla muy sencilla: solo se pueden comunicar con otros elementos que pertenecen a la misma capa o la siguiente más interna pero nunca con una externa, es decir, desde fuera hacia dentro y nunca al revés.

Más en concreto, los elementos de la capa de infraestructura solo se pueden comunicar con otros elementos en su mismo nivel y con la capa de aplicación, a su vez, los elementos de la capa de aplicación solo se pueden comunicar con otros elementos en su mismo nivel y con la capa de dominio y los elementos de la capa de dominio solo pueden comunicarse entre si.

La comunicación entre capas ocurre por medio de interfaces o puertos (la arquitectura hexagonal también es llamada arquitectura puerto-adaptador, en inglés port-adapter).

Para una explicación más exhaustiva de este tipo de arquitectura os recomiendo leer un articulo de Chris Fidao que lo explica muy bien. Hexagonal Architecture.

Representación de la Arquitectura Hexagonal

Más en detalle, las capas están organizadas de la siguiente manera:

Infraestructura

Aquí podemos encontrar elementos como los controladores y las implementaciones de los repositorios, todo aquello que representa un punto de contacto con nuestra infraestructura y que potencialmente podría estar sujeto a los famosos leaks que mencionaba al principio. Son los puntos de entrada y salida de nuestro flujo.

Aplicación

En esta capa solemos tener los casos de uso, también llamados acciones o servicios de aplicación.
Los elementos de esta capa reciben en entrada el input que proviene de los elementos de infraestructura como los controladores y se comunican con el dominio.

Dominio

Aquí es donde se encuentra la lógica de negocio, en esta capa vamos a encontrar elementos como los modelos de dominio, sus value objects, servicios, eventos, excepciones de dominio, las interfaces implementadas por los repositorios que se encuentran en la capa de infraestructura etc.

Show me the code!

Para este ejemplo, he creado un proyecto de Laravel en su versión 7 cuyo código puedes encontrar en este repositorio git

Puede que en el momento en el que estás leyendo este articulo el código del repositorio haya cambiado debido a la evolución del proyecto pero los conceptos siguen siendo los mismos, osea, los de diseño guiado por dominio y de arquitectura hexagonal.

En la siguiente imagen se puede observar la estructura de un proyecto de Laravel recién creado, el único añadido es la carpeta src (a veces también llamada core), ese es el núcleo de nuestra arquitectura.

Estructura de carpetas Laravel 7

Lo primero que encontramos al desplegar la carpeta src es la carpeta BoundedContext.

El nombre es puramente demostrativo, en un caso real esta carpeta sería nombrada adecuadamente con el nombre del contexto que estamos modelando, utilizando el ejemplo anterior de la cadena de talleres, esta carpeta podría renombrarse en CustomerCareMS.

Estructura arquitectura y capas

En el bounded context podemos encontrar nuestros dominios o entidades, en este caso solo hay una, la entidad User, he utilizado esta entidad para este ejemplo porque Laravel ya viene con un modelo User (que no tiene nada que ver con nuestra entidad) y para mantener la instalación intacta he preferido no tocar absolutamente nada (bueno algo he añadido pero son detalles muy pequeños como veremos dentro de poco).

En el módulo User podemos ver las tres capas de la arquitectura hexagonal, Infrastructure, Application y Domain, y en cada una de ellas los elementos mencionados el la descripción de la arquitectura que he dado antes.

  • Los controladores y el repositorio de Eloquent en la capa de infraestructura, es decir, IN y OUT del ciclo de vida de la petición dentro de nuestra arquitectura.
  • Los casos de uso en la capa de aplicación.
  • El modelo de dominio User, sus value objects y la interfaz con el repositorio en la capa de dominio.

Ciclo de vida de una petición

Antes de adentrarnos en como funciona el ciclo de vida de una petición te mostraré lo que he añadido a la instalación de base de Laravel.

Se trata de cuatro controladores de Laravel en la carpeta Controllers, uno para cada acción, sus respectivas rutas en el archivo api.php, un Json Resource para formatear la respuesta, y unos tests de aceptación.

Elementos añadidos a instalación base de Laravel

Lo que hacen los controladores es simplemente tramitar la request al controlador en la capa de infraestructura de nuestra arquitectura, recibir la respuesta dentro de un resource y devolverla al solicitante, nada más.

Podríamos enriquecerlos con validaciones utilizando el facade Validator que Laravel nos proporciona pero no es el objeto de este articulo.

Para este ejemplo solo nos centraremos en la implementación de la arquitectura DDD-Hexagonal.

En el constructor del controlador de Laravel inyectamos el controlador de la capa de infraestructura, lo inicializamos y en el método __invoke lo llamamos pasandolo como argumento del UserResource.

Al final del ciclo devolveremos el resultado de la request y su status code utilizando el objeto response.

Laravel CreateUserController

Al controlador en la capa de infraestructura se le inyecta el repositorio de Eloquent por constructor.

Al principio del archivo, en las declaraciones “use” podrás observar que el controlador solo se está comunicando con la capa de aplicación e infraestructura.

CreateUserController en la carpeta Infrastructure

En el método __invoke del controlador extraemos los datos de la request que hemos recibido, instanciamos el caso de uso CreateUserUseCase, le pasamos el repositorio por constructor, luego llamamos el método __invoke del caso de uso y le pasamos los datos.

En este controlador también instanciamos otro caso de uso, el GetUserByCriteriaUseCase por medio del cual recuperamos el usuario creado y lo devolvemos al controlador de Laravel.

CreateUserController en la carpeta Infrastructure

Vamos a ver ahora lo que ocurre una vez hayamos instanciado el caso de uso CreateUserUseCase y les hayamos pasado los datos.

Observa las declaraciones “use”, el caso de uso solo se está comunicando con la capa de dominio.

En su constructor se le inyecta la interfaz UserRepositoryContract.

CreateUserUseCase en la carpeta Application

El método __invoke recibe los datos tipados de la request que han sido extraídos en el controlador e instancia los value objects del modelo de dominio (aquí el tipo de dato que se le pasa por constructor a cada value object es muy importante y tiene que coincidir con su declaración).

Sucesivamente crea un nuevo usuario utilizando el named constructor create del modelo de dominio User y lo persiste utilizando el método save del repositorio EloquentUserRepository, el que hemos inyectado al instanciar el caso de uso y que implementa la interfaz UserRepositoryContract.

CreateUserUseCase en la carpeta Application

Antes de pasar al método save del EloquentUserRepository vamos a mirar como está estructurado un value object y el modelo de dominio User al que pertenece.

Cuando instanciamos un value object desde el caso de uso le pasamos por constructor el dato correspondiente (un email en este caso) del tipo correspondiente, en el caso de este value object en concreto, espera recibir un dato de tipo string y lo primero que hace una vez se haya instanciado el objeto lo valida por medio del método privado validate, sucesivamente lo inicializa para que podamos acceder a el desde el exterior a través del método value.

UserEmail value object en la capa de dominio

Al modelo de dominio User se le pasan por constructor todas las propriedades que lo definen, esto nos obliga a que cada vez que vamos a crear un nuevo usuario el modelo disponga de todos los datos necesarios para poder guardarlo.

Se suele utilizar este enfoque para garantizar integridad de datos en nuestro sistema de persistencia y evitar encontrarnos con entidades en nuestra base de datos que tengan datos parciales y que sean por ende inconsistentes.

Modelo de dominio User en la capa de dominio

El modelo también dispone de unos métodos (getters) que nos permiten acceder a sus propriedades, de un named constructor (el método create) que a partir de los datos que recibe instancia el modelo, y podría contener otros métodos que definen su comportamiento, esto porque la idea a la base de este tipo de enfoque es tener modelos ricos en comportamiento y poder aplicar el principio del Tell don’t Ask planteado por Martin Fowler.

Modelo de dominio User en la capa de dominio

El método create es el que se utiliza en el caso de uso CreateUserUseCase para crear un nuevo usuario a partir del modelo User y que después se pasa al método save del EloquentUserRepository que lo guarda en base de datos.

Ahora si podemos pasar a ver lo que hace el método save una vez reciba el nuevo usuario que ha sido creado.

Este es el punto en el que se manifiesta el mayor leak de infraestructura de la arquitectura al ser implementada en Laravel debido a la integración de Eloquent y sus modelos con el framework.

Eloquent está basado en el patrón active record, y esto representa la mayor diferencia con respecto a un framework como Symfony, cuyo ORM es Doctrine que por lo contrario utiliza el patrón data mapper.

Para reducir al mínimo el problema y evitar contaminar nuestro modelo de dominio User, lo que hacemos aquí es utilizar el modelo User de Laravel (al cual le asignamos el alias EloquentUserModel) e instanciarlo en el constructor del repositorio.

Esto nos permite tener acceso a todas las propriedades y métodos del modelo User del Laravel y aprovechar los métodos que Eloquent nos proporciona.

En definitiva lo que hacemos es crear una capa de abstracción que nos permite mantener nuestra arquitectura a salvo de posibles contaminaciones y al mismo tiempo aprovechar las funcionalidades que el framework y su ORM nos brindan.

Repositorio EloquentUserRepository en la capa de infraestructura

El método save, lo que hace es recibir el modelo de dominio User como parámetro y mapear sus atributos en un array que se le pasa al método create del EloquentUserModel para que lo persista.

Repositorio EloquentUserRepository en la capa de infraestructura

Una vez haya sido guardado el nuevo usuario, el controlador de nuestra arquitectura lo recupera utilizando el caso de uso GetUserByCriteriaUseCase y lo devuelve al controlador de Laravel que lo envía al solicitante de la petición.

Conclusiones

Aunque DDD y Arquitectura Hexagonal parezcan temas complejos al principio, y sin duda lo son a nivel teórico, a nivel practico su implementación no se traduce en absoluto en miles de líneas de código, todo lo contrario.

De hecho nuestros métodos son mucho más atómicos, las responsabilidades están bien definidas, el software es más robusto, fiable, escalable, y detalle a tomar en cuenta, reutilizable.

Otro punto a favor es que al margen de intervenciones en los detalles de infraestructura, que precisamente por utilizar una arquitectura de este tipo se vuelven algo relativamente trivial gracias al hecho de que nuestra lógica está encapsulada en la arquitectura, con solo copiar la carpeta src y llevártela a otro lado debería bastar para que todo funcione.

Agradecimientos

Espero que el contenido de este articulo pueda ayudar a quien como yo sigue buscando soluciones intentando mejorar día tras día y noche tras noche.

Todo está en proceso y las cosas siempre se pueden mejorar pero por algo se empieza y cualquier feedback será bienvenido.

Quiero agradecer a Rafael Gomez y Javier Ferrer de CodelyTV por compartir a través de su canal en Youtube sus conocimientos y experiencia con toda la comunidad de desarrolladores de una manera muy clara y divertida.
¡Hay cosas finísimas en el canal!

Gracias por haber llegado hasta el final y hasta pronto.

--

--