Vai al contenuto principale
Quando vuoi accedere ai modelli recuperati tramite ID, o raggruppare l’elaborazione dei risultati in uno scope locale, puoi usare afterQuery(). Oltre ad aggiungere condizioni SQL, permette di registrare nel query builder una Closure eseguita sui risultati recuperati. Tuttavia, non è “un hook che riceve un risultato dello stesso tipo a ogni esecuzione SQL”. Il contenuto della Collection passata e il numero di chiamate cambiano a seconda del metodo di recupero.
Questa pagina riguarda Laravel 13. Partendo da After Query Callbacks nella documentazione ufficiale, verifica i limiti di utilizzo a partire dal builder di Eloquent e dai test ufficiali.

Raggruppare l’elaborazione dei risultati in uno scope locale

Se accedi ripetutamente ai modelli tramite ID, puoi preparare uno scope che uniforma le chiavi dei risultati. Definisci l’esempio seguente in app/Models/Post.php.
Il chiamante può concatenarlo come qualsiasi altro scope.
$posts resta una Eloquent Collection e i suoi elementi restano Post. get(42) restituisce il modello con chiave 42, oppure null se non esiste. Se limiti le colonne recuperate con select(), includi anche l’id usato come chiave. Se elabori i risultati in un solo punto, get()->keyBy('id') è più semplice. Scegli afterQuery() quando vuoi includere l’elaborazione successiva al recupero in uno scope o in una query riutilizzata. Per la definizione degli scope, consulta Scope di Eloquent.

Leggere l’ordine di esecuzione di get

Eloquent\Builder::get() applica gli scope globali, crea i modelli, esegue l’eager loading delle relazioni indicate e infine esegue le callback di afterQuery. Con un normale get(), le callback possono usare le relazioni caricate con eager loading. L’accesso a relazioni non indicate può generare SQL aggiuntivo, quindi progetta lo scope insieme all’eager loading. Il momento è diverso da quello dell’evento di modello retrieved. retrieved viene emesso alla creazione di ogni modello in Model::newFromBuilder(), prima dell’eager loading di get(). afterQuery(), che gestisce l’insieme dei modelli recuperati, e gli eventi di ciascun modello non hanno lo stesso ruolo.
Questo è il flusso di Eloquent\Builder::get(). cursor() non esegue l’eager loading delle relazioni ed esegue le callback un modello alla volta. Non aspettarti la stessa elaborazione in batch di get().

Il valore di ritorno viene passato alla callback successiva

afterQuery() aggiunge le Closure in ordine di registrazione. La parte che le esegue, applyAfterQueryCallbacks(), è implementata così.
Qui non si usa ??, ma ?:, che si basa sulla valutazione truthy di PHP. Puoi anche usare una callback void che modifica direttamente la Collection senza restituire nulla. In questo caso viene mantenuto lo stesso oggetto modificato. Se invece crei una nuova Collection, devi restituirla. Internamente argomento e valore di ritorno sono mixed, ma evita di rendere parte dell’API pubblica conversioni di tipo arbitrarie. Ad esempio, sostituire il risultato di get() con una stringa o un array rompe la chiamata ->first() che first() esegue subito dopo. Negli scope, limita l’elaborazione a modifiche che conservano i modelli, il tipo di Collection, il numero di elementi e l’ordine.

Differenze tra i metodi di recupero

La tabella seguente mostra come viene gestito un afterQuery() registrato sul builder di Eloquent. first() chiama internamente limit(1)->get()->first(). Usare first() non significa che devi cambiare il parametro della Closure in Post.
Lo scope keyedById() di questa pagina serve a recuperare modelli. Non puoi usarlo con Post::keyedById()->pluck('id'). pluck() passa una Illuminate\Support\Collection, che non corrisponde al tipo Illuminate\Database\Eloquent\Collection dell’esempio. Allentare la dichiarazione di tipo non basta a rendere sicuro il codice che legge attributi di modello dai valori.
Se ti servono solo valori, non usare lo scope e scrivi Post::query()->pluck('id'). Per estrarre valori dopo aver elaborato i modelli, puoi scrivere Post::keyedById()->get()->pluck('id'). Il pluck() del secondo caso è un’operazione sulla Collection recuperata, non sul query builder.

Non modificare il conteggio delle pagine né la posizione di recupero successiva

paginate() calcola il totale in SQL e poi recupera i modelli della pagina. Filtrare la Collection dopo il recupero non ricalcola il totale. Inoltre, simplePaginate() e cursorPaginate() usano il risultato elaborato per decidere se esiste una pagina successiva. chunkById() usa l’ID alla fine di ogni chunk come posizione di recupero successiva. Se modifichi il numero di elementi o l’ordine nella callback, il rilevamento delle pagine o la posizione di continuazione potrebbero non corrispondere più alla query originale. Specifica filtri, ordinamento e colonne nell’SQL e limita afterQuery() a elaborazioni che non cambiano il significato dei risultati recuperati. Se vuoi solo cambiare il formato di output di un’API, valuta anche la trasformazione con le API resource. La documentazione ufficiale contiene un esempio che aggiunge ai modelli un attributo con un estratto dei risultati di ricerca. Assegnare attributi di modello può anche modificare ciò che viene salvato. Evita progetti che chiamano direttamente save() su modelli con attributi di visualizzazione inesistenti nel database.

La registrazione resta nel query builder

Ogni chiamata a afterQuery() aggiunge una Closure, e l’esecuzione non rimuove la registrazione. Un nuovo recupero con lo stesso builder la riesegue, e chiamare due volte lo stesso scope registra due volte anche la callback. Anche clonando il builder le callback registrate vengono ereditate. Nel codice che esegue più recuperi o nei worker di lunga durata, crea una nuova query per ogni scopo e limita le callback a un’elaborazione dei risultati senza effetti collaterali. Se ci metti l’invio di email o la persistenza, è facile scambiare più esecuzioni dovute a chunk o cursori per una singola operazione di business. Se fornisci scope riutilizzabili in un pacchetto, indica anche i metodi di recupero supportati come API pubblica e testa il contratto su tipi e numero di chiamate.

Testare le differenze tra i percorsi di recupero

Nell’applicazione Laravel 13 che lo utilizza, verifica non solo l’elaborazione delle chiavi, ma anche le chiamate con first(), cursor() e pagine vuote. L’esempio seguente presuppone il modello Post precedente e una migration per la tabella posts con id, title, body, created_at e updated_at. RefreshDatabase isola i record tra i test. Crea il test in tests/Feature/AfterQueryTest.php.
Puoi eseguire solo questo test.

Checklist per la manutenzione

  • Indica i metodi di recupero supportati dallo scope e i tipi del parametro e del valore di ritorno della Closure.
  • Verifica separatamente il risultato vuoto di get() e un paginate() con totale 0.
  • Con chunk e cursori, conferma che il numero di chiamate e l’ordine di recupero non cambiano.
  • Progetta il codice in modo che una doppia registrazione della callback o il riutilizzo del builder non causino effetti collaterali di business.
  • Quando aggiorni Laravel, ricontrolla i percorsi di chiamata di get(), pluck(), cursor() e BuildsQueries.

Pagine correlate

Scope di Eloquent

Ripassa le basi per raggruppare i vincoli SQL in scope locali e globali.

Collection di Eloquent

Ripassa la differenza tra le Collection di modelli e le Support Collection.

Observer di Eloquent ed eventi di modello

Ripassa gli eventi per modello, diversi dall’elaborazione dei risultati recuperati.

API resource

Separa la trasformazione dei dati restituiti dall’API dal salvataggio dei modelli.

Fonti primarie consultate

Ultima modifica il 6 novembre 2026