Dominando a Arquitetura de PHP: Guia Completo para Boas Práticas e Escalabilidade

Dominando a Arquitetura de PHP: Guia Completo para Boas Práticas e Escalabilidade

“`html





Dominando a Arquitetura de PHP — do projeto ao código


Arquitetura em PHP, na prática

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.

Boas decisões de arquitetura
Camadas e responsabilidades
Testabilidade
Manutenibilidade

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.

Exemplo simplificado (conceito) — sem framework
PHP 8+

<?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.



Ler mais posts no yurideveloper.com.br



“`