-
Notifications
You must be signed in to change notification settings - Fork 7.9k
RFC: Clone with v2 #18747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
RFC: Clone with v2 #18747
Conversation
de238ac
to
b99daf4
Compare
…jects_clone_members_ex()`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I did not (yet) carefully look at all tests.
|
||
if (EXPECTED(!EG(exception)) && zend_hash_num_elements(properties) > 0) { | ||
/* Unlock readonly properties once more. */ | ||
if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce) && old_object->ce->clone) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to unlock all properties again? After all, multiple writes to to readonly properties are forbidden within __clone
. This problem can still occur with something like:
class C {
public function __construct(
private readonly string $prop,
) {}
public string $a { set { $this->prop = $value; } }
public string $b { set { $this->prop = $value; } }
}
var_dump(clone(new C('foo'), [
'a' => 'a',
'b' => 'b',
]));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regardless, this can be skipped if !old_object->ce->clone
as nothing happened between unlocking and this path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to unlock all properties again?
I found it easiest to just unlock them again, instead of trying to find the property fields corresponding to the “with” properties.
Regardless, this can be skipped if !old_object->ce->clone as nothing happened between unlocking and this path.
This condition is already there, or did I misunderstand?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found it easiest to just unlock them again, instead of trying to find the property fields corresponding to the “with” properties.
My statement wasn't very precise. I was more wondering whether we need to unlock them at all. Namely, isn't it at least somewhat expected a property written to by __clone
shouldn't also be overwritten again? It wasn't clearly specified how this would be handled in the RFC from what I can see. Regardless, I understand why you're doing it so probably just disregard.
This condition is already there, or did I misunderstand?
No, I just missed it. Apologies.
|
||
ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) | ||
{ | ||
return zend_objects_clone_obj_with(old_object, old_object->ce, &zend_empty_array); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: It seems like scope
is unused when the properties array is empty. Maybe it's better to pass NULL
here? Same in zend_objects_clone_members()
.
{ | ||
zend_object *new_object; | ||
|
||
/* Compatibility with code that only overrides clone_obj. */ | ||
if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this problematic for overridden clone_obj
that reuse zend_objects_clone_obj
? In that case old_object->handlers->clone_obj != zend_objects_clone_obj
would hit, and we'd recurse infinitely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. There are none in php-src and generally speaking it seems unlikely that there are any, since when there is a need to overwrite clone
there likely also is a need to overwrite create
(which explicitly isn't supported):
/* assume that create isn't overwritten, so when clone depends on the
- overwritten one then it must itself be overwritten */
Most of the custom clone handlers just use zend_objects_clone_members()
.
This PR tries to minimize the disruption for existing code, but if there is an external extension that does what you suggest, then it's easy enough for the author to adjust it to add an explicit clone_with
handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, you could do the inverse: Keep clone_obj
as is, call it whenever properties are not provided or when the array is empty. When it isn't, clone_obj_with
is invoked, which can call clone_obj
to clone the object, and adjust the properties afterwards. This way, an extension could simply continue to provide clone_obj
and the property part would just work. This approach seems more logical to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which can call clone_obj to clone the object, and adjust the properties afterwards
This is likely dangerous, since some internal classes might expect their (readonly) properties not to change, which leads to some C structures related to a property not being properly adjusted. Random\Randomizer
is currently not cloneable at all, but it stores both a public readonly \Random\Engine $engine
property exposed to userland and also the raw “engine pointer” for more efficient access.
The implementation also prefers the clone_with
handler, since I would like to see the old clone_obj
hook to go away in the future to remove the duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely dangerous, since some internal classes might expect their (readonly) properties not to change
Can this already be broken with __clone
in a user-declared sub-class? I can't see cases other than enums preventing the declaration of __clone
. The property would need to be private(set)
to prevent this.
|
||
class Clazz { | ||
public string $hooked = 'default' { | ||
set (string $value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set (string $value) { | |
set { |
NIt: Let's stay consistent with the code style in existing tests.
RFC: https://wiki.php.net/rfc/clone_with_v2
see TimWolla#6 for a preliminary review.