Skip to content

Keep the last of two keys resolving to the same property - #46

Merged
nicolas-grekas merged 1 commit into
mainfrom
dynamic-parent-private
Sep 29, 2026
Merged

nicolas-grekas merged 1 commit into
mainfrom
dynamic-parent-private

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Member

Builds on #45, for its scope fix and to not conflict in the changelog.

unserialize() writes a dynamic property named like a private property of a parent class to that private property, and deepclone_to_array() scopes it to that parent accordingly. But when the private property is set too, both were added under the same name with zend_hash_add_new(): debug builds of PHP abort there, and release builds exported the private value where unserialize(serialize()) ends up with the dynamic one:

$o = new Child(); // #[AllowDynamicProperties], extends a class with a private $secret
$o->setSecret('private');
$o->secret = 'dynamic';
deepclone_from_array(deepclone_to_array($o)); // its private $secret is 'dynamic' now

The same happened with the bare and mangled names of a property returned by __serialize() without __unserialize(). The last key now wins, like with unserialize() and the polyfill.

A dynamic property named like a private property of a parent class is
scoped to that parent, as unserialize() writes it to that private property.
With the private property set too, both were added under the same name with
zend_hash_add_new(): the first value was exported instead of the last one,
and debug builds of PHP aborted. The same happened with the bare and mangled
names of a property returned by __serialize() without __unserialize().
Update the entry instead, so that the last key wins, as with unserialize().
@nicolas-grekas
nicolas-grekas merged commit 78d8b13 into main Sep 29, 2026
24 checks passed
nicolas-grekas added a commit that referenced this pull request Sep 29, 2026
… (nicolas-grekas)

This PR was merged into the main branch.

Discussion
----------

Align payloads and warnings with the polyfill and serialize()

Builds on #46, and has a sibling PR in symfony/polyfill#702.

`deepclone_to_array()` listed the properties and markers of objects interleaved with the ones of the objects they reference, while the polyfill lists them object by object: the same value gave different payloads, eg Doctrine's class metadata or the container builder of the Symfony Demo. Both now produce the same payloads, so that caches and exported code don't depend on which one wrote them.

The names returned by `__sleep()` are resolved in their order, like `serialize()` does, so that `__unserialize()` gets them in that order too, with the warnings of `serialize()` at its levels: names returned twice and unset untyped properties were silent. This also cuts the instructions needed to export Doctrine's class metadata by 29%, while graphs of plain objects cost up to 2% more.

Default values returned by `__serialize()` without `__unserialize()` are kept, as `unserialize()` writes each key in turn, integer keys aren't dropped anymore, undeclared protected ones become dynamic properties, and closed resources are reported as `Unknown resource` instead of `(null) resource`.

Commits
-------

b54b752 Align payloads and warnings with the polyfill and serialize()
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