O Domain-Driven Design (DDD) não é apenas um padrão de pastas, mas uma filosofia de design de software que coloca a Regra de Negócio (o Domínio) como o centro de todas as decisões. No Laravel, aplicar DDD significa elevar o nível de maturidade do código, garantindo que o framework sirva à aplicação, e não o contrário.
1. O que é DDD e por que aplicá-lo? Link para o cabeçalho
No desenvolvimento tradicional (MVC puro), é comum encontrarmos lógica de banco de dados em Controllers ou Models gigantes. O DDD propõe o desacoplamento. Ele divide o sistema em camadas para que, se você precisar trocar o banco de dados ou a forma como um comando é disparado, a regra de negócio permaneça intacta.
2. A Anatomia das Camadas e Recursos no Laravel Link para o cabeçalho
Para implementar o DDD com sucesso utilizando os recursos do Laravel, precisamos definir claramente a função de cada componente:
A. O Model (Eloquent) - Camada de Persistência Link para o cabeçalho
No DDD, o Model é apenas um Data Mapper. Ele é o reflexo da tabela no banco de dados.
-
Finalidade: Realizar a comunicação técnica com o banco.
-
O que evitar: Colocar cálculos, validações complexas ou envio de e-mails dentro da Model.
B. O Repository - Abstração de Dados Link para o cabeçalho
O Repository isola o Eloquent da Service.
-
Finalidade: Centralizar as consultas ao banco. A Service não deve saber que existe um
where()ou umfirst(); ela apenas pede ao Repository:buscarPorIdentificador(). -
Vantagem: Facilita a criação de testes unitários e a troca de tecnologias de armazenamento.
C. A Service - O Coração (Domain/Application Service) Link para o cabeçalho
É onde a “mágica” acontece.
-
Finalidade: Orquestrar as regras de negócio. A Service decide o que deve ser feito. Ela recebe dados, valida contra as regras da empresa e chama o Repository para salvar.
-
Exemplo: O método
aplicaUpdateOrCreateque refatoramos é uma lógica de Service.
D. Controller e Command - Camada de Interface (Entry Points) Link para o cabeçalho
São as portas de entrada da aplicação.
-
Finalidade: Receber o input do usuário (via HTTP ou Terminal), validar o formato básico dos dados e repassar para a Service.
-
Regra de Ouro: Devem ser “magros” (Skinny). Se houver um
ifde regra de negócio aqui, ele deve ser movido para a Service.
E. DTO (Data Transfer Object) Link para o cabeçalho
Embora não discutido profundamente antes, o DTO é essencial para transportar dados entre o Command e a Service de forma tipada, evitando o uso de “arrays genéricos” que podem causar erros.
3. Aplicação Prática: Cadastro de Cliente Link para o cabeçalho
Imagine que, ao cadastrar um cliente, precisamos verificar se o CPF é válido e enviar um e-mail de boas-vindas.
A. O Repositório (Interface e Implementação) Link para o cabeçalho
Primeiro, definimos o contrato. Isso permite trocar o banco de dados sem quebrar a Service.
// app/Repositories/Contracts/ClienteRepositoryInterface.php
interface ClienteRepositoryInterface {
public function salvar(array $dados): object;
}
// app/Repositories/Eloquent/ClienteRepository.php
class ClienteRepository implements ClienteRepositoryInterface {
public function salvar(array $dados): object {
return Cliente::create($dados);
}
}
B. A Service (O Coração) Link para o cabeçalho
Aqui aplicamos a lógica. Note que ela recebe o Repositório pelo construtor (Injeção de Dependência).
// app/Services/ClienteService.php
class ClienteService {
public function __construct(
protected ClienteRepositoryInterface $repository
) {}
public function registrarCliente(array $dados): object {
// Regra de Negócio: Validar algo específico do domínio
if ($this->cpfJaExiste($dados['cpf'])) {
throw new \Exception("Este cliente já possui cadastro.");
}
$cliente = $this->repository->salvar($dados);
// Outra ação de orquestração
// Mail::to($cliente->email)->send(new BoasVindas());
return $cliente;
}
}
C. O Controller (A Porta de Entrada) Link para o cabeçalho
Ele é “magro” (Skinny Controller). Apenas repassa o trabalho.
// app/Http/Controllers/ClienteController.php
class ClienteController extends Controller {
public function store(Request $request, ClienteService $service) {
try {
$cliente = $service->registrarCliente($request->all());
return response()->json($cliente, 201);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 422);
}
}
}
Como o Laravel conecta tudo? Link para o cabeçalho
Para que o Laravel saiba que, quando você pede ClienteRepositoryInterface, ele deve entregar o ClienteRepository (Eloquent), usamos o Service Provider.
// app/Providers/RepositoryServiceProvider.php
public function register() {
$this->app->bind(
\App\Repositories\Contracts\ClienteRepositoryInterface::class,
\App\Repositories\Eloquent\ClienteRepository::class
);
}
4. Finalidade de cada recurso no contexto DDD Link para o cabeçalho
| Recurso | Finalidade no DDD | Analogia |
|---|---|---|
| Controller | Entrada: Apenas recebe a requisição (Request), valida o formato básico e entrega para a Service. Não toma decisões de negócio. | O garçom que anota o pedido. |
| Service | Orquestração: Onde reside a regra de negócio. Ela sabe o que deve ser feito e coordena os recursos necessários. | O chef que conhece a receita e coordena a cozinha. |
| Repository | Abstração de Dados: Isola o Eloquent. A Service não sabe se o dado vem do MySQL, Redis ou API; o Repository resolve a busca. | O estoquista que busca os ingredientes, não importa onde estejam. |
| Model (Eloquent) | Persistência: No DDD, ela é apenas o mapeador de dados (Data Mapper). Reflete a tabela no banco, sem lógica de negócio. | A prateleira do estoque. |
| DTO (Data Transfer Object) | Transporte: Garante que os dados passem entre camadas (ex: Command -> Service) de forma tipada e segura. | A bandeja padronizada onde o pedido viaja. |
5. Benefícios da Refatoração para DDD Link para o cabeçalho
Ao atender aos pedidos do seu líder técnico, você está aplicando:
-
Desacoplamento: O Command não depende mais diretamente do Eloquent.
-
Tipagem Estrita: O uso de :
?Modelearraynos parâmetros evita que dados inválidos corrompam o Domínio. -
Manutenibilidade: Se a lógica de duplicidade mudar, você altera apenas um lugar (a Service ou o Repository), e não todos os comandos que usam aquela tabela.
-
Testabilidade: Você pode testar a ClienteService sem precisar de um banco de dados real, usando um “Mock” do repositório.