Lazy Ghost Proxy Hydration Overwrites Local Entity Changes #7490

Open
opened 2026-01-22 15:52:16 +01:00 by admin · 3 comments
Owner

Originally created by @ilianiv on GitHub (Mar 26, 2025).

Bug Report

Q A
Version 3.3.2

Summary

Proxy hydration overwrites local entity changes, potentially causing data inconsistencies.

Current behavior

In a setup with Employee and Car entities having a OneToOne bidirectional relationship, where Employee is the owning side with car_id in the database, and Car has an is_free boolean indicating its association status:

To detach a Car from an Employee, the following steps are undertaken:

  1. Load the Employee entity from the database.
  2. Access the Car from the Employee through lazy loading (resulting in a lazy ghost proxy).
  3. Set Employee::$car to null.
  4. Set Car::$employee to null. (This does not trigger hydration since Car::$employee is part of the lazy proxy.)
  5. Set Car::$is_free to true. (This triggers hydration, overwriting the Car::$employee property.)

After these steps, Car::$employee unexpectedly becomes non-null.

Expected behavior

Hydration should not overwrite existing properties in a lazy-ghost proxy to prevent unintentional data loss.
Maybe there should be a mechanism that prevents local modifications to lazy-ghost proxies before they are hydrated.

How to reproduce

https://github.com/ilianiv/lazy-ghost-issue

Originally created by @ilianiv on GitHub (Mar 26, 2025). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |-------------------------------------------- | ------ | Version | 3.3.2 #### Summary Proxy hydration overwrites local entity changes, potentially causing data inconsistencies. #### Current behavior In a setup with Employee and Car entities having a OneToOne bidirectional relationship, where Employee is the owning side with car_id in the database, and Car has an is_free boolean indicating its association status: To detach a Car from an Employee, the following steps are undertaken: 1. Load the Employee entity from the database. 2. Access the Car from the Employee through lazy loading (resulting in a lazy ghost proxy). 3. Set Employee::$car to null. 4. Set Car::$employee to null. (This does not trigger hydration since Car::$employee is part of the lazy proxy.) 5. Set Car::$is_free to true. (This triggers hydration, overwriting the Car::$employee property.) After these steps, Car::$employee unexpectedly becomes non-null. #### Expected behavior Hydration should not overwrite existing properties in a lazy-ghost proxy to prevent unintentional data loss. Maybe there should be a mechanism that prevents local modifications to lazy-ghost proxies before they are hydrated. #### How to reproduce https://github.com/ilianiv/lazy-ghost-issue
Author
Owner

@beberlei commented on GitHub (Mar 29, 2025):

@ilianiv in Step 4 you write "(This does not trigger hydration since Car::$employee is part of the lazy proxy.)" - Cann you clarify what you mean by that?

@beberlei commented on GitHub (Mar 29, 2025): @ilianiv in Step 4 you write "(This does not trigger hydration since Car::$employee is part of the lazy proxy.)" - Cann you clarify what you mean by that?
Author
Owner

@ilianiv commented on GitHub (Mar 31, 2025):

I mean that the field employee is already assigned to the Car entity, despite the fact that the Car is still an uninitialized proxy. This is likely an optimization as the Car entity was retrieved via the Employee<->Car association. This behavior prevents the hydration skipping the magic getter (LazyGhostTrait::__get)

Image

@ilianiv commented on GitHub (Mar 31, 2025): I mean that the field `employee` is already assigned to the Car entity, despite the fact that the Car is still an uninitialized proxy. This is likely an optimization as the Car entity was retrieved via the Employee<->Car association. This behavior prevents the hydration skipping the magic getter *(LazyGhostTrait::__get)* ![Image](https://github.com/user-attachments/assets/7e9b87d2-9e3e-4506-9564-16896b2b02d5)
Author
Owner

@ilianiv commented on GitHub (May 8, 2025):

Hello, @beberlei!
I've tried updating to doctrine/orm@3.3.3 but this problem still exists.
Can you confirm that this is not the intended behavior and have you been able to replicate it?

@ilianiv commented on GitHub (May 8, 2025): Hello, @beberlei! I've tried updating to doctrine/orm@3.3.3 but this problem still exists. Can you confirm that this is not the intended behavior and have you been able to replicate it?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7490