Zum Hauptinhalt springen
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.
Diese Seite bezieht sich auf Laravel 13. Ausgehend von After Query Callbacks in der offiziellen Dokumentation 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.
Aufrufer können ihn wie jeden anderen Scope verketten.
$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.

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. 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. 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.
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.
Verwendet wird hier nicht ??, sondern ?:, das auf der Truthy-Prüfung von PHP beruht. 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. first() ruft intern limit(1)->get()->first() auf. Nur weil Sie first() verwenden, dürfen Sie den Parameter der Closure nicht auf Post ändern.
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 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 werden die Datensätze zwischen den Tests isoliert. Erstellen Sie den Test in tests/Feature/AfterQueryTest.php.
Sie können nur diesen Test ausführen.

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

Eloquent-Scopes

Die Grundlagen, um SQL-Einschränkungen in lokalen und globalen Scopes zu bündeln.

Eloquent Collections

Der Unterschied zwischen Collections mit Modellen und Support Collections.

Eloquent Observer und Modell-Events

Events pro Modell, die sich von der Verarbeitung abgerufener Ergebnisse unterscheiden.

API-Ressourcen

Die Umwandlung der von der API zurückgegebenen Anzeigedaten vom Speichern der Modelle trennen.

Verwendete Primärquellen

Zuletzt geändert am 6. November 2026