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

# Eloquent afterQuery en het contract van queryresultaten

> Leer hoe je met Eloquent afterQuery de verwerking van resultaten bundelt in lokale scopes. Controleer de uitvoeringsvolgorde, retourwaarden en de verschillen tussen first, pluck, cursor en paginering aan de hand van de implementatie van Laravel 13.

Als je opgehaalde modellen op ID wilt kunnen opzoeken, of de verwerking van resultaten wilt bundelen in een lokale scope, kun je `afterQuery()` gebruiken. Naast het toevoegen van SQL-voorwaarden kun je hiermee ook een Closure in de query builder registreren die op de opgehaalde resultaten wordt uitgevoerd.

Het is echter **geen "hook die bij elke SQL-uitvoering een resultaat van hetzelfde type ontvangt"**. Afhankelijk van de ophaalmethode verschillen de inhoud van de doorgegeven Collection en het aantal aanroepen.

> [!NOTE]
>
> Deze pagina is gericht op Laravel 13. Uitgaande van [After Query Callbacks in de officiële documentatie](https://github.com/laravel/docs/blob/13.x/eloquent.md#after-query-callbacks) controleert ze de grenzen van het gebruik aan de hand van de Eloquent builder en de officiële tests.

## Resultaatverwerking bundelen in een lokale scope

Als je herhaaldelijk modellen op ID opzoekt, kun je een scope maken die de sleutels van de resultaten gelijktrekt. Definieer het volgende voorbeeld 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')
        );
    }
}
```

De aanroeper kan hem net als elke andere scope chainen.

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

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

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

`$posts` blijft een Eloquent Collection en de elementen blijven `Post`. `get(42)` geeft het model met sleutel `42` terug, of `null` als het niet bestaat. Als je de opgehaalde kolommen beperkt met `select()`, neem dan ook de `id` op die als sleutel dient.

Als je de resultaten maar op één plek verwerkt, is `get()->keyBy('id')` eenvoudiger. Kies `afterQuery()` wanneer je **de verwerking na het ophalen wilt opnemen in een scope of een hergebruikte query**. Hoe je scopes zelf definieert, lees je in [Eloquent-scopes](https://kawax.biz/nl/advanced/eloquent-scopes.md).

## De uitvoeringsvolgorde van get lezen

`Eloquent\Builder::get()` past globale scopes toe, maakt de modellen aan, laadt de opgegeven relaties via eager loading en voert als laatste de `afterQuery`-callbacks uit.

```mermaid theme={null}
flowchart TD
    A["Closure registreren<br>met keyedById()"] --> B["get() aanroepen"]
    B --> C["Globale scopes toepassen"]
    C --> D["SQL uitvoeren en<br>modellen aanmaken"]
    D --> E["Opgegeven relaties<br>eager laden"]
    E --> F["Eloquent Collection aanmaken"]
    F --> G["afterQuery-Closures uitvoeren<br>in registratievolgorde"]
    G --> H["Verwerkt resultaat teruggeven"]
```

Bij een gewone `get()` kunnen callbacks de via eager loading geladen relaties gebruiken. Toegang tot relaties die je niet hebt opgegeven kan extra SQL veroorzaken, dus ontwerp de scope samen met [eager loading](https://kawax.biz/nl/eloquent-relationships.md).

Het moment verschilt van dat van het model-event `retrieved`. `retrieved` wordt afgevuurd wanneer elk model in `Model::newFromBuilder()` wordt aangemaakt, dus vóór de eager loading van `get()`. `afterQuery()`, dat de verzameling opgehaalde modellen verwerkt, en de events per model hebben niet dezelfde rol.

> [!WARNING]
>
> Dit is de flow van `Eloquent\Builder::get()`. `cursor()` laadt geen relaties via eager loading en voert de callbacks per model uit. Verwacht niet dezelfde batchverwerking als bij `get()`.

## Retourwaarden worden doorgegeven aan de volgende callback

`afterQuery()` voegt Closures toe in registratievolgorde. Het uitvoerende deel, `applyAfterQueryCallbacks()`, is als volgt geïmplementeerd.

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

    return $result;
}
```

Hier wordt niet `??` gebruikt, maar `?:`, dat steunt op de truthy-evaluatie van PHP.

| Retourwaarde van de callback | Effect op het resultaat |
| - | - |
| Een object zoals een Collection | Het teruggegeven object gaat naar de volgende callback en naar de aanroeper |
| `null`, `false`, `0`, lege string, lege array | Het eerdere resultaat blijft behouden |
| Een lege Collection | Omdat het een object is, wordt het resultaat vervangen door de lege Collection |

Je kunt ook een `void`-callback gebruiken die de Collection direct wijzigt en niets teruggeeft. In dat geval blijft hetzelfde gewijzigde object behouden. Maak je daarentegen een nieuwe Collection, dan moet je die teruggeven.

Intern zijn argument en retourwaarde `mixed`, maar maak willekeurige typeconversies geen onderdeel van je publieke API. Als je bijvoorbeeld het resultaat van `get()` vervangt door een string of array, breek je de `->first()`-aanroep die `first()` daarna doet. Beperk je in scopes tot verwerking die de modellen, het Collection-type, het aantal en de volgorde behoudt.

## Verschillen per ophaalmethode

De volgende tabel laat zien hoe een **in de Eloquent builder geregistreerde** `afterQuery()` wordt behandeld.

| Aangeroepen methode | Resultaat dat de callback ontvangt | Aandachtspunten |
| - | - | - |
| `get()` | Eloquent Collection | Wordt ook bij 0 rijen met een lege Collection uitgevoerd |
| `first()` / `firstOrFail()` | Eloquent Collection | Niet het model zelf, maar een Collection met 0 of 1 element. Het eerste wordt na de verwerking eruit gehaald |
| `paginate()` | Eloquent Collection met de modellen van de pagina | Niet de Paginator. Is het totaal 0, dan wordt `get()` niet aangeroepen en de callback niet uitgevoerd |
| `simplePaginate()` / `cursorPaginate()` | Eloquent Collection | Kan één extra element bevatten om de volgende pagina te controleren, dus er worden maximaal `perPage + 1` elementen verwerkt |
| `cursor()` | Eloquent Collection met één model | Wordt per rij uitgevoerd bij het consumeren van de LazyCollection en geeft het eerste element van het verwerkte resultaat terug |
| `lazy()` / `chunk()` / `chunkById()` | Eloquent Collection per chunk | Wordt bij elke interne `get()` uitgevoerd. Kan ook worden uitgevoerd op het lege resultaat dat het einde detecteert |
| `pluck()` | Support Collection met waarden | Geen modellen. Wordt uitgevoerd na casts en mutators van de doelkolom |
| `count()` / `exists()` | Niet doorgegeven | Slaat de Eloquent-callbacks over en delegeert naar de onderliggende query builder |
| `toBase()->get()` | Niet doorgegeven | Registraties aan Eloquent-zijde gaan niet over naar de basisbuilder. De basisbuilder heeft een eigen `afterQuery()` |

`first()` roept intern `limit(1)->get()->first()` aan. Het gebruik van `first()` betekent niet dat je de parameter van de Closure naar `Post` moet wijzigen.

> [!WARNING]
>
> De scope `keyedById()` op deze pagina is bedoeld om modellen op te halen. Je kunt hem niet gebruiken met `Post::keyedById()->pluck('id')`. `pluck()` geeft een `Illuminate\Support\Collection` door, die niet overeenkomt met het type `Illuminate\Database\Eloquent\Collection` uit het voorbeeld. Alleen de typedeclaratie versoepelen maakt code die modelattributen uit waarden leest niet veilig.

Heb je alleen waarden nodig, gebruik dan geen scope en schrijf `Post::query()->pluck('id')`. Om waarden op te halen na verwerking van de modellen, kun je `Post::keyedById()->get()->pluck('id')` schrijven. De `pluck()` in het tweede geval is een bewerking op de opgehaalde Collection, niet op de query builder.

### Het aantal per pagina en de volgende ophaalpositie niet wijzigen

`paginate()` telt het totaal in SQL en haalt daarna de modellen van de pagina op. Als je de Collection na het ophalen filtert, wordt het totaal niet opnieuw berekend.

Daarnaast gebruiken `simplePaginate()` en `cursorPaginate()` het verwerkte resultaat om te bepalen of er een volgende pagina is. `chunkById()` gebruikt de ID aan het einde van een chunk als volgende ophaalpositie. Als je in de callback het aantal of de volgorde wijzigt, kunnen paginadetectie en vervolgpositie niet meer overeenkomen met de oorspronkelijke query.

Geef filtering, sortering en kolommen op in SQL, en beperk `afterQuery()` tot verwerking die de betekenis van de opgehaalde resultaten niet verandert. Wil je alleen het uitvoerformaat van een API wijzigen, overweeg dan ook een transformatie met [API-resources](https://kawax.biz/nl/eloquent-resources.md).

De officiële documentatie bevat een voorbeeld dat een attribuut met een fragment uit zoekresultaten aan modellen toevoegt. Toewijzen aan modelattributen kan ook wijzigen wat er wordt opgeslagen. Vermijd ontwerpen die `save()` rechtstreeks aanroepen op modellen met weergave-attributen die niet in de database bestaan.

## Registraties blijven in de query builder

Elke aanroep van `afterQuery()` voegt een Closure toe, en uitvoeren verwijdert de registratie niet. Opnieuw ophalen met dezelfde builder voert hem opnieuw uit, en als je dezelfde scope twee keer aanroept, wordt ook de callback twee keer geregistreerd. Ook bij het clonen van de builder worden de geregistreerde callbacks overgenomen.

In code die meerdere keren ophaalt of in langlopende workers maak je voor elk doel een nieuwe query, en beperk je callbacks tot resultaatverwerking zonder bijwerkingen. Als je daar e-mailverzending of persistentie plaatst, verwar je meervoudige uitvoeringen door chunks of cursors snel met één zakelijke handeling.

Bied je herbruikbare scopes aan in een package, leg dan ook de ondersteunde ophaalmethoden vast als publieke API en test het contract voor types en aantal aanroepen.

## De verschillen tussen ophaalpaden testen

Controleer in de Laravel 13-applicatie die de scope gebruikt niet alleen de sleutelverwerking, maar ook de aanroepen bij `first()`, `cursor()` en lege pagina's.

Het volgende voorbeeld gaat uit van het eerdere `Post`-model en een migratie voor een `posts`-tabel met `id`, `title`, `body`, `created_at` en `updated_at`. [RefreshDatabase](https://kawax.biz/nl/database-testing.md) isoleert de records tussen tests.

Maak de test aan 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);
    }
}
```

Je kunt alleen deze test uitvoeren.

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

## Checklist voor onderhoud

* Leg de ophaalmethoden vast die de scope ondersteunt, en de parameter- en retourtypes van de Closure.
* Controleer een leeg resultaat van `get()` en een `paginate()` met totaal 0 afzonderlijk.
* Bevestig bij chunks en cursors dat het aantal aanroepen en de ophaalvolgorde niet veranderen.
* Ontwerp de code zo dat dubbel geregistreerde callbacks of hergebruikte builders geen zakelijke bijwerkingen veroorzaken.
* Controleer bij Laravel-updates opnieuw de aanroeppaden van `get()`, `pluck()`, `cursor()` en `BuildsQueries`.

## Gerelateerde pagina's

> [card] [Eloquent-scopes](https://kawax.biz/nl/advanced/eloquent-scopes.md)
>
> De basis om SQL-beperkingen te bundelen in lokale en globale scopes.

> [card] [Eloquent-collecties](https://kawax.biz/nl/eloquent-collections.md)
>
> Het verschil tussen Collections met modellen en Support Collections.

> [card] [Eloquent-observers en model-events](https://kawax.biz/nl/advanced/eloquent-observers.md)
>
> Events per model, die verschillen van het verwerken van opgehaalde resultaten.

> [card] [API-resources](https://kawax.biz/nl/eloquent-resources.md)
>
> Scheid de transformatie van door de API teruggegeven weergavegegevens van het opslaan van modellen.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: 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)
* [Officiële Laravel-integratietest: AfterQueryTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Integration/Database/AfterQueryTest.php)


## Related topics

- [Scopes in Eloquent](https://kawax.biz/nl/advanced/eloquent-scopes.md)
- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Eloquent accessors, mutators en casts](https://kawax.biz/nl/eloquent-mutators.md)
- [Contracts](https://kawax.biz/nl/contracts.md)
- [Eloquent-factories](https://kawax.biz/nl/eloquent-factories.md)


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