Skip to content

Arquitectura de Módulos — Modules/<Module>

Esta guía describe la arquitectura modular introducida en Sistema Bautista a partir de los módulos CRM y Membresía. Define la estructura de carpetas, la responsabilidad de cada capa y las convenciones que deben seguirse al crear un módulo nuevo con este patrón.

Por qué existe esta arquitectura

La arquitectura Slim Framework de 5 capas (Routes → Controller → Service → Model → DB) funciona bien para módulos CRUD simples. Sin embargo, cuando un módulo contiene subdominios complejos —facturación, eventos de dominio, repositorios con implementaciones intercambiables— la organización en carpetas raíz (service/, models/, controller/) dispersa el código de un mismo módulo en múltiples lugares del proyecto.

La estructura Modules/<Module> agrupa todo el código de un módulo en una sola carpeta, con separación interna por capas DDD. Esto facilita:

  • Navegar y entender un módulo sin saltar entre carpetas raíz.
  • Aplicar Domain-Driven Design con Domain, Application e Infrastructure diferenciados.
  • Definir contratos públicos explícitos para la integración entre módulos.
  • Escalar un módulo complejo sin impacto en el resto del sistema.

Módulos actuales

MóduloRutaComplejidad
CRMModules/Crm/Media — sin repositorios ni eventos de dominio
MembresíaModules/Membresia/Alta — con repositorios, eventos, subdominio de facturación

Árbol de directorios completo

La siguiente estructura toma como referencia el módulo Membresía, que es el más completo. Las carpetas marcadas con (*) están presentes sólo en Membresía.

Modules/<Module>/

├── Application/
│   ├── Services/                   # Casos de uso que orquestan el dominio
│   │   ├── Cache/                  # Servicios con caché integrado
│   │   ├── ExternalData/           # Datos desde sistemas externos (CRM)
│   │   ├── Facturacion/            # Subdominio de facturación (*)
│   │   ├── Grouping/               # Agrupaciones de entidades (*)
│   │   └── Validation/             # Validaciones de negocio complejas (*)
│   └── Validators/                 # Validadores de estructura HTTP (middleware)

├── Contracts/                      # Interfaces públicas del módulo

├── Domain/
│   ├── Contracts/                  # Interfaces de repositorio del dominio (*)
│   ├── Events/                     # Eventos de dominio
│   │   ├── Facturacion/            # Eventos del subdominio de facturación (*)
│   │   └── Listeners/              # Listeners de eventos
│   ├── Facturacion/                # Subdominio de facturación (*)
│   │   ├── Aggregates/             # Raíces de agregado
│   │   ├── Builder/                # Constructores de agregados
│   │   │   ├── Exceptions/
│   │   │   └── Traits/
│   │   ├── Entities/               # Entidades del subdominio
│   │   ├── Enums/                  # Enumeraciones tipadas
│   │   ├── Exceptions/             # Excepciones de dominio
│   │   ├── Interface/              # Interfaces internas del subdominio
│   │   ├── Providers/              # Proveedores de datos de dominio
│   │   ├── Repositories/           # Interfaces de repositorio del subdominio
│   │   ├── Services/               # Servicios de dominio puros
│   │   └── ValueObjects/           # Objetos de valor inmutables
│   └── Shared/
│       └── DTO/                    # DTOs compartidos entre capas del dominio

├── Infrastructure/
│   ├── Documentation/              # Clases de documentación OpenAPI
│   │   └── Resources/              # Recursos OpenAPI por entidad
│   ├── Http/
│   │   ├── Controllers/            # Controladores Slim (HTTP puro)
│   │   └── Routes/                 # Definición de rutas Slim
│   └── Persistence/
│       ├── Models/                 # Modelos de acceso a base de datos (PDO)
│       ├── Queries/                # Query builders especializados (*)
│       └── Repositories/           # Implementaciones concretas de repositorios (*)

└── Presentation/
    └── DTOs/                       # DTOs de request y response HTTP
        ├── Disciplina/             # DTOs por entidad (ejemplo Membresía)
        ├── Enums/
        ├── Facturacion/
        └── ...

Descripción de cada capa

Domain

Contiene la lógica de negocio pura del módulo. No tiene dependencias de frameworks, bases de datos, ni HTTP.

Qué va aquí:

  • Entities: Objetos con identidad que encapsulan comportamiento de negocio.
  • ValueObjects: Objetos inmutables que representan conceptos del dominio (ej. Importe, FechaVencimiento).
  • Aggregates: Raíces de agregado que garantizan consistencia interna.
  • Enums: Enumeraciones tipadas para estados y categorías del dominio.
  • Exceptions: Excepciones que representan violaciones de reglas de negocio.
  • Repositories (interfaces): Contratos de persistencia definidos por el dominio, no por la infraestructura.
  • Services: Servicios de dominio puros para lógica que no pertenece a una sola entidad.
  • Events: Eventos que ocurren como resultado de operaciones de dominio.
  • Listeners: Manejadores de eventos de dominio.
  • Shared/DTO: DTOs de transferencia interna entre componentes del dominio.

Regla fundamental: ninguna clase en Domain/ importa clases de Infrastructure/, Application/, ni frameworks externos.


Application

Orquesta los objetos de dominio para implementar casos de uso. Coordina transacciones, validaciones de negocio y flujos de trabajo compuestos.

Qué va aquí:

  • Services: Implementan casos de uso concretos. Invocan entidades, repositorios y servicios de dominio. Gestionan transacciones.
  • Services/Cache: Variantes de servicios con caché para operaciones de lectura frecuente.
  • Services/Validation: Validadores de reglas de negocio complejas que requieren consultas a la base de datos.
  • Validators: Validadores de estructura HTTP. Se aplican como middleware en las rutas. Solo verifican tipos, formatos y presencia de campos. Devuelven 422 si fallan.

Diferencia entre Validators y Services/Validation:

Application/Validators/Application/Services/Validation/
Cuándo se ejecutaAntes del Controller (middleware)Dentro del Service (tras el Controller)
Qué validaEstructura: tipos, formatos, campos requeridosReglas de negocio: unicidad, existencia de relaciones, límites
Accede a DBNo
Respuesta en falloHTTP 422Excepción de negocio / HTTP 422

Infrastructure

Implementa los detalles técnicos: HTTP, base de datos, documentación. Depende del dominio pero el dominio no depende de ella.

Qué va aquí:

  • Http/Controllers: Controladores Slim. Solo manejan HTTP: extraen datos del request, invocan servicios de Application y construyen la respuesta. Sin lógica de negocio.
  • Http/Routes: Definición de rutas Slim con sus middlewares de validación.
  • Persistence/Models: Modelos de acceso a datos con PDO directo. Ejecutan SQL, mapean resultados a DTOs, implementan soft delete.
  • Persistence/Repositories: Implementaciones concretas de las interfaces definidas en Domain/Contracts/ o Domain/.../Repositories/. Usan Doctrine DBAL o PDO.
  • Persistence/Queries: Query builders especializados para consultas complejas que no encajan en un Model simple.
  • Documentation: Clases que generan la especificación OpenAPI del módulo.

Presentation

Capa de transferencia de datos HTTP. Contiene únicamente DTOs organizados por entidad.

Qué va aquí:

  • DTOs de request: Objetos que representan los datos que llegan del cliente. Generalmente creados por el Controller a partir del body HTTP.
  • DTOs de response: Objetos que representan los datos que se devuelven al cliente.
  • Enums de presentación: Enumeraciones usadas en la serialización de respuestas.

Los DTOs de Presentation/ no contienen lógica de negocio. Solo tienen propiedades tipadas y métodos de construcción (fromArray()) y serialización (toArray()).


Contracts

Interfaces públicas que el módulo expone al exterior. Permiten que otros módulos dependan de una abstracción en lugar de una implementación concreta.

Cuándo usar Contracts/ vs Domain/Contracts/:

Contracts/ (raíz del módulo)Domain/Contracts/
AudienciaOtros módulos del sistemaImplementaciones de infraestructura del mismo módulo
EjemploMiembrosServiceInterface para que otro módulo consulte miembrosCategoriaMembresiaRepository para que Doctrine implemente

Comparación con la arquitectura Slim (legacy del proyecto)

AspectoSlim 5 capas (carpetas raíz)Modules/<Module> (DDD)
OrganizaciónPor tipo de archivo (service/, models/, controller/)Por módulo (Modules/Crm/, Modules/Membresia/)
CohesiónTodo el sistema en cada carpeta raízTodo el módulo en una carpeta
DominioSin capa de dominio explícitaDomain/ con entidades, VOs, eventos
RepositoriosModel como repositorio implícitoInterfaz en dominio + implementación en infraestructura
ContratosSin contratos entre módulosContracts/ explícita
ComplejidadBaja — adecuada para CRUD simpleAlta — necesaria para subdominios complejos
NamespaceApp\service\Ventas\...Membresia\Application\Services\...
Cuándo usarlaMódulos con lógica de negocio directaMódulos con subdominios, eventos o repositorios intercambiables

La arquitectura Modules/<Module> no reemplaza la arquitectura Slim en todos los casos. Para módulos con CRUD directo sin lógica compleja, la estructura de 5 capas sigue siendo apropiada. Usar Modules/<Module> cuando el módulo justifica la separación adicional.


Flujo de una request

HTTP Request

Infrastructure/Http/Routes/<Entidad>Route.php
    ↓ middleware
Application/Validators/<Entidad>Validator.php   → 422 si falla

Infrastructure/Http/Controllers/<Entidad>Controller.php
    ↓ instancia DTOs de Presentation/DTOs/
Application/Services/<Servicio>.php
    ↓ valida reglas de negocio
    ↓ gestiona transacción
Domain/...                                       → lógica pura de negocio

Infrastructure/Persistence/Repositories/<Impl>   → acceso a DB
    ↓ o
Infrastructure/Persistence/Models/<Modelo>       → acceso a DB

PostgreSQL (schema del tenant activo)

Cuándo crear un nuevo módulo con esta estructura

Usar Modules/<Module> cuando el módulo cumple al menos dos de estas condiciones:

  1. Tiene más de un subdominio con lógica propia (ej. facturación separada del core del módulo).
  2. Necesita repositorios con implementaciones intercambiables (ej. Doctrine vs PDO).
  3. Genera eventos de dominio que otros componentes deben escuchar.
  4. Expone contratos que otros módulos consumen (más allá de llamadas directas a servicios).
  5. El módulo tiene dominio rico: entidades con comportamiento, value objects, reglas de negocio que no dependen de DB.

Para módulos CRUD simples sin lógica compleja, continuar usando la estructura de 5 capas en carpetas raíz.


Guía para crear un módulo nuevo

1. Estructura mínima de partida

Para un módulo nuevo, comenzar con las carpetas esenciales:

Modules/<NuevoModulo>/
├── Application/
│   ├── Services/
│   └── Validators/
├── Contracts/
├── Domain/
├── Infrastructure/
│   ├── Http/
│   │   ├── Controllers/
│   │   └── Routes/
│   └── Persistence/
│       └── Models/
└── Presentation/
    └── DTOs/

Agregar subcarpetas adicionales (Domain/Events/, Domain/Facturacion/, Persistence/Repositories/) sólo cuando el módulo las necesite concretamente.

2. Registrar las rutas en el entry point

Las rutas del módulo se registran en index.php o en el archivo de configuración de rutas Slim del proyecto. El archivo principal de rutas del módulo sigue la convención <Modulo>Routes.php.

3. Registrar los servicios en el contenedor DI

Los servicios de Application y las implementaciones de repositorio se registran en el contenedor PHP-DI. Las interfaces del dominio deben mapearse a sus implementaciones de Infrastructure.

4. Configurar el autoloader

El namespace raíz del módulo (ej. Membresia\) debe estar registrado en composer.json bajo autoload.psr-4.


Convenciones de nomenclatura

Namespaces PHP

Los módulos usan el nombre del módulo como namespace raíz, sin prefijo App\:

Membresia\Application\Services\<NombreService>
Membresia\Application\Validators\<Entidad>Validator
Membresia\Contracts\<Nombre>Interface
Membresia\Domain\Facturacion\Entities\<Entidad>
Membresia\Domain\Facturacion\ValueObjects\<Nombre>
Membresia\Domain\Events\<Nombre>Event
Membresia\Infrastructure\Http\Controllers\<Entidad>Controller
Membresia\Infrastructure\Http\Routes\<Entidad>Route
Membresia\Infrastructure\Persistence\Models\<Nombre>
Membresia\Infrastructure\Persistence\Repositories\Doctrine<Nombre>Repository
Membresia\Presentation\DTOs\<Entidad>\<Nombre>DTO

Crm\Application\Services\<NombreService>
Crm\Infrastructure\Http\Routes\<Nombre>Routes

Clases

TipoSufijoEjemplo
ControllerControllerDisciplinaController
RouteRoute o RoutesDisciplinaRoute, MembresiaRoutes
Application ServiceServiceDisciplinaService, MembresiaCacheService
Domain ServiceServiceFacturacionDomainService
Validator (HTTP)ValidatorDisciplinaValidator
Model (persistencia PDO)sin sufijo o ModelDisciplina, DisciplinaModel
Repository interfaceRepository o RepositoryInterfaceCategoriaMembresiaRepository
Repository implementationDoctrine<Nombre>RepositoryDoctrineCategoriaMembresiaRepository
Aggregatesin sufijo especialFacturacionAggregate
Value Objectsin sufijo especialImporteFactura, FechaVencimiento
EventEventMiembroBajaEvent, MiembroReactivacionEvent
ListenerListenerMiembroBajaListener
DTO de presentaciónDTO o RequestDisciplinaDTO, CreateDisciplinaRequest
Contratos públicosInterface o ServiceInterfaceMiembrosServiceInterface

Archivos de rutas

Cada entidad principal tiene su propio archivo de rutas. Existe además un archivo maestro del módulo que los agrupa e importa:

Infrastructure/Http/Routes/
├── <Modulo>Routes.php          # Archivo maestro: registra el grupo principal
├── <Entidad1>Route.php         # Rutas de la entidad 1
└── <Entidad2>Route.php         # Rutas de la entidad 2

Multi-tenancy en módulos

Los módulos bajo Modules/<Module> respetan el mismo modelo de multi-tenancy del sistema. El ConnectionManager está disponible para inyección y la conexión activa respeta el search_path configurado por el middleware de conexión a partir del header X-Schema.

Cada tabla creada para un módulo debe tener definido su nivel de schema en las migraciones:

  • EMPRESA: datos compartidos entre todas las sucursales de una empresa.
  • SUCURSAL: datos del schema de la sucursal activa.
  • CAJA: datos del schema de la caja activa.

La separación oficial/prueba (base de datos con sufijo _p) aplica de la misma manera que en el resto del backend.


Referencias


Última actualización: 2026-02-18