Seu código é uma fortaleza, não um espaguete
\n\nVocê já precisou reescrever um projeto do Laravel para o Symfony, ou migrar de MySQL para PostgreSQL? Se sim, conhece bem a dor: controllers cheios de SQL, models que sabem tudo sobre requisições HTTP e uma lógica de negócio que morre ao trocar de framework. Familiar?
\n\nBem-vindo ao mundo da Arquitetura Hexagonal em PHP (também conhecida como Ports & Adapters). Não é um buzzword da moda, mas sim uma tábua de salvação para projetos que desejam viver por muito tempo. Neste artigo, vamos construir uma aplicação PHP que não tem medo de mudanças: trocar de banco de dados, migrar para um novo cache ou integrar uma API diferente — tudo isso se torna uma simples substituição de adaptador, e não uma reescrita de metade do código.
\n\nO que é a Arquitetura Hexagonal, afinal?
\n\nImagine que sua aplicação é um núcleo (core) que não sabe de onde os dados vêm: web, console, bot do Telegram ou testes. Em volta desse núcleo, temos os portos (interfaces) e, externamente, os adaptadores (implementações). A arquitetura hexagonal em PHP permite separar a lógica de negócio do código de infraestrutura. Você escreve as regras uma vez e as reutiliza em qualquer lugar.
\n\nPrincípios-chave:
\n- \n
- Inversão de dependências — o código de alto nível não depende do de baixo nível. É o contrário. \n
- Lógica de negócio isolada — nenhum framework penetra nos seus Use Cases. \n
- Testabilidade — você pode testar o núcleo sem banco de dados ou HTTP. \n
Diferente da Clean Architecture de Robert Martin, a Hexagonal é mais flexível: ela não dita o número de camadas, apenas diz: "Dentro fica o domínio, fora fica a infraestrutura, conecte-os através de interfaces".
\n\nEstrutura do projeto: da bagunça à organização
\n\nVamos direto ao ponto. Aqui está uma estrutura de pastas típica para a Arquitetura Hexagonal em PHP:
\n\nsrc/\n├── Domain/\n│ ├── Entity/\n│ │ └── Order.php\n│ ├── Repository/\n│ │ └── OrderRepositoryInterface.php\n│ └── Service/\n│ └── OrderService.php\n├── Application/\n│ ├── UseCase/\n│ │ └── CreateOrderUseCase.php\n│ └── Port/\n│ ├── Input/\n│ │ └── CreateOrderInputPort.php\n│ └── Output/\n│ └── OrderOutputPort.php\n├── Infrastructure/\n│ ├── Persistence/\n│ │ └── DoctrineOrderRepository.php\n│ └── Http/\n│ └── OrderController.php\n└── Framework/\n └── Symfony/\n └── Kernel.php\n\nPercebeu? Domain e Application não sabem da existência do Doctrine, Symfony ou MySQL. Eles trabalham apenas com interfaces. Os adaptadores vivem em Infrastructure e são conectados via Injeção de Dependência.
Passo 1: Definindo o núcleo — Domain
\n\nVamos começar com a entidade de pedido. Sem anotações de ORM, sem setters que quebram invariantes:
\n\n<?php\n// src/Domain/Entity/Order.php\nnamespace App\\Domain\\Entity;\n\nclass Order\n{\n public function __construct(\n private readonly string $id,\n private readonly string $productName,\n private float $totalAmount,\n private string $status = 'pending'\n ) {}\n\n public function approve(): void\n {\n if ($this->status !== 'pending') {\n throw new \\DomainException('Only pending orders can be approved.');\n }\n $this->status = 'approved';\n }\n\n public function getId(): string { return $this->id; }\n public function getProductName(): string { return $this->productName; }\n public function getTotalAmount(): float { return $this->totalAmount; }\n public function getStatus(): string { return $this->status; }\n}\n\nAqui está o port (interface) para o repositório. Observe: ele não sabe nada sobre Doctrine ou PDO:
\n\n<?php\n// src/Domain/Repository/OrderRepositoryInterface.php\nnamespace App\\Domain\\Repository;\n\nuse App\\