“`html
Dominando a Arquitetura de PHP — do projeto ao código
Um guia técnico (e direto ao ponto) para você estruturar aplicações PHP de forma consistente:
camadas claras, dependências controladas, regras de negócio isoladas e um fluxo de execução previsível.
01Defina camadas com responsabilidades bem recortadas
Em arquitetura PHP, o maior ganho vem de separar quem faz o quê.
Eu penso em camadas assim: entrada (HTTP/CLI), casos de uso,
domínio (regras) e infraestrutura (IO).
Quando você mistura tudo (controlador chamando SQL, regra no template, validação espalhada),
o código passa a “crescer por inércia”. O objetivo é impedir isso desde o início.
- Controllers/Handlers: traduzem entrada (request) em intenção (chamada de caso de uso).
- Casos de uso (Application): coordenam execução e transações.
- Domínio: contém regras de negócio puras (sem depender de banco/HTTP).
- Infra: implementa persistência, integrações, mensageria, etc.
Regra de ouro
Se a regra de negócio precisa de acesso a banco, ela não é regra pura — transforme em dependência via
interface e mantenha a regra “limpa”.
Resultado: você ganha previsibilidade e reduz o custo de mudanças.
Dica: comece com poucos arquivos e cresça com disciplina. Estrutura não precisa ser enorme para ser correta.
02Modele dependências: “de onde vem o comportamento?”
Quando o projeto cresce, o que mais pesa é o acoplamento.
A arquitetura precisa dizer claramente como cada parte acessa o que precisa.
Em PHP, eu prefiro um fluxo onde o domínio não sabe como acessar banco/HTTP.
Interfaces no domínio / contratos na borda
Use interfaces para abstrair persistência e integrações.
A implementação real fica na camada de infraestrutura.
- Domínio depende de contratos, não de classes concretas.
- Infra implementa e injeta nos casos de uso.
- Você consegue testar regras sem montar o mundo.
Evite “service” genérico demais
Um Service.php que faz tudo vira o novo ponto de falha.
Quebre por intenção: RegistrarCompra, CancelarPedido, AtualizarPerfil.
- Nomeie por caso de uso, não por tecnologia.
- Isola transação e consistência no local certo.
Uma boa arquitetura faz a dependência fluir em uma direção natural:
entrada → caso de uso → domínio (contratos) e domínio → saída via dependências abstratas.
03Pipeline de execução: validação, intenção e resultado
Eu gosto de tratar o fluxo como um pipeline de significado: o request vira uma intenção tipada,
a regra é aplicada, e o resultado volta como um objeto (sucesso/erro) com contexto.
Prática recomendada: separa validação de formato vs. validação de regras.
- Formato/contorno: tipagem, campos obrigatórios, tipos (camada de entrada).
- Regras de negócio: invariantes do domínio (camada de domínio/caso de uso).
- Persistência: somente após garantir consistência.
Erros com semântica
Em vez de retornar strings soltas, retorne erros com categoria.
Isso reduz “gambiarras” no front e melhora logs/observabilidade.
- Erros de validação (client)
- Erros de domínio (business)
- Falhas técnicas (infra)
O ponto é simples: se você consegue explicar o fluxo em 3 ou 4 frases, você está no caminho certo.
Se você precisa “consultar o código” toda vez, sua arquitetura ainda não está dizendo a verdade.
04Estrutura de pastas e padrão de organização que escala
Uma organização coerente reduz atrito.
Eu recomendo separar por camadas e manter dependências alinhadas.
Mesmo sem framework, você consegue manter o projeto legível.
Exemplo de estrutura (enxuta e escalável)
src/
App/
UseCases/
RegisterUser.php
DTO/
RegisterUserCommand.php
Domain/
User/
User.php
UserRules.php
Contracts/
UserRepository.php
Infra/
Persistence/
MysqlUserRepository.php
Http/
Controllers/
RegisterUserController.php
Requests/
RegisterUserRequest.php
public/
index.php
Observação: “UseCases” e “Domain” ficam separados. Persistência fica em “Infra”.
HTTP não deve atravessar camadas.
Padrões práticos que evito
- Classes com nome genérico (ex.: “Manager”, “Helper” sem contexto).
- SQL espalhado por controladores e templates.
- Modelo de domínio como DTO: domínio precisa preservar invariantes.
- Dependência circular (soluções “rápidas” que viram dívida).
Se você quiser, consigo adaptar essa estrutura para um projeto real seu (API, monólito, CLI, filas, etc.).
05Exemplo: caso de uso + domínio + repositório (com separação real)
Abaixo vai um exemplo que mostra o que eu considero “arquitetura bem feita” em PHP:
o controller traduz entrada, o caso de uso coordena, o domínio aplica regras e o repositório faz IO.
<?php
// src/App/DTO/RegisterUserCommand.php
final class RegisterUserCommand {
public function __construct(
public readonly string $email,
public readonly string $name
) {}
}
// src/Domain/User/UserRules.php
final class UserRules {
public static function validateEmail(string $email): void {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new DomainException("E-mail inválido.");
}
}
public static function validateName(string $name): void {
$name = trim($name);
if ($name === "" || mb_strlen($name) < 2) {
throw new DomainException("Nome deve ter pelo menos 2 caracteres.");
}
}
}
// src/Domain/Contracts/UserRepository.php
interface UserRepository {
public function existsByEmail(string $email): bool;
public function save(User $user): void;
}
// src/Domain/User/User.php
final class User {
public function __construct(
public readonly string $email,
public string $name
) {}
public function rename(string $name): void {
UserRules::validateName($name);
$this->name = trim($name);
}
}
// src/App/UseCases/RegisterUser.php
final class RegisterUser {
public function __construct(
private readonly UserRepository $users
) {}
public function execute(RegisterUserCommand $cmd): void {
UserRules::validateEmail($cmd->email);
UserRules::validateName($cmd->name);
if ($this->users->existsByEmail($cmd->email)) {
throw new DomainException("Já existe um usuário com este e-mail.");
}
$user = new User($cmd->email, trim($cmd->name));
$this->users->save($user);
}
}
// src/Http/Controllers/RegisterUserController.php
final class RegisterUserController {
public function __construct(
private readonly RegisterUser $useCase
) {}
public function handle(array $request): array {
// Camada de entrada: validação de formato (ex.: campos presentes)
if (!isset($request['email'], $request['name'])) {
return ['status' => 400, 'error' => 'Campos obrigatórios: email e name.'];
}
try {
$cmd = new RegisterUserCommand($request['email'], $request['name']);
$this->useCase->execute($cmd);
return ['status' => 201, 'message' => 'Usuário registrado com sucesso.'];
} catch (DomainException $e) {
return ['status' => 422, 'error' => $e->getMessage()];
} catch (Throwable $e) {
// Erro técnico (infra)
return ['status' => 500, 'error' => 'Falha interna.'];
}
}
}
Mesmo sendo simplificado, dá para ver a divisão: o domínio não toca em HTTP nem em banco; o controller não conhece detalhes de persistência.
Quer continuar evoluindo essa base?
Recomendo ler os próximos posts para consolidar arquitetura: testes, camada de persistência, contratos e estratégias de versionamento para evitar refatorações dolorosas.
“`
Sou Apaixonado pela programação e estou trilhando o caminho de ter cada diz mais conhecimento e trazer toda minha experiência vinda do Design para a programação resultando em layouts incríveis e idéias inovadoras! Conecte-se Comigo!