Ir para o conteúdo principal

Devlog · Fase 0 · 5/7

Os cinco módulos Laravel de aubia.dev

aubia.dev agrupa o backend por responsabilidade. Actions, Queries e DTOs ajudam a acompanhar uma alteração, mas as pastas sozinhas não controlam as dependências.

Publicado em 10 de agosto de 2026Atualizado em 15 de setembro de 20266 min de leitura
  • laravel
  • architecture
  • octane

O controller de inscrição, a validação e os dados enviados ao cloud ficam todos em app/Modules/Waitlist/. Para acompanhar um envio, permaneço nessa pasta até a chamada HTTP de saída.

Trabalho com agentes de IA e reviso suas pull requests. Agrupar os arquivos por responsabilidade me dá um ponto de partida: uma alteração no formulário me leva a Waitlist, uma alteração no sitemap a Seo. Ainda preciso examinar os imports e o diff para entender o que a alteração afeta.

Agrupar o código por responsabilidade

O Laravel fornece uma estrutura inicial sem exigir módulos. Neste site de apresentação, uso cinco pastas em app/Modules/:

  • Blog lê os artigos Markdown, converte seu conteúdo em HTML e monta o feed RSS.
  • Cloud reúne o transporte HTTP para api.aubia.cloud em CloudApiClient.
  • Locale seleciona o idioma e monta as URLs localizadas.
  • Seo monta o sitemap a partir das páginas públicas e dos artigos publicados.
  • Waitlist encaminha os pedidos de inscrição e consulta o número de inscrições confirmadas.

Cada módulo tem as subpastas de que precisa. Cloud tem apenas uma classe na raiz. Blog também inclui comandos Artisan e adaptadores para o motor Markdown.

Mapa simplificado dos cinco módulos: Locale, Seo, Waitlist, Blog e Cloud. Um fluxo conecta Waitlist a Cloud e depois a api.aubia.cloud.

Este mapa mostra os módulos e seu gateway HTTP. Não mostra todas as dependências ou subpastas: Blog e Waitlist também têm uma pasta Queries/.

Actions e Queries

Uma Action executa uma operação por meio de um método de instância chamado execute(). SubmitWaitlistEmailAction orquestra a inscrição; BuildSitemapAction monta o XML do sitemap. Uma Query expõe uma leitura por meio de run(): ListBlogPostsQuery retorna os artigos publicados, FindBlogPostQuery busca um artigo e FetchWaitlistCountQuery lê o contador remoto.

A distinção não depende de onde os dados são armazenados. O blog lê arquivos locais, enquanto o contador chama uma API. As duas leituras são Queries.

Os artigos de produção são preparados durante o build. CompileBlogAction chama ParseBlogPostAction, que valida as fontes e orquestra a renderização Markdown, o realce Phiki e a estimativa do tempo de leitura. ListBlogPostsQuery lê depois o catálogo por meio de ReadCompiledBlogQuery e filtra as datas de publicação. O parsing com cache continua disponível localmente e nos testes; em produção, um catálogo ausente, inválido ou desatualizado retorna 503, sem recorrer ao parsing.

Esses nomes e pontos de entrada são convenções do repo. O Laravel pode construir e injetar classes concretas a partir de seus tipos; não é necessária uma biblioteca adicional de Actions.

Uma dependência visível no construtor

A raiz / redireciona para uma URL localizada com uma resposta 302. ResolveLocaleRedirectAction recebe DetectLocaleAction pelo construtor e lhe passa os candidatos em ordem de prioridade.

namespace App\Modules\Locale\Actions;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;

final readonly class ResolveLocaleRedirectAction
{
    public function __construct(private DetectLocaleAction $detectLocaleAction) {}

    public function execute(Request $request): string
    {
        return $this->detectLocaleAction->execute([
            $request->cookie(DetectLocaleAction::COOKIE_NAME),
            $this->detectLocaleAction->extractAcceptLanguage($request),
            Config::string('app.locale'),
        ])->value;
    }
}

O primeiro candidato reconhecido prevalece: o cookie de preferência, depois o idioma extraído do cabeçalho Accept-Language, depois a configuração. DetectLocaleAction retorna um SupportedLocale, o enum dos seis idiomas suportados. O acesso a ->value fornece a string usada na URL.

O middleware SetLocale chama a mesma Action com o segmento da URL acrescentado ao início da lista. A regra de seleção é compartilhada, mas a lista de candidatos depende de quem faz a chamada. A requisição HTTP é passada a execute(), sem ser armazenada no construtor.

Os dados depois da validação

O formulário de inscrição passa por uma FormRequest, a classe de validação do Laravel. Ela também verifica as proteções anti-bot locais e depois seleciona os campos necessários a WaitlistSubmissionData.

Esse DTO, ou objeto de transferência de dados, contém o endereço de e-mail, os três valores UTM de atribuição e a locale. Os campos de controle anti-bot não fazem parte de suas propriedades, por isso o objeto não os passa à Action de inscrição.

O método de saída enumera o payload destinado ao cloud:

public function toCloudPayload(): array
{
    return [
        'email' => $this->email,
        'utm_source' => $this->utmSource,
        'utm_medium' => $this->utmMedium,
        'utm_campaign' => $this->utmCampaign,
        'locale' => $this->locale->value,
    ];
}

A locale continua sendo um enum no código PHP e se torna uma string na saída. Acrescentar uma propriedade ao DTO não a inclui automaticamente no payload: é preciso alterar esse método.

O blog usa o mesmo princípio para destinos diferentes. BlogPostData fornece um resumo sem HTML para o índice, o artigo completo para sua página e um array de valores serializáveis para o catálogo compilado e o cache local. O DTO transporta esses resultados; uma Action cuida da renderização Markdown.

O gateway HTTP do módulo Cloud

As chamadas da aplicação a aubia.cloud passam por CloudApiClient. Duas classes o usam: a Action de envio e a Query do contador, ambas em Waitlist. O site de apresentação não tem banco de dados local para as inscrições.

A Action prepara o corpo JSON e sua assinatura. O cliente HTTP envia os bytes recebidos e cuida dos timeouts, das novas tentativas e dos logs de falha. Esses logs registram o tipo de erro sem armazenar o endereço de e-mail ou o corpo enviado.

Uma GET pode ser repetida após um erro de conexão ou uma resposta 5xx, mas não após uma resposta 4xx. A POST assinada só é repetida após um erro de conexão. Esse erro não prova que o serviço remoto não processou nada: a resposta pode não chegar mesmo que a requisição tenha sido recebida.

CloudApiClient é uma classe final sem propriedades de instância. Seus testes usam Http::fake() para simular respostas e erros de rede enquanto executam o cliente real da aplicação.

O que readonly não garante com Octane

Actions, Queries e DTOs são declarados final readonly. final impede a herança. readonly impede a reatribuição de propriedades após a inicialização, mas não torna imutável um objeto armazenado nelas.

ListBlogPostsQuery armazena as dependências recebidas no construtor, mas não as coleções de artigos. Os resultados das leituras ficam nas variáveis locais de seus métodos. O catálogo compilado em disco é separado do cache Redis usado pelo modo de parsing local.

Com o Octane, o Laravel permanece carregado entre requisições. A palavra-chave readonly não determina o tempo de vida de uma instância. O repo não registra essas classes como singletons; registrá-las como serviços compartilhados exigiria revisar seu estado e suas dependências. A documentação do Octane descreve os riscos de manter uma requisição HTTP em um serviço de uma requisição para a seguinte.

As dependências ainda precisam de revisão

O sitemap usa ListBlogPostsQuery para encontrar os artigos publicados. Uma alteração em Blog pode, portanto, mudar a saída de Seo, mesmo que a pull request não altere nenhum arquivo em Seo.

Os testes de arquitetura verificam os sufixos, as declarações final readonly e os métodos públicos execute() ou run(). Eles não proíbem imports entre módulos. Um agente pode introduzir uma dependência indevida em uma pasta com o nome correto.

Essa estrutura acrescenta classes e exige alternar entre arquivos para acompanhar uma operação. Em um site de apresentação desse tamanho, esse custo é real. Mantenho essa organização para encontrar validação, orquestração e dados de saída em lugares consistentes. Durante a revisão, ainda preciso acompanhar suas dependências e verificar o comportamento com testes.

Esta construção é contada aqui, artigo após artigo. O que vem a seguir depende do que você disser sobre ela.

Entrar na lista de espera