ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Unknown
    • None
    • Affects Version/s: None
    • None
    • None
    • None
    • None
    • None
    • None

      Summary: EmbedsOneOrMany::match() sets the root model's parent relation during eager loading, causing wrong write paths and infinite recursion on re-serialization (Laravel 13)

      Environment

      Component Version
      laravel-mongodb 5.10.0 (same code in 5.11.0 and on 5.x at 2139f38)
      PHP 8.4.15
      Database driver ext-mongodb 2.5.3, MongoDB server 8.2.3
      Laravel 13.34.0

      Description

      EmbedsOneOrMany::match() calls $model->setParentRelation($this) on each parent model it eager-loads into:
      EmbedsOneOrMany.php#L99-L110

      After User::with('settings'), the root User reports an EmbedsOne as its parent relation, as if it were itself embedded inside settings. The embedded children already get the correct parent in toModel(), so this line only adds a wrong pointer to the root.

      Two effects:

      1. Writes go to the wrong path. A later write to a different embedded relation on the root is stored under settings. because getPathHierarchy() now treats the root as nested.
      2. Infinite recursion when the model is serialized again (new in Laravel 13). Laravel 13 SerializesAndRestoresModelIdentifiers::restoreCollection() now calls loadMissing() on restored collections (laravel/framework#58477, upgrade-guide item "Collection Model Serialization Restores Eager-Loaded Relations"). That runs match(). When the restored models are serialized again (a job dispatching another job, a queued listener, a notification), getQueueableRelations() / getRelationsWithoutParent() unset the wrong back-link, and serialization recurses parent → embed → parent until memory runs out. On Laravel 11 this only happened for single queued models; Laravel 13 makes it hit every queued collection.

      This is the same bug reported in #488, #641 and #1743 (closed without a fix). The Laravel 13 queue change makes it much easier to hit.

      Steps to Reproduce

      Plain models, nothing app-specific:

      use MongoDB\Laravel\Eloquent\Model;
      use MongoDB\Laravel\Relations\EmbedsMany;
      use MongoDB\Laravel\Relations\EmbedsOne;
      
      class User extends Model
      {
          protected $guarded = [];
          public function settings(): EmbedsOne { return $this->embedsOne(Settings::class); }
          public function addresses(): EmbedsMany { return $this->embedsMany(Address::class); }
      }
      
      class Settings extends Model
      {
          protected $guarded = [];
          public function theme(): EmbedsOne { return $this->embedsOne(Theme::class); }
      }
      
      class Theme extends Model { protected $guarded = []; }
      class Address extends Model { protected $guarded = []; }
      

      A. Wrong write path

      $user = User::create(['name' => 'A']);
      $user->settings()->create(['lang' => 'en']);
      
      User::find($user->id)->getParentRelation();                 // null
      $loaded = User::with('settings')->find($user->id);
      $loaded->getParentRelation();                                // EmbedsOne  <-- wrong
      $loaded->addresses()->create(['city' => 'Paris']);
      

      Stored document:

      {"name": "A", "settings": {"lang": "en", "addresses": [{"city": "Paris"}]}}
      

      B. Recursion on re-serialize (Laravel 13)

      use Illuminate\Queue\SerializesModels;
      
      class SomeJob { use SerializesModels; public function __construct(public $users) {} }
      
      foreach (['A', 'B'] as $name) {
          $u = User::create(['name' => $name]);
          $u->settings()->create(['lang' => 'en'])->theme()->create(['color' => 'blue']);
      }
      
      $users = User::all();
      foreach ($users as $u) { $u->settings->theme; }   // queueable relations: ["settings", "settings.theme"]
      
      $restored = unserialize(serialize(new SomeJob($users)));
      $restored->users->first()->getParentRelation();   // EmbedsOne  <-- wrong, set by match()
      serialize(new SomeJob($restored->users));         // never returns
      

      Expected Behaviour

      • Eager-loading an embedded relation does not change the root model's parent relation (getParentRelation() stays null).
      • addresses()->create() writes to addresses.
      • A restored collection serializes again normally.

      Actual Behaviour

      • The root model's parent relation is the EmbedsOne it loaded.
      • The address is stored under settings.addresses.
      • The second serialize() fails with:
        Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 20480 bytes) in vendor/mongodb/laravel-mongodb/src/Eloquent/DocumentModel.php on line 683
        

      Suggested Fix

      Remove $model->setParentRelation($this); from match(). Children already get their parent in toModel(), and a model that really is embedded gets its parent when it is built. We have not run this against the package test suite.

      Current Workaround

      We work around the queue side by keeping embedded relations out of getQueueableRelations() in our base model. Embedded data comes back with the document anyway, so nothing is lost on restore. This does not cover the write-path case.

              Assignee:
              Jérôme Tamarelle
              Reporter:
              Muhammad Memon (EXT)
              Votes:
              0 Vote for this issue
              Watchers:
              2 Start watching this issue

                Created:
                Updated: