> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# afterQuery di Eloquent e il contratto dei risultati

> Scopri come raggruppare l'elaborazione dei risultati negli scope locali con afterQuery di Eloquent. Verifica ordine di esecuzione, valori di ritorno e differenze tra first, pluck, cursor e paginazione a partire dall'implementazione di Laravel 13.

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.

> [!NOTE]
>
> Questa pagina riguarda Laravel 13. Partendo da [After Query Callbacks nella documentazione ufficiale](https://github.com/laravel/docs/blob/13.x/eloquent.md#after-query-callbacks), 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`.

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'body'];

    #[Scope]
    protected function keyedById(Builder $query): void
    {
        $query->afterQuery(
            fn (Collection $posts): Collection => $posts->keyBy('id')
        );
    }
}
```

Il chiamante può concatenarlo come qualsiasi altro scope.

```php theme={null}
use App\Models\Post;

$posts = Post::keyedById()->orderBy('id')->get();

$post = $posts->get(42);
```

`$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](https://kawax.biz/it/advanced/eloquent-scopes.md).

## 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`.

```mermaid theme={null}
flowchart TD
    A["Registrare la Closure<br>con keyedById()"] --> B["Chiamare get()"]
    B --> C["Applicare gli scope globali"]
    C --> D["Eseguire l'SQL e<br>creare i modelli"]
    D --> E["Eager loading delle<br>relazioni indicate"]
    E --> F["Creare la Eloquent Collection"]
    F --> G["Eseguire le Closure di afterQuery<br>in ordine di registrazione"]
    G --> H["Restituire il risultato elaborato"]
```

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](https://kawax.biz/it/eloquent-relationships.md).

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.

> [!WARNING]
>
> 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ì.

```php theme={null}
public function applyAfterQueryCallbacks($result)
{
    foreach ($this->afterQueryCallbacks as $afterQueryCallback) {
        $result = $afterQueryCallback($result) ?: $result;
    }

    return $result;
}
```

Qui non si usa `??`, ma `?:`, che si basa sulla valutazione truthy di PHP.

| Valore di ritorno della callback | Effetto sul risultato |
| - | - |
| Un oggetto come una Collection | L'oggetto restituito passa alla callback successiva e al chiamante |
| `null`, `false`, `0`, stringa vuota, array vuoto | Viene mantenuto il risultato precedente |
| Una Collection vuota | Essendo un oggetto, il risultato viene sostituito dalla Collection vuota |

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**.

| Metodo chiamato | Risultato passato alla callback | Note |
| - | - | - |
| `get()` | Eloquent Collection | Viene eseguita con una Collection vuota anche con 0 righe |
| `first()` / `firstOrFail()` | Eloquent Collection | Non il modello stesso, ma una Collection di 0 o 1 elemento. Il primo viene estratto dopo l'elaborazione |
| `paginate()` | Eloquent Collection con i modelli della pagina | Non il Paginator. Se il totale è 0, `get()` non viene chiamato e la callback non viene eseguita |
| `simplePaginate()` / `cursorPaginate()` | Eloquent Collection | Può includere un elemento in più per verificare la pagina successiva, quindi elabora fino a `perPage + 1` elementi |
| `cursor()` | Eloquent Collection con un modello | Viene eseguita per ogni riga mentre la LazyCollection viene consumata e restituisce il primo elemento del risultato elaborato |
| `lazy()` / `chunk()` / `chunkById()` | Eloquent Collection per chunk | Viene eseguita a ogni `get()` interno. Può essere eseguita anche sul risultato vuoto che rileva la fine |
| `pluck()` | Support Collection di valori | Non modelli. Viene eseguita dopo cast e mutator della colonna |
| `count()` / `exists()` | Non passato | Salta le callback di Eloquent e delega al query builder di base |
| `toBase()->get()` | Non passato | Le registrazioni lato Eloquent non passano al builder di base. Il builder di base ha un proprio `afterQuery()` |

`first()` chiama internamente `limit(1)->get()->first()`. Usare `first()` non significa che devi cambiare il parametro della Closure in `Post`.

> [!WARNING]
>
> 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](https://kawax.biz/it/eloquent-resources.md).

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](https://kawax.biz/it/database-testing.md) isola i record tra i test.

Crea il test in `tests/Feature/AfterQueryTest.php`.

```php theme={null}
<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AfterQueryTest extends TestCase
{
    use RefreshDatabase;

    public function test_get_preserves_models_and_keys_them_by_id(): void
    {
        $first = Post::create(['title' => 'First', 'body' => 'First body']);
        $second = Post::create(['title' => 'Second', 'body' => 'Second body']);

        $posts = Post::keyedById()->orderBy('id')->get();

        $this->assertInstanceOf(Collection::class, $posts);
        $this->assertSame([$first->getKey(), $second->getKey()], $posts->keys()->all());
        $this->assertTrue($first->is($posts->get($first->getKey())));
    }

    public function test_first_passes_a_collection_to_the_callback(): void
    {
        $expected = Post::create(['title' => 'First', 'body' => 'First body']);
        $sizes = [];

        $post = Post::query()
            ->afterQuery(function (Collection $posts) use (&$sizes): void {
                $sizes[] = $posts->count();
            })
            ->first();

        $this->assertSame([1], $sizes);
        $this->assertTrue($expected->is($post));
    }

    public function test_cursor_runs_callbacks_when_each_model_is_consumed(): void
    {
        $first = Post::create(['title' => 'First', 'body' => 'First body']);
        $second = Post::create(['title' => 'Second', 'body' => 'Second body']);
        $sizes = [];

        $cursor = Post::keyedById()->orderBy('id')
            ->afterQuery(function (Collection $posts) use (&$sizes): void {
                $sizes[] = $posts->count();
            })
            ->cursor();

        $this->assertSame([], $sizes);
        $this->assertSame([$first->getKey(), $second->getKey()], $cursor->pluck('id')->all());
        $this->assertSame([1, 1], $sizes);
    }

    public function test_empty_get_runs_a_callback_but_zero_total_pagination_does_not(): void
    {
        $sizes = [];
        $query = Post::query()->whereKey(-1)
            ->afterQuery(function (Collection $posts) use (&$sizes): void {
                $sizes[] = $posts->count();
            });

        $this->assertCount(0, $query->get());
        $this->assertSame([0], $sizes);

        $page = $query->paginate(perPage: 15, page: 1);

        $this->assertSame(0, $page->total());
        $this->assertSame([0], $sizes);
    }
}
```

Puoi eseguire solo questo test.

```bash theme={null}
php artisan test --filter=AfterQueryTest
```

## 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

> [card] [Scope di Eloquent](https://kawax.biz/it/advanced/eloquent-scopes.md)
>
> Ripassa le basi per raggruppare i vincoli SQL in scope locali e globali.

> [card] [Collection di Eloquent](https://kawax.biz/it/eloquent-collections.md)
>
> Ripassa la differenza tra le Collection di modelli e le Support Collection.

> [card] [Observer di Eloquent ed eventi di modello](https://kawax.biz/it/advanced/eloquent-observers.md)
>
> Ripassa gli eventi per modello, diversi dall'elaborazione dei risultati recuperati.

> [card] [API resource](https://kawax.biz/it/eloquent-resources.md)
>
> Separa la trasformazione dei dati restituiti dall'API dal salvataggio dei modelli.

## Fonti primarie consultate

* [Documentazione ufficiale di Laravel: After Query Callbacks](https://github.com/laravel/docs/blob/f7a20c401a30827991659174d48d958f92517a9e/eloquent.md#after-query-callbacks)
* [Laravel Framework v13.40.0: Eloquent Builder](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Builder.php)
* [Laravel Framework v13.40.0: BuildsQueries](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Concerns/BuildsQueries.php)
* [Laravel Framework v13.40.0: Model](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Database/Eloquent/Model.php)
* [Test di integrazione ufficiale di Laravel: AfterQueryTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Integration/Database/AfterQueryTest.php)


## Related topics

- [Scope di Eloquent](https://kawax.biz/it/advanced/eloquent-scopes.md)
- [Argomenti avanzati](https://kawax.biz/it/advanced/index.md)
- [Laravel Scout](https://kawax.biz/it/scout.md)
- [Hook di risoluzione dei servizi nei pacchetti](https://kawax.biz/it/advanced/package-service-resolution.md)
- [Introduzione alle relazioni Eloquent](https://kawax.biz/it/eloquent-relationships.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.