Cada passo foi importante. Nenhum garante, sozinho, uma arquitetura sustentável.
Atualizado não significa sustentável
Aplicação A
Aplicação B
Stack
PHP e Laravel na última versão
Versões suportadas, adequadas ao produto
Testes
Quebram a cada refatoração
Protegem as regras críticas
Acoplamento
Tudo conhece tudo
Dependências explícitas
Uma mudança
Ninguém sabe o que vai quebrar
Tem lugar certo e risco previsível
Qual das duas é a moderna?
E como a aplicação A chegou nesse estado, se ela já está no Laravel?
Você pode migrar o framework sem migrar os problemas.
Vamo ver com os olhos 👀
Um controller que faz tudo
Um controller que faz tudo
Validar, autorizar, decidir, persistir, integrar, responder. Um método.
Single Responsibility que se exploda!
Tamanho importa? 🍆
Tamanho do documento importa? 🫦
O problema não é o tamanho do arquivo
Requisição HTTPRequest, user()
Regras de negóciodono, saldo, limite
PersistênciaEloquent, transaction
Integração externaHttp::post
A regra lê o usuário do $request: só dá para testá-la simulando HTTP.
A regra fala Eloquent: só dá para testá-la com banco.
O saldo é lido fora da transação e sem trava: duas requisições simultâneas passam pela mesma checagem.
Parceiro lento ou fora do ar vira erro na tela, com o dinheiro já movido.
Requisição HTTPRequest, user()
Regras de negóciodono, saldo, limite
PersistênciaEloquent, transaction
Integração externaHttp::post
Cada seta é um acoplamento
Problema de testabilidade
Bug de concorrência
Efeito colateral / depende do terceiro
Laravel Framework não é bala de prata!
O Service virou controller
Criar várias classes não garante separação de responsabilidades.
O dev chora e mãe não vê
Pedido do negócio
O limite diário passa a depender do tipo de conta e não vale entre contas do mesmo titular.
TransferService::execute()
regra misturada com query, HTTP e integração
RunScheduledTransfer
o job de agendamento copiou a mesma checagem
LimitsController
a tela "quanto ainda posso enviar" repete a query
TransferTest
para testar o limite: banco, usuário, login e Http::fake()
Uma regra de negócio. Quatro lugares para lembrar.
Vamos modernizar uma transferência
Uma transferência só pode ser concluída quando respeitar as regras de saldo, limites e autorização.
Em apenas 13 passos
Primeiro, identifique as responsabilidades
Entrada HTTP
validate()
$request->user()
back()->withErrors()
redirect()
Regras de negócio
a conta é do solicitante
valor ≤ saldo
enviado hoje + valor ≤ limite
Persistência
ler a conta
somar o dia
debitar e creditar
DB::transaction()
Serviços externos
avisar o parceiro
Ainda não tem pasta nova, nem interface, nem padrão. Só um mapa mental.
Antes de qualquer arquitetura
O que é protocolo?
O que é regra?
O que é guardar?
O que é falar com terceiros?
A regra não deveria depender do transporte
TransferRequest é um FormRequest.
O controller traduz HTTP para um comando e um erro de negócio para uma resposta HTTP. Só isso.
O mesmo caso de uso, sem HTTP
Lembra do job que copiava a regra?
Agora ele chama a mesma regra.
Se dá para chamar de um job, dá para chamar de um teste.
Melhoramos (e muito) a testabilidade
Dependências explícitas
ConnectionInterface
Contrato do próprio Laravel. A transação fica visível, não escondida.
Accountsinterface criada pela gente, sangue do nosso sangue
Fronteira com o banco: o contrato exige trava e leitura consistente do dia.
Dispatcher
Contrato do Laravel para agendar efeitos colaterais.
SourceAccountsem interface
Regra pura, uma implementação, nada para substituir.
O domínio não precisa conhecer o Laravel
Controller / JobHTTP, fila, CLI
TransferMoneycaso de uso
SourceAccountregra de negócio
AccountslockForTransfer(), move()
EloquentAccountsinfraestrutura
Não vamos banir o framework. Vamo botar ele no lugar dele.
O framework deve servir ao propósito do negócio, não o negócio ao propósito do framework.
A regra, em PHP puro
Nenhum use Illuminate\….
TransferRejected é uma DomainException com construtores nomeados.
A fronteira, de perto
Com a conta travada, toda transferência da mesma origem espera a anterior. A soma do dia fica consistente.
Bug de concorrência foi mitigado
Testar comportamento e não a implementação
PHPUnit puro: sem banco, sem HTTP, sem mock.
Testes e Negócios, mesma lingua 👅
GERENCIAR AS CONSEQUÊNCIAS
Só vai avisar o parceiro se a transferencia der bom
🙋♀️Paciente
Tem sintomas
→
🥵Sintomas
São consequências, não o problema em si
→
💊Tratamento
dos sintomas
→
❤️🩹Cura
Resolve o problema de verdade
O requisito define a estratégia
Se o requisito é…
…então
O aviso pode atrasar, mas nunca sair para uma transferência que não aconteceu
Job na fila com afterCommit()
O evento não pode se perder, mesmo se a fila cair
Gravar o evento na mesma transação (outbox) e publicar depois
O parceiro precisa aprovar antes de concluir
Status como "Pendente" e estratégia de timeout
Filas e eventos não são soluções universais. São respostas a garantias específicas.
A arquitetura digivolveu
TransferController::store()
HTTPvalidate, user()
Regrasdono, saldo, limite
Persistênciatransaction, increment
IntegraçãoHttp::post
A arquitetura evoluiu
TransferControllerHTTP vira comando
TransferMoneycaso de uso e transação
SourceAccountregras, PHP puro
RunScheduledTransfermesma regra, via fila
Accountscontrato, com trava
NotifyTransferPartnerdepois do commit
Regras testáveis sem banco nem HTTP
Menos dependências inesperadas
Um lugar claro para cada mudança
Infraestrutura substituível onde importa
Não confunda organização com qualidade
Um CRUD de categorias
app/Domain/Category/
Contracts/CategoryRepositoryInterface.php
Contracts/CategoryServiceInterface.php
DTOs/CreateCategoryDTO.php
DTOs/UpdateCategoryDTO.php
Factories/CategoryFactoryInterface.php
Mappers/CategoryMapper.php
Services/CategoryService.php
… tudo isso para salvar o diabo de um nome
A transferência
app/Transfers/
TransferMoney.php
TransferCommand.php
SourceAccount.php
TransferRejected.php
Accounts.php
EloquentAccounts.php
uma interface, onde há fronteira
Arquitetura boa não é a que tem mais camadas.
É a que resolve o problema sem criar complexidade desnecessária.
Arquitetura proporcional ao problema
O sistema não pode parar para você modernizar.
Usuárioscontinuam transferindo
Dadoscontinuam crescendo
Integraçõescontinuam chamando
Entregascontinuam sendo cobradas
Reescrever também tem custo
regras implícitas esquecidas
comportamentos incompatíveis
migração de dados
meses sem entregar melhorias
defeitos novos em funcionalidades antigas
Reescrever pode ser a decisão certa. Só não pode ser a decisão padrão.
Conheça o comportamento antes de mudar
Tipo desconhecido paga como conta corrente. Acima de R$ 10 mil, não paga. Alguém depende disso?
Teste de caracterização
O teste não diz que o comportamento está certo. Diz que ele não muda sem alguém perceber.
Modernização incremental
Escolha o próximo problema com critério
custo de manutenção
risco operacional
complexidade desnecessária
"esse código é feio"
Migrar tecnologia é trocar ferramentas. Modernizar software é diminuir o custo de digivolvê-lo.
Arquiteturaa mudança tem um lugar certo
Qualidadea mudança tem rede de proteção
Operaçãoa mudança chega e é observada
Um checklist que não pergunta o framework
A versão do PHP recebe suporte?
As dependências são gerenciadas e mantidas?
As regras críticas têm testes?
Dá para alterar um comportamento com risco controlado?
Existem logs, métricas e diagnóstico adequados?
A entrega detecta problemas antes da produção?
A arquitetura é proporcional à complexidade do produto?
O que medir
Tempo para entregar uma mudançado pedido até produção
Frequência e impacto de defeitosquanto escapa e quanto dói
Tempo de recuperaçãodo incidente até a normalidade
Facilidade de testar regras críticasquanto custa escrever o próximo teste
Nenhum indicador isolado prova que a arquitetura é boa. A tendência ao longo do tempo diz mais que o número.
O framework não é o vilão
O Laravel resolve
Roteamento e validação
Filas e jobs
ORM e transações
Autenticação
Container de dependências
Você decide
Onde mora a regra de negócio
Quem depende de quem
O que precisa de teste
Qual garantia cada efeito exige
Quanta estrutura o problema pede
Seu sistema está moderno porque usa Laravel
ou porque consegue continuar evoluindo?
Modernizar não é trocar tudo. É tornar a próxima mudança menos arriscada do que a anterior.
Valeu o boi. Valeu o vaqueiro
slides.feitoza.tec.br
PHP moderno não é trocar mysql_* por LaravelPHPeste 2026