Naar hoofdinhoud gaan
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.
Deze pagina is gericht op Laravel 13. Uitgaande van After Query Callbacks in de officiële documentatie 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.
De aanroeper kan hem net als elke andere scope chainen.
$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.

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. 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. 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.
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.
Hier wordt niet ?? gebruikt, maar ?:, dat steunt op de truthy-evaluatie van PHP. 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. 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.
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. 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 isoleert de records tussen tests. Maak de test aan in tests/Feature/AfterQueryTest.php.
Je kunt alleen deze test uitvoeren.

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

Eloquent-scopes

De basis om SQL-beperkingen te bundelen in lokale en globale scopes.

Eloquent-collecties

Het verschil tussen Collections met modellen en Support Collections.

Eloquent-observers en model-events

Events per model, die verschillen van het verwerken van opgehaalde resultaten.

API-resources

Scheid de transformatie van door de API teruggegeven weergavegegevens van het opslaan van modellen.

Geraadpleegde primaire bronnen

Laatst gewijzigd op 6 november 2026