mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 06:52:09 +01:00
False positive A managed+dirty entity can not be scheduled for insertion exception due to spl_object_id() collision
#7545
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @eXsio on GitHub (Aug 27, 2025).
Bug Report
Summary
This one is going to be borderline impossible to create a replication for, but I'll try to explain what I think is happening:
Doctrine uses
spl_object_id()PHP function to uniquely identify an Entity within theUnitOfWork. The problem with that function is that it provides unique values for all objects that are currently referenced anywhere and haven't been GC'd. So, in theory, it can produce duplicate values, if the Object was there, but was GC'd. This theory became reality for me recently.I'm using PHP 8.4, Symfony 7.3 and Doctrine 3.5.0. When switching from
devtoprodenvs in Symfony I've began to experience a weird "A managed+dirty entity can not be scheduled for insertion" exceptions from within Doctrine.Upon further investigation it was obvious that my code is correct and something weird is happening with Doctrine, because my code was extremely straightforward:
persist()on itIt turned out that, in the process of handling a Request, I had another Entity that PHP assigned the same value of
spl_object_id(), but that Entity was deleted and un-referenced by everything in the code, so PHP has GC'd it. Upon instantiating my new Entity, PHP reused the same value ofspl_object_id()and the followingUnitOfWorkpiece of code threw an Exception:200a505f36/src/UnitOfWork.php (L1390-L1392)So, to sum up, it is perfectly possible to stumble upon this exception, even using this simple code:
My Workaround
I've created a Doctrine Listener on a
preRemoveEvent and created anarraythat I store all removed Entities into. I never read from it, I'm just making sure that the object is referenced somewhere, so that it doesn't get GC'd and it'sspl_object_id()value isn't available for another object. This can potentially have negative memory consumption impact, but I have no other choice.Proposed fixes
There is a number of things that can be done here, but the most important thing is to acknowledge that
spl_object_id()can potentially produce collisions and never treat it as an absolutely unique value.managed+dirtyexception altogether, acknowledging that the check can be invalid and lead to false positives. Alternatively, we could assume that, if this check returnstrue, we are in the current scenario, we clear up the$originalEntityDataand assume that the later/newer Entity has priority over the older one (and log a Warning).UnitOfWorkso that we make sure that thespl_object_id()doesn't produce collisions, at least in the context of Doctrine's world.$originalEntityDatacould be cleaned as a part of Entity Deletion process. This would help if the deletion was done throughEntityManager::remove(), but still could cause failures if aQueryBuilder::deletewas used to bulk-remove Entities.Current behavior
"A managed+dirty entity can not be scheduled for insertion" Exception can potentially be thrown for an Entity that was never touched by an Entity Manager, depending on what was already done by the Unit of Work:
Expected behavior
Entity is persisted without exceptions.
How to reproduce
Impossible to reliably reproduce, because this bug depends on a number of different factors (like PHP behavior, frameworks used etc) and occurrs randomly (at lest for me).
@pich commented on GitHub (Aug 27, 2025):
+1
@greg0ire commented on GitHub (Aug 27, 2025):
Regarding your claim that this cannot be reproduced, wouldn't
gc_collect_cycles()help with that? Also, what if$oldEntityis not part of a cycle in the first place? Wouldn't its Id get immediately freed?Regarding your claim that the simple code you mentioned would reproduce the issue, wouldn't you need to
unset($oldEntity);for the id to be freed?If this is as simple as you say it is, I'm a bit surprised that you are the first to experience this. Are there no existing bug reports about this? I'm on my phone. Do you think it could have been introduced recently?
Note that in the past, we used
spl_object_hash(), switching back to that might be a solution.@pich please use emoji reactions instead of +1
@eXsio commented on GitHub (Aug 27, 2025):
I understand that my examples may not fully describe the issue. I've stumbled upon it while working on a pretty complex system and it occurred only in
prodenv for some reason. I don't understand the internal logic of PHP's oid generation and what it actually takes to reliably generate a collision.The things I know for sure was that there was a oid collision and that keeping an object reference on the side fixed it. From what I understand both object hash and id suffer from collision possibility. (given that one od the objects gets GC'd), as per PHP documentation.
It may just as well be a rare edge case, but it is possible.
@greg0ire commented on GitHub (Aug 27, 2025):
Maybe
scheduleForDelete()should act onoriginalEntityDatainstead of waiting forexecuteDeletions()to do so 🤔@stof commented on GitHub (Oct 29, 2025):
This suffers exactly form the same issue, given that
spl_object_hash()is derived from the object id:336fbf09d7/ext/spl/php_spl.c (L664-L680)@beberlei commented on GitHub (Oct 30, 2025):
EntityManager::remove & flush should cleanup all spl_object_id references already, so either that does not work anymore or we have a more complicated scenario at hand here