> ## 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 und der Vertrag der Abfrageergebnisse

> Erfahren Sie, wie Sie mit Eloquent afterQuery die Nachbearbeitung von Ergebnissen in lokalen Scopes bündeln. Prüfen Sie Ausführungsreihenfolge, Rückgabewerte und die Unterschiede zwischen first, pluck, cursor und Paginierung anhand der Implementierung von Laravel 13.

Wenn Sie abgerufene Modelle nach ID adressierbar machen oder die Nachbearbeitung von Ergebnissen in einem lokalen Scope bündeln möchten, können Sie `afterQuery()` verwenden. Neben zusätzlichen SQL-Bedingungen können Sie damit auch eine Closure im Query Builder registrieren, die auf die abgerufenen Ergebnisse angewendet wird.

Es handelt sich jedoch **nicht um „einen Hook, der bei jeder SQL-Ausführung ein Ergebnis desselben Typs erhält“**. Je nach Abrufmethode ändern sich der Inhalt der übergebenen Collection und die Anzahl der Aufrufe.

> [!NOTE]
>
> Diese Seite bezieht sich auf Laravel 13. Ausgehend von [After Query Callbacks in der offiziellen Dokumentation](https://github.com/laravel/docs/blob/13.x/eloquent.md#after-query-callbacks) prüft sie die Grenzen der Verwendung anhand des Eloquent Builders und der offiziellen Tests.

## Ergebnisverarbeitung in einem lokalen Scope bündeln

Wenn Sie Modelle wiederholt über ihre ID ansprechen, können Sie einen Scope vorbereiten, der die Schlüssel der Ergebnisse vereinheitlicht. Das folgende Beispiel definieren Sie 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')
        );
    }
}
```

Aufrufer können ihn wie jeden anderen Scope verketten.

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

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

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

`$posts` bleibt eine Eloquent Collection, und die Elemente bleiben `Post`-Modelle. `get(42)` gibt das Modell mit dem Schlüssel `42` zurück oder `null`, wenn keines existiert. Wenn Sie die abgerufenen Spalten mit `select()` einschränken, nehmen Sie auch die als Schlüssel verwendete `id` auf.

Wenn Sie die Ergebnisse nur an einer Stelle verarbeiten, ist `get()->keyBy('id')` einfacher. Wählen Sie `afterQuery()`, wenn Sie **die Verarbeitung nach dem Abruf in einen Scope oder eine wiederverwendete Abfrage aufnehmen möchten**. Wie Sie Scopes selbst definieren, lesen Sie unter [Eloquent-Scopes](https://kawax.biz/de/advanced/eloquent-scopes.md).

## Die Ausführungsreihenfolge von get verstehen

`Eloquent\Builder::get()` wendet globale Scopes an, erzeugt die Modelle, lädt die angegebenen Beziehungen per Eager Loading und führt zuletzt die `afterQuery`-Callbacks aus.

```mermaid theme={null}
flowchart TD
    A["Closure mit<br>keyedById() registrieren"] --> B["get() aufrufen"]
    B --> C["Globale Scopes anwenden"]
    C --> D["SQL ausführen und<br>Modelle erzeugen"]
    D --> E["Angegebene Beziehungen<br>eager laden"]
    E --> F["Eloquent Collection erstellen"]
    F --> G["afterQuery-Closures in<br>Registrierungsreihenfolge ausführen"]
    G --> H["Verarbeitetes Ergebnis zurückgeben"]
```

Bei einem normalen `get()` können Callbacks die per Eager Loading geladenen Beziehungen verwenden. Der Zugriff auf nicht angegebene Beziehungen kann zusätzliches SQL auslösen. Planen Sie den Scope daher zusammen mit [Eager Loading](https://kawax.biz/de/eloquent-relationships.md).

Der Zeitpunkt unterscheidet sich vom Modell-Event `retrieved`. `retrieved` wird ausgelöst, wenn jedes Modell in `Model::newFromBuilder()` erzeugt wird, also vor dem Eager Loading in `get()`. `afterQuery()`, das die Menge der abgerufenen Modelle verarbeitet, und die Events einzelner Modelle erfüllen nicht dieselbe Aufgabe.

> [!WARNING]
>
> Dies ist der Ablauf von `Eloquent\Builder::get()`. `cursor()` lädt keine Beziehungen per Eager Loading und führt die Callbacks für jeweils ein Modell aus. Erwarten Sie keine Stapelverarbeitung wie bei `get()`.

## Rückgabewerte werden an den nächsten Callback übergeben

`afterQuery()` fügt Closures in der Reihenfolge ihrer Registrierung hinzu. Der ausführende Teil, `applyAfterQueryCallbacks()`, ist wie folgt implementiert.

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

    return $result;
}
```

Verwendet wird hier nicht `??`, sondern `?:`, das auf der Truthy-Prüfung von PHP beruht.

| Rückgabewert des Callbacks | Auswirkung auf das Ergebnis |
| - | - |
| Ein Objekt wie eine Collection | Das zurückgegebene Objekt wird an den nächsten Callback und den Aufrufer übergeben |
| `null`, `false`, `0`, leerer String, leeres Array | Das bisherige Ergebnis bleibt erhalten |
| Eine leere Collection | Da es ein Objekt ist, wird das Ergebnis durch die leere Collection ersetzt |

Sie können auch einen `void`-Callback verwenden, der die Collection direkt verändert und nichts zurückgibt. In diesem Fall bleibt dasselbe veränderte Objekt erhalten. Erzeugen Sie dagegen eine neue Collection, müssen Sie diese zurückgeben.

Intern sind Argument und Rückgabewert `mixed`, doch machen Sie beliebige Typumwandlungen nicht zur öffentlichen API. Ersetzen Sie etwa das Ergebnis von `get()` durch einen String oder ein Array, bricht der anschließende Aufruf `->first()` in `first()`. Beschränken Sie sich in Scopes auf Verarbeitungen, die Modelle, den Collection-Typ, die Anzahl und die Reihenfolge beibehalten.

## Unterschiede zwischen den Abrufmethoden

Die folgende Tabelle zeigt, wie ein **im Eloquent Builder registriertes** `afterQuery()` behandelt wird.

| Aufgerufene Methode | An den Callback übergebenes Ergebnis | Hinweise |
| - | - | - |
| `get()` | Eloquent Collection | Wird auch bei 0 Datensätzen mit einer leeren Collection ausgeführt |
| `first()` / `firstOrFail()` | Eloquent Collection | Nicht das Modell selbst, sondern eine Collection mit 0 oder 1 Element. Das erste Element wird nach der Verarbeitung entnommen |
| `paginate()` | Eloquent Collection mit den Modellen der Seite | Nicht der Paginator. Ist die Gesamtzahl 0, wird `get()` nicht aufgerufen und der Callback nicht ausgeführt |
| `simplePaginate()` / `cursorPaginate()` | Eloquent Collection | Kann ein zusätzliches Element zur Prüfung der nächsten Seite enthalten, daher werden bis zu `perPage + 1` Elemente verarbeitet |
| `cursor()` | Eloquent Collection mit einem Modell | Wird beim Konsumieren der LazyCollection für jede Zeile ausgeführt und gibt das erste Element des verarbeiteten Ergebnisses zurück |
| `lazy()` / `chunk()` / `chunkById()` | Eloquent Collection pro Chunk | Wird bei jedem internen `get()` ausgeführt. Kann auch für das leere Ergebnis zur Endeerkennung ausgeführt werden |
| `pluck()` | Support Collection mit Werten | Keine Modelle. Wird nach Casts und Mutatoren der Zielspalte ausgeführt |
| `count()` / `exists()` | Nicht übergeben | Umgeht die Eloquent-Callbacks und delegiert an den zugrunde liegenden Query Builder |
| `toBase()->get()` | Nicht übergeben | Registrierungen auf Eloquent-Seite werden nicht an den Basis-Builder übertragen. Der Basis-Builder hat ein eigenes `afterQuery()` |

`first()` ruft intern `limit(1)->get()->first()` auf. Nur weil Sie `first()` verwenden, dürfen Sie den Parameter der Closure nicht auf `Post` ändern.

> [!WARNING]
>
> Der Scope `keyedById()` auf dieser Seite dient dem Abruf von Modellen. Mit `Post::keyedById()->pluck('id')` können Sie ihn nicht verwenden. `pluck()` übergibt eine `Illuminate\Support\Collection`, die nicht zum Typ `Illuminate\Database\Eloquent\Collection` im Beispiel passt. Eine gelockerte Typdeklaration allein macht Code, der aus Werten auf Modellattribute zugreift, nicht sicher.

Wenn Sie nur Werte benötigen, verzichten Sie auf den Scope und schreiben `Post::query()->pluck('id')`. Um Werte nach der Verarbeitung der Modelle zu entnehmen, können Sie `Post::keyedById()->get()->pluck('id')` schreiben. Das `pluck()` im zweiten Fall ist eine Operation auf der abgerufenen Collection, nicht auf dem Query Builder.

### Seitenanzahl und nächste Abrufposition nicht verändern

`paginate()` ermittelt die Gesamtzahl per SQL und ruft danach die Modelle der Seite ab. Filtern Sie die Collection nach dem Abruf, wird die Gesamtzahl nicht neu berechnet.

Außerdem entscheiden `simplePaginate()` und `cursorPaginate()` anhand des verarbeiteten Ergebnisses, ob es eine nächste Seite gibt. `chunkById()` verwendet die ID am Ende eines Chunks als nächste Abrufposition. Wenn Sie im Callback Anzahl oder Reihenfolge ändern, stimmen Seitenerkennung und Fortsetzungsposition möglicherweise nicht mehr mit der ursprünglichen Abfrage überein.

Geben Sie Filterung, Sortierung und abgerufene Spalten im SQL an, und beschränken Sie `afterQuery()` auf Verarbeitungen, die die Bedeutung der abgerufenen Ergebnisse nicht verändern. Wenn Sie nur das Ausgabeformat einer API ändern möchten, ziehen Sie auch eine Umwandlung mit [API-Ressourcen](https://kawax.biz/de/eloquent-resources.md) in Betracht.

Die offizielle Dokumentation enthält ein Beispiel, das Modellen ein Attribut mit einem Auszug aus Suchergebnissen hinzufügt. Zuweisungen an Modellattribute können auch die zu speichernden Daten verändern. Vermeiden Sie Designs, bei denen Modelle mit reinen Anzeigeattributen, die nicht in der Datenbank existieren, direkt mit `save()` gespeichert werden.

## Registrierungen bleiben im Query Builder erhalten

Jeder Aufruf von `afterQuery()` fügt eine Closure hinzu, und die Ausführung entfernt die Registrierung nicht. Ein erneuter Abruf mit demselben Builder führt sie erneut aus, und wenn Sie denselben Scope zweimal aufrufen, wird auch der Callback zweimal registriert. Auch beim Klonen des Builders werden die registrierten Callbacks übernommen.

In Code mit mehreren Abrufen oder in langlebigen Workern erstellen Sie für jeden Zweck eine neue Abfrage und beschränken Callbacks auf eine Ergebnisverarbeitung ohne Seiteneffekte. Wenn Sie dort E-Mail-Versand oder Persistierung unterbringen, verwechselt man mehrfache Ausführungen durch Chunks oder Cursor leicht mit einem einzigen fachlichen Vorgang.

Wenn Sie wiederverwendbare Scopes in einem Paket bereitstellen, legen Sie die unterstützten Abrufmethoden ebenfalls als öffentliche API fest und testen Sie den Vertrag zu Typen und Aufrufanzahl.

## Unterschiede zwischen den Abrufwegen testen

Prüfen Sie in der nutzenden Laravel-13-Anwendung nicht nur die Schlüsselverarbeitung, sondern auch die Aufrufe bei `first()`, `cursor()` und leeren Seiten.

Das folgende Beispiel setzt das vorherige `Post`-Modell und eine Migration für eine `posts`-Tabelle mit `id`, `title`, `body`, `created_at` und `updated_at` voraus. Mit [RefreshDatabase](https://kawax.biz/de/database-testing.md) werden die Datensätze zwischen den Tests isoliert.

Erstellen Sie den 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);
    }
}
```

Sie können nur diesen Test ausführen.

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

## Checkliste für die Wartung

* Legen Sie die vom Scope unterstützten Abrufmethoden sowie Parameter- und Rückgabetypen der Closure fest.
* Prüfen Sie ein leeres Ergebnis von `get()` und ein `paginate()` mit Gesamtzahl 0 getrennt.
* Stellen Sie bei Chunks und Cursorn sicher, dass sich Aufrufanzahl und Abrufreihenfolge nicht ändern.
* Gestalten Sie den Code so, dass doppelt registrierte Callbacks oder wiederverwendete Builder keine fachlichen Seiteneffekte auslösen.
* Prüfen Sie bei Laravel-Updates die Aufrufwege von `get()`, `pluck()`, `cursor()` und `BuildsQueries` erneut.

## Verwandte Seiten

> [card] [Eloquent-Scopes](https://kawax.biz/de/advanced/eloquent-scopes.md)
>
> Die Grundlagen, um SQL-Einschränkungen in lokalen und globalen Scopes zu bündeln.

> [card] [Eloquent Collections](https://kawax.biz/de/eloquent-collections.md)
>
> Der Unterschied zwischen Collections mit Modellen und Support Collections.

> [card] [Eloquent Observer und Modell-Events](https://kawax.biz/de/advanced/eloquent-observers.md)
>
> Events pro Modell, die sich von der Verarbeitung abgerufener Ergebnisse unterscheiden.

> [card] [API-Ressourcen](https://kawax.biz/de/eloquent-resources.md)
>
> Die Umwandlung der von der API zurückgegebenen Anzeigedaten vom Speichern der Modelle trennen.

## Verwendete Primärquellen

* [Offizielle Laravel-Dokumentation: 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)
* [Offizieller Laravel-Integrationstest: AfterQueryTest](https://github.com/laravel/framework/blob/v13.40.0/tests/Integration/Database/AfterQueryTest.php)


## Related topics

- [Eloquent-Scopes](https://kawax.biz/de/advanced/eloquent-scopes.md)
- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Eloquent-Collections](https://kawax.biz/de/eloquent-collections.md)
- [Query Builder](https://kawax.biz/de/query-builder.md)
- [Eloquent-Observer und Modell-Events](https://kawax.biz/de/advanced/eloquent-observers.md)


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