Cuando su monolito PHP empieza a resquebrajarse
\n¿Alguna vez se ha encontrado en una situación donde cambiar la base de datos de MySQL a PostgreSQL se convertía en una misión de una semana? ¿O cuando necesitaba añadir un nuevo cliente API, y el código estaba tan entrelazado con el framework que era más fácil escribirlo todo desde cero? ¿Le suena familiar?
\nLa mayoría de los desarrolladores PHP, tarde o temprano, se topan con "espaguetis" en los controladores y modelos. Frameworks como Laravel o Symfony ofrecen herramientas convenientes, pero también imponen un fuerte acoplamiento con ellos mismos. Hay una solución: la Arquitectura Hexagonal (también conocida como "puertos y adaptadores" o arquitectura hexagonal).
\nEn este artículo, analizaremos cómo construir una aplicación en PHP que no dependa del framework, la base de datos o servicios externos. Verá ejemplos de código que puede aplicar inmediatamente en su proyecto. ¡Allá vamos!
\n\n¿Qué es la Arquitectura Hexagonal y para qué sirve?
\nImagine un hexágono. En el centro está su lógica de negocio (dominio). Alrededor, los "puertos" (interfaces) a través de los cuales el mundo exterior se comunica con el núcleo. Y en el exterior, los "adaptadores" que implementan estos puertos: controladores, repositorios, clientes API.
\nLa idea principal: el núcleo de la aplicación no sabe nada del mundo exterior. No importa Laravel, no conoce PDO ni llama a curl. Toda la dependencia apunta hacia adentro: los adaptadores dependen de los puertos, y no al revés.
\nLas ventajas de este enfoque son evidentes:
\n- \n
- Capacidad de prueba: la lógica de negocio se puede probar sin base de datos ni solicitudes HTTP. \n
- Flexibilidad: cambiar la BD, la cola o el framework es simplemente escribir un nuevo adaptador. \n
- Limpieza del código: no mezcla "qué hacer" con "cómo hacerlo". \n
¿Suena a magia? Veamos el código.
\n\nEstructura del proyecto: esqueleto básico
\nPara el ejemplo, tomemos un escenario simple: registro de usuario. Necesitamos guardar el usuario en la BD y enviar un correo de bienvenida.
\nAsí es como se vería una estructura de directorios típica:
\nsrc/\n├── Domain/\n│ ├── Entity/\n│ │ └── User.php\n│ ├── Repository/\n│ │ └── UserRepositoryInterface.php\n│ ├── Service/\n│ │ └── UserRegistrationService.php\n│ └── Event/\n│ └── UserRegisteredEvent.php\n├── Infrastructure/\n│ ├── Persistence/\n│ │ └── DoctrineUserRepository.php\n│ ├── Mail/\n│ │ └── SymfonyMailerAdapter.php\n│ └── Controller/\n│ └── RegisterUserController.php\n└── Application/\n └── UseCase/\n └── RegisterUserUseCase.php\nObserve: Domain es la lógica de negocio pura. Infrastructure son los adaptadores. Application son los casos de uso (use cases).
Paso 1: El núcleo — dominio y puertos
\nEntidad (Entity)
\nEmpecemos por lo más simple: el usuario. Es solo una clase con datos, sin dependencias.
\n<?php\nnamespace App\\Domain\\Entity;\n\nclass User\n{\n public function __construct(\n private readonly string $id,\n private string $name,\n private string $email\n ) {}\n\n public function getId(): string\n {\n return $this->id;\n }\n\n public function getName(): string\n {\n return $this->name;\n }\n\n public function getEmail(): string\n {\n return $this->email;\n }\n}\n\nPuerto — interfaz del repositorio
\nAhora definamos cómo guardaremos al usuario. Esto es un puerto — una interfaz que será implementada por un adaptador.
\n<?php\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Entity\\User;\n\ninterface UserRepositoryInterface\n{\n public function save(User $user): void;\n public function findByEmail(string $email): ?User;\n}\n\nPuerto — interfaz del servicio de correo
\nDe manera similar para el envío de correos:
\n<?php\nnamespace App\\Domain\\Service;\n\nuse App\\