ARQUITECTURA HEXAGONAL

La arquitectura hexagonal organiza el código en tres capas principales: Dominio, Aplicación e Infraestructura. Estas capas se conectan mediante puertos o interfaces y adaptadores, asegurando que la lógica de negocio esté aislada de los sistemas externos.

Capa de Dominio

La capa de dominio es pura, sin referencias a bases de datos, frameworks o tecnologías externas.

Propósito:

Es el núcleo de la aplicación, donde reside la lógica de negocio pura. Contiene las reglas y conceptos fundamentales del sistema, independientes de tecnologías externas, bases de datos, frameworks, etc.

Componentes

  • Entidades: Objetos que representan conceptos del negocio, con sus reglas y comportamientos (por ejemplo, un User con validaciones).
  • Puertos de salida: Interfaces que definen cómo el dominio interactúa con sistemas externos (por ejemplo, UserRepository para guardar usuarios).

Características:

  • No depende de ninguna otra capa.
  • No contiene código relacionado con bases de datos, APIs o frameworks.
  • Es el corazón de la aplicación, donde se codifican las reglas de negocio esenciales.

Capa de Aplicación

La capa de aplicación orquesta la lógica, pero no sabe cómo se guarda el usuario, en MySQL, redis o un archivo, etc.

Propósito:

Contiene la lógica de aplicación, que orquesta las interacciones entre el dominio y los sistemas externos. Define los casos de uso del sistema (por ejemplo, "registrar un usuario").

Componentes

  • Casos de use: Clases que implementan flujos de negocio, como RegisterUserUseCase, que coordinan entidades y puertos.
  • Puertos de entrada (opcional): Interfaces que definen cómo el exterior puede interactuar con los casos de use, por ejemplo, RegisterUserService.

Características:

  • Depende del dominio (usa entidades y puertos de salida).
  • No depende de la infraestructura, no sabe si los datos se guardan en MySQL o un archivo.
  • Es el puente entre el dominio y el mundo exterior.

Capa de Infraestructura

La capa de infraestructura implementa los puertos definidos en las capas superiores y conecta la aplicación con sistemas externos.

Propósito:

Contiene los detalles técnicos y las implementaciones concretas que conectan la aplicación con el mundo exterior, como bases de datos, APIs, interfaces de usuario, etc.

Componentes

  • Adaptadores de salida: Implementaciones de los puertos de salida, como un repositorio para MySQL, por ejemplo "MySqlUserRepository".
  • Adaptadores de entrada: Componentes que reciben solicitudes externas, como controladores HTTP o comandos CLI, por ejemplo "RegisterUserController".

Características:

  • Depende del dominio y la aplicación, implementa los puertos.
  • Contiene código específico de tecnologías (por ejemplo, PDO para MySQL, Guzzle para HTTP).
  • Es intercambiable: puedes cambiar un adaptador (por ejemplo, de MySQL a MongoDB) sin afectar el dominio o la aplicación.

Relación entre las capas

  • Flujo de dependencias: Las dependencias van de afuera hacia adentro (Infraestructura → Aplicación → Dominio). El dominio es independiente, la aplicación depende del dominio, y la infraestructura depende de ambas.
  • Puertos y adaptadores:
    • Los puertos son interfaces definidas en el dominio (salida) o la aplicación (entrada) que especifican contratos.
    • Los adaptadores en la infraestructura implementan esos contratos para conectar con sistemas externos.
  • Aislamiento: El dominio y la aplicación no saben cómo se implementan los adaptadores, lo que permite cambiar tecnologías sin modificar la lógica de negocio.

Cómo interactúan las capas

  • Entrada: El adaptador de entrada (RegisterUserController) recibe una solicitud HTTP y la traduce en una llamada al caso de use (RegisterUserUseCase).
  • Aplicación: El caso de uso crea una entidad (User) y usa un puerto de salida (UserRepository) para guardar los datos.
  • Dominio: La entidad contiene las reglas de negocio, y el puerto define el contrato para la persistencia.
  • Salida: El adaptador de salida (MySqlUserRepository) implementa el puerto, conectando con una base de datos MySQL.

Ventajas de las capas en la arquitectura hexagonal

  • Independencia: El dominio no depende de tecnologías externas, lo que facilita cambiar bases de datos o interfaces.
  • Testabilidad: Puedes probar la capa de dominio y aplicación con mocks, sin necesidad de infraestructura real.
  • Modularidad: Puedes añadir nuevos adaptadores (por ejemplo, un repositorio para MongoDB) sin modificar el núcleo.
  • Mantenibilidad: La separación clara de responsabilidades hace que el código sea más fácil de entender y modificar.
La estructura de árbol del proyecto sería:

project/
├── Application/
│   └── RegisterUserUseCase.php         # Capa de Aplicación: Caso de uso
├── Domain/
│   │   ├── Entities/
│   │   └── User.php                   # Capa de Dominio: Entidad
│   └── Ports/
│       └── UserRepository.php         # Capa de Dominio: Puerto de salida
├── Infrastructure/
│   ├── Http/
│   │   │   └── RegisterUserController.php # Capa de Infraestructura: Adaptador de entrada
│   └── Persistence/
│       └── MySqlUserRepository.php    # Capa de Infraestructura: Adaptador de salida

Ejemplo de código

Capa de Dominio

  • Entidad "/m>/Domain/Entities/User.php":

    namespace App\Domain\Entities;
    
    class User
    {
        private $name;
        private $email;
    
        public function __construct(string $name, string $email)
        {
            if (empty($name) || empty($email)) {
                throw new \InvalidArgumentException('Name and email are required');
            }
            $this->name = $name;
            $this->email = $email;
        }
    
        public function getName(): string
        {
            return $this->name;
        }
    
        public function getEmail(): string
        {
            return $this->email;
        }
    }
    
  • Puerto de Salida

    namespace App\Domain\Ports;
    
    use App\Domain\Entities\User;
    
    interface UserRepository
    {
        public function save(User $user): void;
    }
    

Capa de Aplicación

  • Caso de uso "Application/RegisterUserUseCase.php":

    namespace App\Application;
    
    use App\Domain\Entities\User;
    use App\Domain\Ports\UserRepository;
    
    class RegisterUserUseCase
    {
        private $userRepository;
    
        public function __construct(UserRepository $userRepository)
        {
            $this->userRepository = $userRepository;
        }
    
        public function execute(string $name, string $email): void
        {
            $user = new User($name, $email); // Usa la entidad del dominio
            $this->userRepository->save($user); // Usa el puerto de salida
        }
    }
    

Capa de Infraestructura

  • Adaptador de entrada "Infrastructure/Http/RegisterUserController.php":

    namespace App\Infrastructure\Http;
    
    use App\Application\RegisterUserUseCase;
    use Psr\Http\Message\ResponseInterface;
    use Psr\Http\Message\ServerRequestInterface;
    
    class RegisterUserController
    {
        private $registerUserUseCase;
    
        public function __construct(RegisterUserUseCase $registerUserUseCase)
        {
            $this->registerUserUseCase = $registerUserUseCase;
        }
    
        public function register(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
        {
            $data = json_decode($request->getBody()->getContents(), true);
            try {
                $this->registerUserUseCase->execute($data['name'], $data['email']);
                $response->getBody()->write(json_encode(['message' => 'User registered successfully']));
                return $response->withStatus(201);
            } catch (\InvalidArgumentException $e) {
                $response->getBody()->write(json_encode(['error' => $e->getMessage()]));
                return $response->withStatus(400);
            }
        }
    }
    
  • Adaptador de salida "Infrastructure/Persistence/MySqlUserRepository.php":

    namespace App\Infrastructure\Persistence;
    
    use App\Domain\Entities\User;
    use App\Domain\Ports\UserRepository;
    use PDO;
    
    class MySqlUserRepository implements UserRepository
    {
        private $pdo;
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        public function save(User $user): void
        {
            $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
            $stmt->execute([
                'name' => $user->getName(),
                'email' => $user->getEmail(),
            ]);
        }
    }