Blog de Gonzalo

Blog de programación de Gonzalo López

ARQUITECTURA HEXAGONAL

PHP

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.

  1. Capa de Dominio o Domain: 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.
  2. Capa de Aplicación o Application La capa de aplicación orquesta la lógica, pero no sabe cómo se guarda el usuario (en MySQL, 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 uso: 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 uso (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.
  3. Capa de Infraestructura o Infrastructure
    • Propósito: Contiene los detalles técnicos y las implementaciones concretas que conectan la aplicación con el mundo exterior (bases de datos, APIs, interfaces de usuario, etc.).
    • Componentes:
      • Adaptadores de salida: Implementaciones de los puertos de salida, como un repositorio para MySQL (MySqlUserRepository).
      • Adaptadores de entrada: Componentes que reciben solicitudes externas, como controladores HTTP o comandos CLI (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 uso (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
    1. Capa de Dominio
      • Entidad (src/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;
        }
        

    2. Capa de Aplicación
      • Caso de uso (src/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
            }
        }
        


    3. Capa de Infraestructura
        Adaptador de entrada (src/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 (src/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(),
                ]);
            }
        }
        


      Compartir en twitter