Skip to content

[DeepClone] Fix round-tripping objects whose __serialize() nests another object#629

Merged
nicolas-grekas merged 1 commit into
1.xfrom
deepclone-nested-serialize-objects
Jun 8, 2026
Merged

[DeepClone] Fix round-tripping objects whose __serialize() nests another object#629
nicolas-grekas merged 1 commit into
1.xfrom
deepclone-nested-serialize-objects

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Member
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):

$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.

…her object

A needsFullUnserialize object (a final internal class with __unserialize
that rejects an empty payload, e.g. Random\Randomizer) whose __serialize()
nests another object carries an object-ref mask on its state. Such an
object cannot be built as an early empty shell, and the eager-finalize
pass skipped masked states, leaving a null placeholder; when another
object referenced it, the properties loop threw "unknown object id"
before the states loop could reconstruct it.

These objects are now finalized to a fixpoint after the refs pass, once
the objects their mask references exist. Round-tripping Random\Randomizer
as an object property (or deeper) now works, matching the extension.

A shared object nested inside such a state is still reconstructed as an
independent copy (pure PHP cannot inject a live shared object into an
internal final class's __unserialize); the extension preserves identity
there.
@nicolas-grekas nicolas-grekas force-pushed the deepclone-nested-serialize-objects branch from b3cced5 to daa74de Compare June 8, 2026 20:10
@nicolas-grekas nicolas-grekas merged commit 2c26fff into 1.x Jun 8, 2026
27 of 40 checks passed
@nicolas-grekas nicolas-grekas deleted the deepclone-nested-serialize-objects branch June 8, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant