Skip to content

Fix: nested relations not serialized for repeated child entities across index items#723

Merged
binaryk merged 1 commit intoBinarCode:10.xfrom
george-todica:fix-nested-relations-shared-child
Mar 30, 2026
Merged

Fix: nested relations not serialized for repeated child entities across index items#723
binaryk merged 1 commit intoBinarCode:10.xfrom
george-todica:fix-nested-relations-shared-child

Conversation

@george-todica
Copy link
Copy Markdown
Contributor

@george-todica george-todica commented Mar 27, 2026

The Bug

When requesting ?related=vendor.contacts on an index endpoint, contacts are only serialized for the first occurrence of each vendor. If multiple parent records (e.g., InvoicePayments) reference the same vendor, the 2nd+ occurrences get the vendor serialized without its nested relationships.

Root Cause

RelatedDto::$resolvedRelationships is a request-scoped singleton that tracks which entity+relation combinations have been serialized,
using the key {uriKey}{primaryKey}{relation} (e.g., vendors5contacts). This was designed to prevent circular references (e.g., users.companies.users creating an infinite loop).

The problem: resolvedRelationships is global across all index items. When PHP's json_encode serializes the response, it processes each item's nested Repository objects sequentially. The first vendor's contacts resolution adds vendors5contacts to the list. When the second vendor (same ID, different parent) tries to resolve contacts, unserialized() finds it already in the list and filters it out.

Why these specific changes

ScopedRelatedItem wrapper (new class) — Wraps each index item in a JsonSerializable that clears resolvedRelationships before json_encode processes that item's nested repos. This scopes the deduplication to each top-level item: circular references within one item are still prevented, but the same child entity under different parents gets a fresh slate.

.all() on the data collection — Critical. Without this, Collection::jsonSerialize() calls jsonSerialize() on ALL ScopedRelatedItem wrappers at once (via array_map ), collecting the unwrapped results into an array BEFORE json_encode processes any nested repos. The .all() converts to a raw PHP array so json_encode processes each item sequentially — calling ScopedRelatedItem::jsonSerialize() (which clears) → fully serializing that item's nested repos → then moving to the next item.

Segment-by-segment dot-notation validation in prepareRelations()$query->getRelation('vendor.contacts') fails because Eloquent's getRelation() doesn't handle dot notation (it tries to call $model->{'vendor.contacts'}() ). This silently filtered dot-notation relations out of the with() call, causing N+1 queries for nested relations. The fix validates each segment independently (vendor → valid, contacts on vendor's model → valid), while keeping the full dot string for $query->with() which does handle dots natively.

Alternatives considered and rejected

  1. spl_object_id in uniqueIdentifierForRepository() — Each Repository instance gets a unique PHP object ID. This fixes the dedup issue but breaks circular reference protection because circular entities wrapped in new Repository objects have different spl_object_id values → infinite recursion → stack overflow.
  2. Ancestor chain (push/pop in jsonSerialize() ) — Track which entities are "currently being serialized" via a stack. Block an entity if it appears in its own ancestor chain. Fails because jsonSerialize() returns an array with nested Repository objects that are serialized after the parent's jsonSerialize() returns (and pops). The ancestor is no longer on the stack when descendants serialize.
  3. Clearing resolvedRelationships in the performIndexAsArray() map — Runs before json_encode , so by the time nested repos serialize, the cleared state is stale (all items' top-level relations have already added their entries).

@what-the-diff
Copy link
Copy Markdown

what-the-diff bot commented Mar 27, 2026

PR Summary

  • Introduction of a new code module
    A new class called 'ScopedRelatedItem' has been added, which redefines how certain data sets are interpreted and structured.

  • Update to existing database functionality
    There's a modification to the 'Repository' component to now utilize the newly introduced 'ScopedRelatedItem' class. This alters how the data is structured for easy retrieval.

  • Improvement of data linking processes
    The logic that prepares relations among data entities in the 'RepositorySearchService' has been further refined. This strengthens the validation process for nested relations, making sure that all associations within a data chain are validated.

  • Addition of a new verification test
    A new test has been introduced to confirm that interconnected data entries load correctly, especially for those having the same underlying linkage. This ensures the nested relations function as expected.

@arthurkirkosa arthurkirkosa requested a review from binaryk March 27, 2026 09:01
@binaryk binaryk merged commit bf025b5 into BinarCode:10.x Mar 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants