Skip to content

[DeepClone] Fix round-tripping and hydrating BcMath\Number#628

Merged
nicolas-grekas merged 1 commit into
1.xfrom
deepclone-bcmath-number
Jun 8, 2026
Merged

[DeepClone] Fix round-tripping and hydrating BcMath\Number#628
nicolas-grekas merged 1 commit into
1.xfrom
deepclone-bcmath-number

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Member
Q A
Branch? 1.x
Bug fix? yes
New feature? no
Deprecations? no
Issues symfony/symfony#64323
License MIT

Brings the polyfill in line with the extension's handling of BcMath\Number.

BcMath\Number is a final internal class with a custom create_object whose __unserialize() rejects an empty payload, so newInstanceWithoutConstructor() is refused and there is no empty-shell prototype. Two bugs:

  • Round-trip. deepclone_from_array() reconstructs such an object via the full O: serialization form, which runs __unserialize() internally, and then called __unserialize() a second time in the states loop, throwing Cannot modify readonly property. The redundant call is now skipped for these objects.
  • Hydrate. deepclone_hydrate(BcMath\Number::class, ...) hit clone null (a TypeError), because the class is reported cloneable while its prototype is null. It now throws \DeepClone\NotInstantiableException with 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

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 nicolas-grekas merged commit 37b9438 into 1.x Jun 8, 2026
30 of 40 checks passed
@nicolas-grekas nicolas-grekas deleted the deepclone-bcmath-number branch June 8, 2026 20:07
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
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