[DeepClone] Fix round-tripping and hydrating BcMath\Number#628
Merged
Conversation
BcMath\Number is a final internal class whose __unserialize() rejects an empty payload, so it has no empty-shell prototype. Two bugs surfaced: - deepclone_from_array() reconstructs it via the full O: serialization form (which runs __unserialize() internally), then called __unserialize() a second time, throwing "Cannot modify readonly property". The second call is now skipped for such objects. - deepclone_hydrate() reached `clone null` (TypeError) because the class is reported cloneable while its prototype is null. It now throws \DeepClone\NotInstantiableException, matching the extension: such a class can only be reconstructed via a full serialization round-trip, not by property injection.
nicolas-grekas
added a commit
that referenced
this pull request
Jun 8, 2026
…ests another object (nicolas-grekas) This PR was merged into the 1.x branch. Discussion ---------- [DeepClone] Fix round-tripping objects whose __serialize() nests another object | Q | A | ------------- | --- | Branch? | 1.x | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT Builds on #628 (which adds the states-loop guard this relies on). A `needsFullUnserialize` object (a `final` internal class whose `__unserialize()` rejects an empty payload, so it has no empty-shell prototype) whose `__serialize()` nests another object carries an object-ref mask on its reconstruction state. `Random\Randomizer`, which wraps a `Random\Engine\*`, is the common case. `reconstruct()` defers these to the states loop, but the eager-finalize pass that turns deferred objects into real instances (so the properties loop can resolve references to them) skipped any state with an object-ref mask. The placeholder stayed `null`, and as soon as another object referenced it the properties loop threw `deepclone_from_array(): ... unknown object id`. The extension does not have this problem: it instantiates every object shell up front via `object_init_ex()` and calls `__unserialize()` later. Reproducer (works with the extension, threw with the polyfill): ```php $g = (object) ['r' => new \Random\Randomizer(new \Random\Engine\Mt19937(1234))]; deepclone_from_array(deepclone_to_array($g)); ``` These objects are now finalized to a fixpoint after the refs pass, once the objects their mask references exist (a remaining cycle, which pure PHP cannot reconstruct, is still reported by the existing loops). `Random\Randomizer` now round-trips as an object property, top-level, in arrays and nested. Known limitation: a *shared* object nested inside such a state is reconstructed as an independent copy, because pure PHP cannot inject a live shared object into an internal `final` class's `__unserialize()`. The extension preserves identity in that case. This only affects an object shared between a `needsFullUnserialize` object's serialized state and the rest of the graph. Commits ------- daa74de [DeepClone] Fix round-tripping objects whose __serialize() nests another object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the polyfill in line with the extension's handling of
BcMath\Number.BcMath\Numberis afinalinternal class with a customcreate_objectwhose__unserialize()rejects an empty payload, sonewInstanceWithoutConstructor()is refused and there is no empty-shell prototype. Two bugs:deepclone_from_array()reconstructs such an object via the fullO:serialization form, which runs__unserialize()internally, and then called__unserialize()a second time in the states loop, throwingCannot modify readonly property. The redundant call is now skipped for these objects.deepclone_hydrate(BcMath\Number::class, ...)hitclone null(aTypeError), because the class is reported cloneable while its prototype isnull. It now throws\DeepClone\NotInstantiableExceptionwith the same message as the extension; such a class cannot be built by property injection, only by a full serialization round-trip.Companion extension fix: symfony/php-ext-deepclone#20