DDD y Arquitectura Hexagonal en Symfony Flex

Vamos a crear un proyecto de Symfony Flex siguiendo Domain Driven Design y la arquitectura hexagonal.

DDD y Arquitectura Hexagonal en Symfony Flex
🇬🇧 Article available in english

Escribí un pequeño proyecto para probar cómo podemos seguir la arquitectura hexagonal y DDD en un proyecto vacío de Symfony Flex. La idea era mantener todo nuestro código agnóstico de Symfony tanto como fuera posible.

Como esto es solo un ejemplo, hice solo un endpoint para crear clientes potenciales (aka. Leads). Pero implementé un Event Bus sincrónico y asincrónico usando MySql. También tenemos un Command Query y un Command Bus porque también estamos siguiendo CQRS.

En este primer artículo voy a explicar cómo son nuestros controladores, ya que son la capa que más cercana a Symfony, y la usamos para procesar peticiones y rutas.

En el futuro cubriremos más temas como:

  1. Introducción y controladores en un proyecto DDD Symfony Flex (este artículo)
  2. Estructurar un proyecto DDD en Symfony Flex
  3. Crear un Comman Bus y un Query Bus
  4. Crear un Event Bus síncrono y asíncrono con MySQL
  5. Cómo configurar PHP Unit para hacer tests unitarios
  6. Configurar e instalar Behat para hacer test de integración

Vamos con el primer artículo de esta serie:

Controladores

Tenemos un solo controlador para cada acción de la API que implementemos. Ahora mismo solo tengo una acción para crear Leads y puedes encontrar el controlador en app/Controller/Leads/LeadsPostController.php.

Estamos almacenando los controladores en la carpeta app porque es nuestro único código que depende totalmente del framework. El resto de nuestro código estará en la carpeta src como veremos más adelante.

Como se puede ver en el controlador es muy simple. Solo obtenemos la petición en tipos primitivos y creamos un comando que entendemos, está en nuestra capa aplicación, y así no pasamos ningún valor del controlador a nuestra capa de dominio. Si en el futuro, queremos crear leads desde la línea de comandos, por ejemplo, podemos utilizar este mismo comando.

Si todo está bien devolvemos un HTTP_CREATED o, si no, la respuesta con el mensaje para aclarar lo que está pasando.

$name = $request->get('name');
$email = $request->get('email');

try {
    $this->commandBus->dispatch(new CreateLeadCommand($name, $email));
    return new Response(null, Response::HTTP_CREATED);
} catch (DuplicatedLeadException $e) {
    return new ErrorResponse($e, Response::HTTP_BAD_REQUEST);
}

Todavía no tenemos ningún ejemplo en este proyecto, pero si queremos obtener algún dato de la API podríamos lanzar una query por el query bus, en vez del command, y obtener así una respuesta para devolverla.

El resultado será algo similar a esto:

$id = $request->get('id');

try {
    /** @var Lead $lead */
    $lead = $this->queryBus->dispatch(new GetLeadCommand($id));
    return new Response($lead);
} catch (NotFoundException $e) {
    return new ErrorResponse($e, Response::HTTP_BAD_REQUEST);
}

Siguiendo esta sencilla estructura podemos tener pequeños controladores que hagan lo que necesitemos.

Es importante que nuestro controlador se limite a transformar y validar la petición a algo que controlemos y entendamos completamente. Debemos evitar pasar primitivas a nuestra carpeta src (la capa de dominio y aplicación).

Cambiar la ruta de los controladores en Symfony

Para mover nuestros controladores a una nueva ruta, en Symfony, tenemos que configurarlos en el archivo config/services.yaml. Con algo así es suficiente.

App\Controller\:
    resource: '../app/Controller/'
    tags: [ 'controller.service_arguments' ]

En unos días hablaré de cómo estructuro el proyecto en carpetas y cómo configurar Symfony para que funcione con esta estructura.

Todo el código está disponible públicamente en este repositorio.