Quando seu monolito PHP começa a rachar
\nVocê já se viu em uma situação em que trocar o banco de dados de MySQL para PostgreSQL se transformou em uma missão de uma semana? Ou quando precisou adicionar um novo cliente de API, mas o código estava tão entrelaçado com o framework que era mais fácil reescrever tudo do zero? Familiar?
\nA maioria dos desenvolvedores PHP, mais cedo ou mais tarde, enfrenta o "macarrão" em controllers e models. Frameworks como Laravel ou Symfony oferecem ferramentas convenientes, mas também impõem um forte acoplamento a eles. Existe uma saída — a Arquitetura Hexagonal (também conhecida como "portas e adaptadores" ou arquitetura hexagonal).
\nNeste artigo, vamos analisar como construir uma aplicação em PHP que não depende de framework, banco de dados ou serviços externos. Você verá exemplos de código que podem ser aplicados imediatamente no seu projeto. Vamos lá!
\n\nO que é Arquitetura Hexagonal e por que você precisa dela?
\nImagine um hexágono. No centro — sua lógica de negócios (domínio). Ao redor — as "portas" (interfaces), através das quais o mundo externo se comunica com o núcleo. E do lado de fora — os "adaptadores", que implementam essas portas: controllers, repositórios, clientes de API.
\nA ideia principal: o núcleo da aplicação não sabe nada sobre o mundo externo. Ele não importa o Laravel, não conhece PDO e não chama curl. Toda a dependência é direcionada para dentro — os adaptadores dependem das portas, e não o contrário.
\nAs vantagens dessa abordagem são óbvias:
\n- \n
- Testabilidade: a lógica de negócios pode ser testada sem banco de dados e requisições HTTP. \n
- Flexibilidade: trocar de BD, fila ou framework é simplesmente escrever um novo adaptador. \n
- Limpeza do código: você não mistura "o que fazer" com "como fazer". \n
Parece mágica? Vamos ver o código.
\n\nEstrutura do projeto: esqueleto básico
\nPara o exemplo, vamos usar um cenário simples: registro de usuário. Precisamos salvar o usuário no BD e enviar um e-mail de boas-vindas.
\nAssim será a estrutura típica de diretórios:
\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 é a lógica de negócios pura. Infrastructure são os adaptadores. Application são os casos de uso (use cases).
Passo 1: O núcleo — domínio e portas
\nEntidade (Entity)
\nVamos começar pelo mais simples — o usuário. É apenas uma classe com dados, sem dependências.
\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\nPorta — interface do repositório
\nAgora definimos como vamos salvar o usuário. Esta é uma porta — uma interface que será implementada pelo 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\nPorta — interface do serviço de e-mail
\nDe forma análoga para o envio de e-mails:
\n<?php\nnamespace App\\Domain\\Service;\n\nuse App\\