Skip to content

[2.x] fix(api): guard EloquentBuffer against orphaned/mock models#4669

Merged
imorland merged 1 commit into
2.xfrom
im/eloquent-buffer-orphan-guard
May 21, 2026
Merged

[2.x] fix(api): guard EloquentBuffer against orphaned/mock models#4669
imorland merged 1 commit into
2.xfrom
im/eloquent-buffer-orphan-guard

Conversation

@imorland
Copy link
Copy Markdown
Member

Summary

  • Filter models with exists=false (or a null key) out of the collection passed to load()/loadAggregate() in EloquentBuffer::load(), and always clear the buffer entry so stale mocks don't accumulate.

Background

Closes #4667.

Laravel's Illuminate\Database\Eloquent\Collection::loadAggregate() (see Collection.php:127) reads attributes off the per-key result map without a null check:

$extraAttributes = Arr::only($models->get($model->getKey())->getAttributes(), $attributes);

If a model in the source collection has a key that doesn't match any DB row, $models->get(...) is null and the request fatals with Call to a member function getAttributes() on null.

The JSON:API BelongsTo linkage shortcut in EloquentResource::getRelationshipValue() (vendor/flarum/json-api-server) and the mirror in our AbstractDatabaseResource::getRelationshipValue() create exactly this kind of mock model:

return $related->newInstance()->forceFill([$related->getKeyName() => $key]);

When such a mock ends up in EloquentBuffer for a relationAggregate field (e.g. fof/upload's countRelation('foffiles') on UserResource), the flush at the end of the request crashes the whole response with a 500 — affecting any page that serializes that resource (including the forum index for guests).

Fix

In EloquentBuffer::load():

  • Filter the buffered models down to those that actually exist and have a non-null key before constructing the collection that gets passed to load() / loadAggregate().
  • Clear the buffer entry unconditionally (moved above the early return) so the same stale mocks aren't re-walked on subsequent loads.

Test plan

  • New regression test EloquentBufferTest::aggregate_load_does_not_crash_for_models_that_do_not_exist_in_the_database — fails on 2.x with the exact stack from the issue, passes with this fix.
  • tests/integration/extenders/ApiResourceTest.php — 41/41 pass.
  • tests/integration/api/discussions/ — 39/39 pass.
  • tests/integration/api/users/ + tests/integration/api/posts/ — 136/136 pass.

Laravel's Collection::loadAggregate fatals with "Call to a member
function getAttributes() on null" when the collection contains a model
whose key has no matching row in the database — the JSON:API BelongsTo
linkage shortcut can produce such mock models (exists=false) for
orphaned foreign keys, which then crash the request when an aggregate
field (e.g. fof/upload's countRelation on UserResource) flushes the
buffer. Filter non-existent models out before forwarding to load()/
loadAggregate(), and always clear the buffer entry so stale mocks
don't accumulate.

Refs #4667.
@imorland imorland added this to the 2.0.0-rc.2 milestone May 21, 2026
@imorland imorland marked this pull request as ready for review May 21, 2026 20:40
@imorland imorland requested a review from a team as a code owner May 21, 2026 20:40
@imorland imorland merged commit 65de555 into 2.x May 21, 2026
25 checks passed
@imorland imorland deleted the im/eloquent-buffer-orphan-guard branch May 21, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fatal Error: Call to a member function getAttributes() on null when aggregating on orphaned relationship/mock models

1 participant