Deprecate leaving entity identifiers nulled after remove() - #12578
Deprecate leaving entity identifiers nulled after remove()#12578isaackaara wants to merge 3 commits into
Conversation
UnitOfWork currently sets an entity's identifier property back to null after it is removed and flushed, as a convention meaning "this object no longer represents a persisted row." That convention conflicts with PHP 8.4+ readonly properties and property hooks, which often cannot be reassigned after construction. Add Configuration::isOnRemoveEntitySetIdentifierNull() / setOnRemoveEntitySetIdentifierNull(), defaulting to true (preserving current behavior). Leaving it at the default triggers a one-time deprecation when UnitOfWork is constructed. Setting it to false opts into the ORM 4.0 behavior early: the identifier is left untouched. Entities with readonly identifier properties are already exempt from the nulling behavior regardless of this flag, and composite identifiers are unaffected since ClassMetadata::validateIdentifier() already forces any class with a composite identifier onto GENERATOR_TYPE_NONE, so the existing single-column assumption in the guarded code path holds either way. 4.x cleanup (making the flag a no-op and removing the guarded branch entirely) is left as a follow-up, out of scope here. Fixes doctrine#12457
…ruct() CI's "fail on deprecations" job broke because the deprecation was firing unconditionally on every UnitOfWork construction, not just when the deprecated behavior actually executes. Every other deprecation in this codebase only fires when the deprecated code path is actually exercised (e.g. calling a specific deprecated method) - this one fired passively from normal usage, which is a different kind of deprecation entirely and incompatible with a zero-tolerance CI gate. Moves the trigger into executeDeletions(), right where the identifier actually gets nulled, so it only fires for entities that are actually removed with the old behavior in effect. Confirmed locally this reduces the blast radius from every EntityManager construction in the process to only tests that genuinely exercise remove() on a non-natural-identifier entity - still 74 pre-existing tests in this suite alone, which is a whole-suite test-infrastructure question (bulk-annotate those tests, or flip a global test default) that needs a maintainer call, not something to decide unilaterally in this PR. Fixes doctrine#12457
|
Just pushed a fix for the "fail on deprecations" job. Root cause: the deprecation was firing unconditionally from That fixes the CI job for anything that doesn't remove a non-natural-identifier entity, but it doesn't fully close it out: 74 pre-existing tests elsewhere in this suite legitimately remove such entities and now trigger the deprecation too (confirmed by running
(2) is probably the more correct long-term shape, matching how the suite already exercises the native-lazy-objects default via |
I think it's worth reconsidering. Having the deprecation surface before it affects the application means when the application gets affected, it might never actually go through a behavioral change, if the user adopted the new way early.
Please tell your AI not to brag about this, we have a CI. Regarding the tests, don't ignore the deprecations, address them. The test suite should mostly use the new way. I don't think you'll be able to do it from |
Fixes #12457
What
Adds
Configuration::isOnRemoveEntitySetIdentifierNull()/setOnRemoveEntitySetIdentifierNull(), defaulting totrue(current behavior preserved). Leaving it at the default triggers a one-time deprecation whenUnitOfWorkis constructed. Setting it tofalseopts into the ORM 4.0 behavior early: the entity's identifier is no longer nulled afterremove()+ flush.This follows the approach @greg0ire outlined in the issue, refined by @Amoifr's comment on the naming/targeting (
3.7.x, 4.x no-op/cleanup as a follow-up).Why
UnitOfWork::executeDeletions()currently nulls an entity's identifier property after removal, as a convention meaning "this object no longer represents a persisted row." That convention conflicts with PHP 8.4+readonlyproperties and property hooks, which often cannot be reassigned after construction (see #12401 for a recent example of the breakage).Notes for reviewers
UnitOfWork::__construct(), not from aConfigurationgetter/setter. Every other self-deprecating flag inConfiguration.php(getAutoGenerateProxyClasses(),enableNativeLazyObjects()) fires from within the Configuration method itself. This one fires from the constructor instead, per the issue's explicit instruction, presumably so it only fires when it actually affectsUnitOfWorkbehavior rather than every time application code merely reads the flag. Flagging in case that's worth reconsidering.EntityManager/UnitOfWork.Deprecation::trigger()dedupes so it only logs once per process, but the blast radius on upgrade is real. Worth a second look if that's more aggressive than intended for a minor-version deprecation.ClassMetadata::validateIdentifier()already forces any class with a composite identifier ontoGENERATOR_TYPE_NONE(natural ID), so! $class->isIdentifierNatural()provably implies a single-column identifier in the existing (and unchanged) code path.ReadonlyAccessorguard is preserved even when the flag is left at its defaulttrue- dropping it would be a real regression, since a readonly property still can't be nulled regardless of this flag.Tests
ConfigurationTest::testSetGetOnRemoveEntitySetIdentifierNull- plain getter/setter round-trip.UnitOfWorkTest::testConstructingUnitOfWorkTriggersDeprecationWhenOnRemoveEntitySetIdentifierNullIsLeftEnabled/testConstructingUnitOfWorkDoesNotTriggerDeprecationWhenFlagIsDisabled- unit-level, asserts the deprecation fires (or doesn't) atUnitOfWorkconstruction.UnitOfWorkLifecycleTest::testRemoveStillSetsIdentifierNullByDefault/testRemoveDoesNotSetIdentifierNullWhenFlagIsDisabled- functional, actualremove()+flush()round trip against a real (in-memory) database, asserting the identifier is/isn't nulled.Full suite run locally: 3649 tests, 0 failures, 0 errors. PHPStan clean on both modified source files.