Vai al contenuto principale

Diario di bordo · Fase 0 · 5/7

I cinque moduli Laravel di aubia.dev

aubia.dev raggruppa il backend per responsabilità. Action, Query e DTO aiutano a seguire una modifica, ma le cartelle da sole non controllano le dipendenze.

Pubblicato il 10 agosto 2026Aggiornato il 15 settembre 20266 min di lettura
  • laravel
  • architecture
  • octane

Il controller di iscrizione, la validazione e i dati inviati al cloud si trovano tutti sotto app/Modules/Waitlist/. Per seguire un invio, resto in quella cartella fino alla chiamata HTTP in uscita.

Lavoro con agenti IA e rivedo le loro pull request. Raggruppare i file per responsabilità mi dà un punto di partenza: una modifica al modulo di iscrizione mi indirizza a Waitlist, una modifica alla sitemap a Seo. Per capire su cosa incide la modifica, devo comunque esaminare gli import e il diff.

Raggruppare il codice per responsabilità

Laravel fornisce una struttura iniziale senza imporre i moduli. Per questo sito vetrina uso cinque cartelle sotto app/Modules/:

  • Blog legge gli articoli Markdown, li converte in HTML e costruisce il feed RSS.
  • Cloud riunisce il trasporto HTTP verso api.aubia.cloud in CloudApiClient.
  • Locale seleziona la lingua e costruisce gli URL localizzati.
  • Seo costruisce la sitemap a partire dalle pagine pubbliche e dagli articoli pubblicati.
  • Waitlist inoltra le richieste di iscrizione e recupera il numero di iscrizioni confermate.

Ogni modulo ha le sottocartelle di cui ha bisogno. Cloud contiene una sola classe alla radice. Blog comprende anche comandi Artisan e gli adattatori per il motore Markdown.

Mappa semplificata dei cinque moduli: Locale, Seo, Waitlist, Blog e Cloud. Un flusso collega Waitlist a Cloud, poi ad api.aubia.cloud.

Questa mappa mostra i moduli e il loro gateway HTTP. Non rappresenta tutte le dipendenze o le sottocartelle: anche Blog e Waitlist hanno una cartella Queries/.

Action e Query

Una Action esegue un'operazione attraverso un metodo di istanza chiamato execute(). SubmitWaitlistEmailAction orchestra l'iscrizione; BuildSitemapAction costruisce l'XML della sitemap. Una Query espone una lettura tramite run(): ListBlogPostsQuery restituisce gli articoli pubblicati, FindBlogPostQuery cerca un articolo e FetchWaitlistCountQuery legge il contatore remoto.

La distinzione non dipende da dove sono memorizzati i dati. Il blog legge file locali, mentre il contatore chiama un'API. Entrambe le letture sono Query.

Gli articoli di produzione vengono preparati durante il build. CompileBlogAction chiama ParseBlogPostAction, che valida le sorgenti e orchestra il rendering Markdown, l'evidenziazione Phiki e la stima del tempo di lettura. ListBlogPostsQuery legge poi il catalogo tramite ReadCompiledBlogQuery e filtra le date di pubblicazione. Il parsing con cache resta disponibile in locale e nei test; in produzione, un catalogo assente, non valido o obsoleto restituisce 503 senza ricorrere al parsing.

Questi nomi e punti di ingresso sono convenzioni del repo. Laravel può costruire e iniettare classi concrete in base ai loro tipi; non serve una libreria aggiuntiva per le Action.

Una dipendenza visibile nel costruttore

La radice / reindirizza a un URL localizzato con una risposta 302. ResolveLocaleRedirectAction riceve DetectLocaleAction tramite il costruttore e le passa i candidati in ordine di priorità.

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;
    }
}

Prevale il primo candidato riconosciuto: il cookie di preferenza, poi la lingua estratta dall'header Accept-Language, poi la configurazione. DetectLocaleAction restituisce un SupportedLocale, l'enum delle sei lingue supportate. Accedendo a ->value si ottiene la stringa usata nell'URL.

Il middleware SetLocale chiama la stessa Action aggiungendo il segmento dell'URL in testa alla lista. La regola di selezione è condivisa, ma la lista dei candidati dipende dal chiamante. La richiesta HTTP viene passata a execute(), senza essere memorizzata nel costruttore.

I dati dopo la validazione

Il modulo di iscrizione passa attraverso una FormRequest, la classe di validazione di Laravel. Questa verifica anche le protezioni anti-bot locali, poi seleziona i campi necessari a WaitlistSubmissionData.

Questo DTO, o oggetto di trasferimento dati, contiene l'indirizzo email, i tre valori UTM di attribuzione e la locale. I campi di controllo anti-bot non fanno parte delle sue proprietà, quindi l'oggetto non li trasmette alla Action di iscrizione.

Il metodo di uscita elenca il payload destinato al 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,
    ];
}

La locale resta un enum nel codice PHP e diventa una stringa in uscita. Aggiungere una proprietà al DTO non la aggiunge automaticamente al payload: occorre modificare questo metodo.

Il blog applica lo stesso principio a destinazioni diverse. BlogPostData fornisce un riepilogo senza HTML per l'indice, l'articolo completo per la sua pagina e un array di valori serializzabili per il catalogo compilato e la cache locale. Il DTO trasporta questi risultati; una Action si occupa del rendering Markdown.

Il gateway HTTP del modulo Cloud

Le chiamate applicative ad aubia.cloud passano da CloudApiClient. Lo usano due classi: la Action di invio e la Query del contatore, entrambe in Waitlist. Il sito vetrina non ha un database locale per le iscrizioni.

La Action prepara il corpo JSON e la sua firma. Il client HTTP invia i byte ricevuti e gestisce timeout, tentativi successivi e log degli errori. Questi log registrano il tipo di errore senza memorizzare l'indirizzo email o il corpo inviato.

Una GET può essere ritentata dopo un errore di connessione o una risposta 5xx, ma non dopo una risposta 4xx. La POST firmata viene ritentata solo dopo un errore di connessione. Questo errore non dimostra che il servizio remoto non abbia elaborato nulla: la risposta può mancare anche se la richiesta è stata ricevuta.

CloudApiClient è una classe final senza proprietà di istanza. I suoi test usano Http::fake() per simulare risposte ed errori di rete, eseguendo il vero client applicativo.

Cosa readonly non garantisce con Octane

Action, Query e DTO sono dichiarati final readonly. final impedisce l'ereditarietà. readonly impedisce di riassegnare le proprietà dopo l'inizializzazione, ma non rende immutabile un oggetto memorizzato al loro interno.

ListBlogPostsQuery conserva le dipendenze ricevute dal costruttore, ma non le collezioni di articoli. I risultati delle letture restano nelle variabili locali dei suoi metodi. Il catalogo compilato su disco è distinto dalla cache Redis usata dalla modalità di parsing locale.

Con Octane, Laravel resta caricato tra una richiesta e l'altra. La parola chiave readonly non determina la durata di vita di un'istanza. Il repo non registra queste classi come singleton; registrarle come servizi condivisi richiederebbe di riesaminarne lo stato e le dipendenze. La documentazione di Octane descrive i rischi legati alla conservazione di una richiesta HTTP in un servizio da una richiesta alla successiva.

Le dipendenze vanno comunque esaminate

La sitemap usa ListBlogPostsQuery per trovare gli articoli pubblicati. Una modifica a Blog può quindi cambiare l'output di Seo, anche se la pull request non modifica alcun file in Seo.

I test di architettura verificano i suffissi, le dichiarazioni final readonly e i metodi pubblici execute() o run(). Non vietano gli import tra moduli. Un agente può introdurre una dipendenza fuori posto in una cartella dal nome corretto.

Questa struttura aggiunge classi e richiede di passare da un file all'altro per seguire un'operazione. Per un sito vetrina di queste dimensioni, è un costo reale. Mantengo questa organizzazione per ritrovare validazione, orchestrazione e dati in uscita in posizioni coerenti. Durante la revisione, devo comunque seguire le loro dipendenze e verificare il comportamento con i test.

Questo cantiere si racconta qui, articolo dopo articolo. Ciò che viene dopo dipende da ciò che lei ne dirà.

Si iscriva alla lista d'attesa