PHP além do CRUD: como escrever código que não vira problema em produção

Foto de Ben Griffiths na Unsplash

A maioria dos desenvolvedores PHP sabe fazer CRUD.
Isso não te torna pleno. Muito menos sênior.

O que diferencia um dev mais experiente não é o que ele sabe fazer, mas o que ele evita fazer.

Vou te ensinar aqui um padrão prático, usado em projetos reais, que resolve três problemas clássicos:

  • Controllers inchados
  • Regras de negócio espalhadas
  • Código impossível de testar ou evoluir

public function store(Request $request)
{
if (!$request->email) {
return response()->json([‘error’ => ‘Email obrigatório’], 422);
}

$user = User::create([
    'name' => $request->name,
    'email' => $request->email,
]);

Mail::to($user->email)->send(new WelcomeMail($user));

Log::info('Usuário criado', ['id' => $user->id]);

return response()->json($user);

}

Funciona? Funciona.

É bom? Não.

Por quê?

  • Controller decide regra de negócio
  • Controller cria usuário
  • Controller dispara e-mail
  • Controller registra log

Isso acopla tudo.

Pensamento pleno/sênior: separar responsabilidade

Controller não decide regra.
Controller orquestra.

Vamos refatorar com um Service + DTO, padrão simples e poderoso.


Criando um DTO (Data Transfer Object)

Isso evita Request sendo usado como regra de negócio.

final class CreateUserDTO
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
    ) {}
}

Simples, explícito e tipado.


Criando o Service (onde a regra mora)

final class CreateUserService
{
    public function execute(CreateUserDTO $dto): User
    {
        if (!filter_var($dto->email, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email inválido');
        }

        $user = User::create([
            'name' => $dto->name,
            'email' => $dto->email,
        ]);

        Mail::to($user->email)->send(new WelcomeMail($user));

        Log::info('Usuário criado', ['id' => $user->id]);

        return $user;
    }
}

Agora sim:

  • Regra centralizada
  • Código reutilizável
  • Fácil de testar
  • Fácil de evoluir

Controller limpo (como deveria ser)

public function store(Request $request, CreateUserService $service)
{
    $dto = new CreateUserDTO(
        name: $request->name,
        email: $request->email
    );

    $user = $service->execute($dto);

    return response()->json($user);
}

O controller:

  • Recebe request
  • Constrói DTO
  • Chama serviço
  • Retorna resposta

Nada além disso.


O ganho real (que júnior não enxerga)

Testabilidade

Agora você testa a regra sem framework:

public function test_user_creation()
{
    $service = new CreateUserService();

    $dto = new CreateUserDTO(
        name: 'Leo',
        email: '[email protected]'
    );

    $user = $service->execute($dto);

    $this->assertEquals('Leo', $user->name);
}

Sem Request.
Sem Controller.
Sem gambiarra.


Evolução sem dor

Amanhã você precisa:

  • Enviar evento para fila
  • Criar usuário em sistema externo
  • Validar regra nova

Você altera um lugar só.


Mentalidade sênior (isso vale ouro)

” Código que funciona não é código bom.mCódigo bom é o que aguenta mudança.”

Pleno/sênior pensa assim:

  • Onde essa regra deve morar?
  • O que vai mudar daqui 6 meses?
  • Quem vai dar manutenção nisso?

Erros clássicos que isso evita

  • Fat models
  • Controllers gigantes
  • Services que viram controllers
  • Regra duplicada
  • Código impossível de refatorar

Esse tipo de abordagem não aparece em tutorial de YouTube, mas é exatamente o que mantém sistemas vivos em produção.