diff --git a/Zend/tests/assert/expect_015.phpt b/Zend/tests/assert/expect_015.phpt index 695c4c166a83c..9f8e9b77003bc 100644 --- a/Zend/tests/assert/expect_015.phpt +++ b/Zend/tests/assert/expect_015.phpt @@ -183,7 +183,7 @@ assert(0 && ($a = function () { $x = $a ?? $b; [$a, $b, $c] = [1, 2 => 'x', 'z' => 'c']; @foo(); - $y = clone $x; + $y = \clone($x); yield 1 => 2; yield from $x; })) diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt new file mode 100644 index 0000000000000..d11f2ac53cbb8 --- /dev/null +++ b/Zend/tests/clone/ast.phpt @@ -0,0 +1,49 @@ +--TEST-- +Ast Printing +--FILE-- +getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x, foo: $foo, bar: $bar)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x, ...$array)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x, ...[ + "foo" => $foo, + "bar" => $bar, + ])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + + +?> +--EXPECT-- +assert(false && ($y = \clone($x))) +assert(false && ($y = \clone($x))) +assert(false && ($y = \clone($x, foo: $foo, bar: $bar))) +assert(false && ($y = \clone($x, ...$array))) +assert(false && ($y = \clone($x, ...['foo' => $foo, 'bar' => $bar]))) diff --git a/Zend/tests/clone/bug36071.phpt b/Zend/tests/clone/bug36071.phpt index 945118fef3754..d9b3bc9cbe1f2 100644 --- a/Zend/tests/clone/bug36071.phpt +++ b/Zend/tests/clone/bug36071.phpt @@ -8,7 +8,8 @@ $a = clone 0; $a[0]->b = 0; ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug36071.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, int given in %s:%d Stack trace: -#0 {main} +#0 %s(%d): clone(0) +#1 {main} thrown in %sbug36071.php on line 2 diff --git a/Zend/tests/clone/bug42817.phpt b/Zend/tests/clone/bug42817.phpt index a681d861d0c8f..635e8f2a608d0 100644 --- a/Zend/tests/clone/bug42817.phpt +++ b/Zend/tests/clone/bug42817.phpt @@ -6,7 +6,8 @@ $a = clone(null); array_push($a->b, $c); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug42817.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d Stack trace: -#0 {main} +#0 %s(%d): clone(NULL) +#1 {main} thrown in %sbug42817.php on line 2 diff --git a/Zend/tests/clone/bug42818.phpt b/Zend/tests/clone/bug42818.phpt index b37ce13fd174a..9cd7050d55b97 100644 --- a/Zend/tests/clone/bug42818.phpt +++ b/Zend/tests/clone/bug42818.phpt @@ -5,7 +5,8 @@ Bug #42818 ($foo = clone(array()); leaks memory) $foo = clone(array()); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug42818.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d Stack trace: -#0 {main} +#0 %s(%d): clone(Array) +#1 {main} thrown in %sbug42818.php on line 2 diff --git a/Zend/tests/clone/callback.phpt b/Zend/tests/clone/callback.phpt new file mode 100644 index 0000000000000..30ce6a93bc59f --- /dev/null +++ b/Zend/tests/clone/callback.phpt @@ -0,0 +1,24 @@ +--TEST-- +As Callback +--FILE-- +clone_me()[0]; + +var_dump($f !== $clone); + +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/clone/clone_001.phpt b/Zend/tests/clone/clone_001.phpt index 87024c3cd5614..dbbb16049b80b 100644 --- a/Zend/tests/clone/clone_001.phpt +++ b/Zend/tests/clone/clone_001.phpt @@ -7,7 +7,8 @@ $a = clone array(); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %s:%d +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d Stack trace: -#0 {main} +#0 %s(%d): clone(Array) +#1 {main} thrown in %s on line %d diff --git a/Zend/tests/clone/clone_003.phpt b/Zend/tests/clone/clone_003.phpt index f163616a876dc..19f775f1e9ff3 100644 --- a/Zend/tests/clone/clone_003.phpt +++ b/Zend/tests/clone/clone_003.phpt @@ -9,7 +9,8 @@ $a = clone $b; --EXPECTF-- Warning: Undefined variable $b in %s on line %d -Fatal error: Uncaught Error: __clone method called on non-object in %s:%d +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d Stack trace: -#0 {main} +#0 %s(%d): clone(NULL) +#1 {main} thrown in %s on line %d diff --git a/Zend/tests/clone/clone_with_001.phpt b/Zend/tests/clone/clone_with_001.phpt new file mode 100644 index 0000000000000..bd2f70a0d53a8 --- /dev/null +++ b/Zend/tests/clone/clone_with_001.phpt @@ -0,0 +1,81 @@ +--TEST-- +Clone with basic +--FILE-- + 'BAZ', + 'array' => [1, 2, 3], +]; + +function gen() { + yield 'from_gen' => 'value'; +} + +var_dump(clone $x); +var_dump(clone($x)); +var_dump(clone($x, foo: $foo, bar: $bar)); +var_dump(clone($x, ...$array)); +var_dump(clone($x, ...['obj' => $x])); + +var_dump(clone($x, ...[ + 'abc', + 'def', + new Dummy(), + 'named' => 'value', +])); + +var_dump(clone($x, ...gen())); + +?> +--EXPECTF-- +object(stdClass)#%d (0) { +} +object(stdClass)#%d (0) { +} +object(stdClass)#%d (2) { + ["foo"]=> + string(3) "FOO" + ["bar"]=> + object(Dummy)#%d (0) { + } +} +object(stdClass)#%d (2) { + ["baz"]=> + string(3) "BAZ" + ["array"]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } +} +object(stdClass)#%d (1) { + ["obj"]=> + object(stdClass)#%d (0) { + } +} +object(stdClass)#%d (4) { + ["0"]=> + string(3) "abc" + ["1"]=> + string(3) "def" + ["2"]=> + object(Dummy)#%d (0) { + } + ["named"]=> + string(5) "value" +} +object(stdClass)#%d (1) { + ["from_gen"]=> + string(5) "value" +} diff --git a/Zend/tests/clone/clone_with_002.phpt b/Zend/tests/clone/clone_with_002.phpt new file mode 100644 index 0000000000000..362cc73e03cdf --- /dev/null +++ b/Zend/tests/clone/clone_with_002.phpt @@ -0,0 +1,114 @@ +--TEST-- +Clone with respects visiblity +--FILE-- +m1()); + +$c = new C(); +var_dump($c->m1()); +var_dump($c->m2()); +try { + var_dump($c->m3()); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump(clone($p, b: 'inaccessible')); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump(clone($p, d: 'inaccessible')); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump((new Unrelated())->m3($p)); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +object(P)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(7) "default" + ["c":"P":private]=> + string(7) "default" + ["d"]=> + string(7) "default" +} +object(P)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(9) "updated C" + ["d"]=> + string(9) "updated D" +} +object(C)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(9) "updated C" + ["d"]=> + string(9) "updated D" +} + +Deprecated: Creation of dynamic property C::$c is deprecated in %s on line %d +object(C)#%d (5) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(7) "default" + ["d"]=> + string(7) "default" + ["c"]=> + string(9) "dynamic C" +} +Error: Cannot modify private(set) property P::$d from scope C +Error: Cannot access protected property P::$b +Error: Cannot modify private(set) property P::$d from global scope +Error: Cannot access protected property P::$b diff --git a/Zend/tests/clone/clone_with_003.phpt b/Zend/tests/clone/clone_with_003.phpt new file mode 100644 index 0000000000000..0bc671d1cd822 --- /dev/null +++ b/Zend/tests/clone/clone_with_003.phpt @@ -0,0 +1,23 @@ +--TEST-- +Clone with supports property hooks +--FILE-- +hooked = strtoupper($value); + } + } +} + +$c = new Clazz(); + +var_dump(clone($c, hooked: 'updated')); + +?> +--EXPECTF-- +object(Clazz)#%d (1) { + ["hooked"]=> + string(7) "UPDATED" +} diff --git a/Zend/tests/clone/clone_with_004.phpt b/Zend/tests/clone/clone_with_004.phpt new file mode 100644 index 0000000000000..cd60b202a9a2f --- /dev/null +++ b/Zend/tests/clone/clone_with_004.phpt @@ -0,0 +1,82 @@ +--TEST-- +Clone with evaluation order +--FILE-- +hooked = strtoupper($value); + } + } + + public string $maxLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) > 5) { + throw new \Exception('Length exceeded'); + } + + $this->maxLength = $value; + } + } + + public string $minLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) < 5) { + throw new \Exception('Length unsufficient'); + } + + $this->minLength = $value; + } + } +} + +$c = new Clazz(); + +var_dump(clone($c, hooked: 'updated')); +echo PHP_EOL; +var_dump(clone($c, hooked: 'updated', maxLength: 'abc', minLength: 'abcdef')); +echo PHP_EOL; +var_dump(clone($c, minLength: 'abcdef', hooked: 'updated', maxLength: 'abc')); + +?> +--EXPECTF-- +$hooked::set +object(Clazz)#%d (1) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + uninitialized(string) + ["minLength"]=> + uninitialized(string) +} + +$hooked::set +$maxLength::set +$minLength::set +object(Clazz)#%d (3) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + string(3) "abc" + ["minLength"]=> + string(6) "abcdef" +} + +$minLength::set +$hooked::set +$maxLength::set +object(Clazz)#%d (3) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + string(3) "abc" + ["minLength"]=> + string(6) "abcdef" +} diff --git a/Zend/tests/clone/clone_with_005.phpt b/Zend/tests/clone/clone_with_005.phpt new file mode 100644 index 0000000000000..abea8416fc967 --- /dev/null +++ b/Zend/tests/clone/clone_with_005.phpt @@ -0,0 +1,64 @@ +--TEST-- +Clone with error handling +--FILE-- +hooked = strtoupper($value); + } + } + + public string $maxLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) > 5) { + throw new \Exception('Length exceeded'); + } + + $this->maxLength = $value; + } + } + + public string $minLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) < 5) { + throw new \Exception('Length insufficient'); + } + + $this->minLength = $value; + } + } +} + +$c = new Clazz(); + +try { + var_dump(clone($c, hooked: 'updated', maxLength: 'abcdef', minLength: 'abc')); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +echo PHP_EOL; + +try { + var_dump(clone($c, hooked: 'updated', minLength: 'abc', maxLength: 'abcdef')); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +$hooked::set +$maxLength::set +Exception: Length exceeded + +$hooked::set +$minLength::set +Exception: Length insufficient diff --git a/Zend/tests/clone/clone_with_006.phpt b/Zend/tests/clone/clone_with_006.phpt new file mode 100644 index 0000000000000..fe5589b358798 --- /dev/null +++ b/Zend/tests/clone/clone_with_006.phpt @@ -0,0 +1,16 @@ +--TEST-- +Clone with error cases +--FILE-- +getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +TypeError: Only arrays and Traversables can be unpacked, int given diff --git a/Zend/tests/clone/clone_with_007.phpt b/Zend/tests/clone/clone_with_007.phpt new file mode 100644 index 0000000000000..c26d1dfebd77c --- /dev/null +++ b/Zend/tests/clone/clone_with_007.phpt @@ -0,0 +1,29 @@ +--TEST-- +Clone with supports __clone +--FILE-- +foo = 'foo updated in __clone'; + $this->bar = 'bar updated in __clone'; + } +} + +$c = new Clazz('foo', 'bar'); + +var_dump(clone($c, foo: 'foo updated in clone-with')); + +?> +--EXPECTF-- +object(Clazz)#%d (2) { + ["foo"]=> + string(25) "foo updated in clone-with" + ["bar"]=> + string(22) "bar updated in __clone" +} diff --git a/Zend/tests/clone/clone_with_008.phpt b/Zend/tests/clone/clone_with_008.phpt new file mode 100644 index 0000000000000..1a983290b5556 --- /dev/null +++ b/Zend/tests/clone/clone_with_008.phpt @@ -0,0 +1,35 @@ +--TEST-- +Clone with readonly +--FILE-- +b = '__clone'; + } +} + +$c = new Clazz('default', 'default'); + +var_dump(clone($c, a: "with")); + +try { + var_dump(clone($c, b: "with")); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +object(Clazz)#%d (2) { + ["a"]=> + string(4) "with" + ["b"]=> + string(7) "__clone" +} +Error: Cannot modify readonly property Clazz::$b diff --git a/Zend/tests/clone/clone_with_009.phpt b/Zend/tests/clone/clone_with_009.phpt new file mode 100644 index 0000000000000..e9a79b7558a95 --- /dev/null +++ b/Zend/tests/clone/clone_with_009.phpt @@ -0,0 +1,72 @@ +--TEST-- +Clone with lazy objects +--FILE-- +isUninitializedLazyObject($obj)); + var_dump($obj); + var_dump($reflector->isUninitializedLazyObject($clone)); + var_dump($clone); +} + +$reflector = new ReflectionClass(C::class); + +$obj = $reflector->newLazyGhost(function ($obj) { + var_dump("initializer"); + $obj->__construct(); +}); + +test('Ghost', $obj); + +$obj = $reflector->newLazyProxy(function ($obj) { + var_dump("initializer"); + return new C(); +}); + +test('Proxy', $obj); + +?> +--EXPECTF-- +# Ghost: +string(11) "initializer" +bool(false) +object(C)#%d (1) { + ["a"]=> + int(1) +} +bool(false) +object(C)#%d (1) { + ["a"]=> + int(2) +} +# Proxy: +string(11) "initializer" +bool(false) +lazy proxy object(C)#%d (1) { + ["instance"]=> + object(C)#%d (1) { + ["a"]=> + int(1) + } +} +bool(false) +lazy proxy object(C)#%d (1) { + ["instance"]=> + object(C)#%d (1) { + ["a"]=> + int(2) + } +} diff --git a/Zend/tests/clone/clone_with_010.phpt b/Zend/tests/clone/clone_with_010.phpt new file mode 100644 index 0000000000000..97778a2420b01 --- /dev/null +++ b/Zend/tests/clone/clone_with_010.phpt @@ -0,0 +1,21 @@ +--TEST-- +Clone with native classes +--FILE-- +getMessage(), PHP_EOL; +} + +try { + var_dump(clone(new \Random\Engine\Xoshiro256StarStar(), with: "something")); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Error: Trying to clone an uncloneable object of class Random\Engine\Secure +Error: Trying to clone an object with updated properties that is not compatible Random\Engine\Xoshiro256StarStar diff --git a/Zend/tests/generators/clone.phpt b/Zend/tests/generators/clone.phpt index 1e8da25136a13..748b826365cc0 100644 --- a/Zend/tests/generators/clone.phpt +++ b/Zend/tests/generators/clone.phpt @@ -7,12 +7,14 @@ function gen() { yield; } -$gen = gen(); -clone $gen; + +try { + $gen = gen(); + clone $gen; +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught Error: Trying to clone an uncloneable object of class Generator in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +Error: Trying to clone an uncloneable object of class Generator diff --git a/Zend/tests/magic_methods/bug73288.phpt b/Zend/tests/magic_methods/bug73288.phpt index 52e8eedeaf013..09819f0853cca 100644 --- a/Zend/tests/magic_methods/bug73288.phpt +++ b/Zend/tests/magic_methods/bug73288.phpt @@ -28,7 +28,8 @@ test_clone(); --EXPECTF-- Fatal error: Uncaught Exception: No Cloneable in %sbug73288.php:%d Stack trace: -#0 %s(%d): NoClone->__clone() -#1 %s(%d): test_clone() -#2 {main} +#0 [internal function]: NoClone->__clone() +#1 %s(%d): clone(Object(NoClone)) +#2 %s(%d): test_clone() +#3 {main} thrown in %sbug73288.php on line %d diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 2551f876d4465..6d6804b39f626 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2285,7 +2285,15 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appendc(str, '`'); break; case ZEND_AST_CLONE: - PREFIX_OP("clone ", 270, 271); + smart_str_appends(str, "clone"); + smart_str_appendc(str, '('); + zend_ast_export_ex(str, ast->child[0], 0, indent); + if (ast->child[1]) { + smart_str_appends(str, ", "); + zend_ast_export_ex(str, ast->child[1], 0, indent); + } + smart_str_appendc(str, ')'); + break; case ZEND_AST_PRINT: PREFIX_OP("print ", 60, 61); case ZEND_AST_INCLUDE_OR_EVAL: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 9348c35f6cc07..261cccc571fde 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -89,7 +89,6 @@ enum _zend_ast_kind { ZEND_AST_ISSET, ZEND_AST_SILENCE, ZEND_AST_SHELL_EXEC, - ZEND_AST_CLONE, ZEND_AST_PRINT, ZEND_AST_INCLUDE_OR_EVAL, ZEND_AST_UNARY_OP, @@ -154,6 +153,7 @@ enum _zend_ast_kind { ZEND_AST_MATCH_ARM, ZEND_AST_NAMED_ARG, ZEND_AST_PARENT_PROPERTY_HOOK_CALL, + ZEND_AST_CLONE, /* 3 child nodes */ ZEND_AST_METHOD_CALL = 3 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 7a07ceadce2e2..c5b393ca51454 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -69,6 +69,81 @@ zend_result zend_startup_builtin_functions(void) /* {{{ */ } /* }}} */ +ZEND_FUNCTION(clone) +{ + zend_object *zobj; + zval *args; + uint32_t argc; + HashTable *named_params; + + ZEND_PARSE_PARAMETERS_START(1, -1) + Z_PARAM_OBJ(zobj) + Z_PARAM_VARIADIC_WITH_NAMED(args, argc, named_params); + ZEND_PARSE_PARAMETERS_END(); + + zend_class_entry *scope = zend_get_executed_scope(); + + zend_class_entry *ce = zobj->ce; + zend_function *clone = ce->clone; + + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { + zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (clone && !(clone->common.fn_flags & ZEND_ACC_PUBLIC)) { + if (clone->common.scope != scope) { + if (UNEXPECTED(clone->common.fn_flags & ZEND_ACC_PRIVATE) + || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { + zend_throw_error(NULL, "Call to %s %s::__clone() from %s%s", + zend_visibility_string(clone->common.fn_flags), ZSTR_VAL(clone->common.scope->name), + scope ? "scope " : "global scope", + scope ? ZSTR_VAL(scope->name) : "" + ); + RETURN_THROWS(); + } + } + } + + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + if (UNEXPECTED(argc > 0)) { + HashTable params; + zend_hash_init(¶ms, argc + (named_params ? zend_hash_num_elements(named_params) : 0), NULL, NULL, false); + for (uint32_t i = 0; i < argc; i++) { + zend_hash_index_add_new(¶ms, i, &args[i]); + } + if (named_params != NULL) { + zend_string *key; + zval *val; + ZEND_HASH_FOREACH_STR_KEY_VAL(named_params, key, val) { + zend_hash_update(¶ms, key, val); + } ZEND_HASH_FOREACH_END(); + } + cloned = zobj->handlers->clone_obj_with(zobj, scope, ¶ms); + zend_hash_destroy(¶ms); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, named_params); + } + } else { + if (UNEXPECTED(named_params || argc > 0)) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + cloned = zobj->handlers->clone_obj(zobj); + } + + if (EG(exception)) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + RETURN_THROWS(); + } + + RETURN_OBJ(cloned); +} + ZEND_FUNCTION(exit) { zend_string *str = NULL; diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 7f316835aea6b..56ccb11922f94 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -7,6 +7,8 @@ class stdClass { } +function _clone(object $object, mixed ...$updatedProperties): object {} + function exit(string|int $status = 0): never {} /** @alias exit */ diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index 9498b8292f892..2297d45629310 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,10 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: a24761186f1ddf758e648b0a764826537cbd33b9 */ + * Stub hash: 1bdd20e0dc61506977d38c240d7cad3b0acf7f98 */ + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) + ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, updatedProperties, IS_MIXED, 0) +ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_exit, 0, 0, IS_NEVER, 0) ZEND_ARG_TYPE_MASK(0, status, MAY_BE_STRING|MAY_BE_LONG, "0") @@ -243,6 +248,7 @@ static const zend_frameless_function_info frameless_function_infos_class_exists[ { 0 }, }; +ZEND_FUNCTION(clone); ZEND_FUNCTION(exit); ZEND_FUNCTION(zend_version); ZEND_FUNCTION(func_num_args); @@ -306,6 +312,7 @@ ZEND_FUNCTION(gc_disable); ZEND_FUNCTION(gc_status); static const zend_function_entry ext_functions[] = { + ZEND_FE(clone, arginfo_clone) ZEND_FE(exit, arginfo_exit) ZEND_RAW_FENTRY("die", zif_exit, arginfo_die, 0, NULL, NULL) ZEND_FE(zend_version, arginfo_zend_version) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 2ad3f6b323d8f..4e983c1cfa0a1 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5404,7 +5404,50 @@ static void zend_compile_clone(znode *result, zend_ast *ast) /* {{{ */ znode obj_node; zend_compile_expr(&obj_node, obj_ast); - zend_emit_op_tmp(result, ZEND_CLONE, &obj_node, NULL); + znode value_node; + if (ast->child[1]) { + ZEND_ASSERT(ast->child[1]->kind == ZEND_AST_ARG_LIST); + zend_ast_list *args_ast = zend_ast_get_list(ast->child[1]); + ZEND_ASSERT(args_ast->children > 0); + + zend_ast *array; + zend_ast *last; + + for (uint32_t i = 0; i < args_ast->children; ++i) { + zend_ast *arg = args_ast->child[i]; + + switch (arg->kind) { + case ZEND_AST_UNPACK: + if (args_ast->children != 1) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot combine unpacking with other arguments"); + } + + array = arg->child[0]; + + break; + case ZEND_AST_NAMED_ARG: { + zend_ast *elem = zend_ast_create(ZEND_AST_ARRAY_ELEM, arg->child[1], arg->child[0]); + if (i == 0) { + array = zend_ast_create_list(args_ast->children, ZEND_AST_ARRAY, elem); + last = array; + } else { + last = zend_ast_list_add(last, elem); + } + break; + } + default: + zend_error_noreturn(E_COMPILE_ERROR, "Cannot use unnamed argument"); + break; + } + } + + zend_compile_expr(&value_node, array); + } else { + value_node.op_type = IS_UNUSED; + } + + + zend_emit_op_tmp(result, ZEND_CLONE, &obj_node, &value_node); } /* }}} */ diff --git a/Zend/zend_iterators.c b/Zend/zend_iterators.c index f67033b11161c..64dbb0541a80d 100644 --- a/Zend/zend_iterators.c +++ b/Zend/zend_iterators.c @@ -31,6 +31,7 @@ static const zend_object_handlers iterator_object_handlers = { iter_wrapper_free, iter_wrapper_dtor, NULL, /* clone_obj */ + NULL, /* clone_obj_with */ NULL, /* prop read */ NULL, /* prop write */ NULL, /* read dim */ diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 9483a83b4e955..ddf22f7b851f4 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -46,7 +46,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %define api.pure full %define api.value.type {zend_parser_stack_elem} %define parse.error verbose -%expect 0 +%expect 1 %destructor { zend_ast_destroy($$); } %destructor { if ($$) zend_string_release_ex($$, 0); } @@ -1214,7 +1214,16 @@ expr: { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); } | variable '=' ampersand variable { $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); } - | T_CLONE expr { $$ = zend_ast_create(ZEND_AST_CLONE, $2); } + | T_CLONE argument_list { + zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); + name->attr = ZEND_NAME_FQ; + $$ = zend_ast_create(ZEND_AST_CALL, name, $2); + } + | T_CLONE expr { + zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); + name->attr = ZEND_NAME_FQ; + $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2)); + } | variable T_PLUS_EQUAL expr { $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); } | variable T_MINUS_EQUAL expr diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index d1b950160e1cc..acd0c00fa6ca4 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -709,7 +709,7 @@ ZEND_API HashTable *zend_lazy_object_get_properties(zend_object *object) /* Initialize object and clone it. For proxies, we clone both the proxy and its * real instance, and we don't call __clone() on the proxy. */ -zend_object *zend_lazy_object_clone(zend_object *old_obj) +zend_object *zend_lazy_object_clone(zend_object *old_obj, zend_class_entry *scope, HashTable *properties) { ZEND_ASSERT(zend_object_is_lazy(old_obj)); @@ -724,7 +724,7 @@ zend_object *zend_lazy_object_clone(zend_object *old_obj) } if (!zend_object_is_lazy_proxy(old_obj)) { - return zend_objects_clone_obj(old_obj); + return zend_objects_clone_obj_with(old_obj, scope, properties); } zend_lazy_object_info *info = zend_lazy_object_get_info(old_obj); @@ -748,7 +748,7 @@ zend_object *zend_lazy_object_clone(zend_object *old_obj) zend_lazy_object_info *new_info = emalloc(sizeof(*info)); *new_info = *info; - new_info->u.instance = zend_objects_clone_obj(info->u.instance); + new_info->u.instance = zend_objects_clone_obj_with(info->u.instance, scope, properties); zend_lazy_object_set_info(new_proxy, new_info); diff --git a/Zend/zend_lazy_objects.h b/Zend/zend_lazy_objects.h index 64f68d66360cd..db185ef60f485 100644 --- a/Zend/zend_lazy_objects.h +++ b/Zend/zend_lazy_objects.h @@ -71,7 +71,7 @@ zend_object *zend_lazy_object_get_instance(zend_object *obj); zend_lazy_object_flags_t zend_lazy_object_get_flags(zend_object *obj); void zend_lazy_object_del_info(zend_object *obj); ZEND_API HashTable *zend_lazy_object_get_properties(zend_object *object); -zend_object *zend_lazy_object_clone(zend_object *old_obj); +zend_object *zend_lazy_object_clone(zend_object *old_obj, zend_class_entry *scope, HashTable *properties); HashTable *zend_lazy_object_debug_info(zend_object *object, int *is_temp); HashTable *zend_lazy_object_get_gc(zend_object *zobj, zval **table, int *n); bool zend_lazy_object_decr_lazy_props(zend_object *obj); diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index a31c7d2afdeee..cbbf747de4147 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -2522,6 +2522,7 @@ ZEND_API const zend_object_handlers std_object_handlers = { zend_object_std_dtor, /* free_obj */ zend_objects_destroy_object, /* dtor_obj */ zend_objects_clone_obj, /* clone_obj */ + zend_objects_clone_obj_with, /* clone_obj_with */ zend_std_read_property, /* read_property */ zend_std_write_property, /* write_property */ diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h index 7e7d3df37a6ad..30644e05ddb2f 100644 --- a/Zend/zend_object_handlers.h +++ b/Zend/zend_object_handlers.h @@ -180,6 +180,7 @@ typedef void (*zend_object_free_obj_t)(zend_object *object); typedef void (*zend_object_dtor_obj_t)(zend_object *object); typedef zend_object* (*zend_object_clone_obj_t)(zend_object *object); +typedef zend_object* (*zend_object_clone_obj_with_t)(zend_object *object, zend_class_entry *scope, HashTable *properties); /* Get class name for display in var_dump and other debugging functions. * Must be defined and must return a non-NULL value. */ @@ -209,6 +210,7 @@ struct _zend_object_handlers { zend_object_free_obj_t free_obj; /* required */ zend_object_dtor_obj_t dtor_obj; /* required */ zend_object_clone_obj_t clone_obj; /* optional */ + zend_object_clone_obj_with_t clone_obj_with; /* optional */ zend_object_read_property_t read_property; /* required */ zend_object_write_property_t write_property; /* required */ zend_object_read_dimension_t read_dimension; /* required */ diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index fd0e97c5f4131..ae64fce55756d 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -213,9 +213,9 @@ ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce) return object; } -ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object) +ZEND_API void ZEND_FASTCALL zend_objects_clone_members_ex(zend_object *new_object, zend_object *old_object, zend_class_entry *scope, HashTable *properties) { - bool has_clone_method = old_object->ce->clone != NULL; + bool has_clone_method = old_object->ce->clone != NULL || properties != NULL; if (old_object->ce->default_properties_count) { zval *src = old_object->properties_table; @@ -289,7 +289,35 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, if (has_clone_method) { GC_ADDREF(new_object); - zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL); + if (old_object->ce->clone) { + zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL); + } + + if (EXPECTED(!EG(exception)) && properties != NULL) { + zend_class_entry *old_scope = EG(fake_scope); + + EG(fake_scope) = scope; + + zend_ulong num_key; + zend_string *key; + zval *val; + ZEND_HASH_FOREACH_KEY_VAL(properties, num_key, key, val) { + if (UNEXPECTED(key == NULL)) { + key = zend_long_to_str(num_key); + new_object->handlers->write_property(new_object, key, val, NULL); + zend_string_release_ex(key, false); + } else { + new_object->handlers->write_property(new_object, key, val, NULL); + } + + if (UNEXPECTED(EG(exception))) { + break; + } + } ZEND_HASH_FOREACH_END(); + + EG(fake_scope) = old_scope; + } + if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce)) { for (uint32_t i = 0; i < new_object->ce->default_properties_count; i++) { @@ -303,12 +331,33 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, } } -ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) +ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object) +{ + ZEND_ASSERT(old_object->ce == new_object->ce); + + zend_objects_clone_members_ex(new_object, old_object, old_object->ce, NULL); +} + +ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *old_object, zend_class_entry *scope, HashTable *properties) { zend_object *new_object; + /* Compatibility with code that only overrides clone_obj. */ + if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) { + if (!old_object->handlers->clone_obj) { + zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(old_object->ce->name)); + return NULL; + } + if (properties && zend_hash_num_elements(properties) > 0) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(old_object->ce->name)); + return NULL; + } else { + return old_object->handlers->clone_obj(old_object); + } + } + if (UNEXPECTED(zend_object_is_lazy(old_object))) { - return zend_lazy_object_clone(old_object); + return zend_lazy_object_clone(old_object, scope, properties); } /* assume that create isn't overwritten, so when clone depends on the @@ -325,7 +374,12 @@ ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) } while (p != end); } - zend_objects_clone_members(new_object, old_object); + zend_objects_clone_members_ex(new_object, old_object, scope, properties); return new_object; } + +ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) +{ + return zend_objects_clone_obj_with(old_object, old_object->ce, NULL); +} diff --git a/Zend/zend_objects.h b/Zend/zend_objects.h index 41e3bcd9594b1..b4b9285f782c7 100644 --- a/Zend/zend_objects.h +++ b/Zend/zend_objects.h @@ -30,6 +30,7 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, ZEND_API void zend_object_std_dtor(zend_object *object); ZEND_API void zend_objects_destroy_object(zend_object *object); ZEND_API zend_object *zend_objects_clone_obj(zend_object *object); +ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *object, zend_class_entry *scope, HashTable *properties); void zend_object_dtor_dynamic_properties(zend_object *object); void zend_object_dtor_property(zend_object *object, zval *p); diff --git a/Zend/zend_string.h b/Zend/zend_string.h index e9e2b947a6c91..cad79742506df 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -575,6 +575,7 @@ EMPTY_SWITCH_DEFAULT_CASE() _(ZEND_STR_UNKNOWN, "unknown") \ _(ZEND_STR_UNKNOWN_CAPITALIZED, "Unknown") \ _(ZEND_STR_EXIT, "exit") \ + _(ZEND_STR_CLONE, "clone") \ _(ZEND_STR_EVAL, "eval") \ _(ZEND_STR_INCLUDE, "include") \ _(ZEND_STR_REQUIRE, "require") \ diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 9317d1ff592f5..bdd7548568bf5 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6002,7 +6002,6 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = GET_OP1_OBJ_ZVAL_PTR_UNDEF(BP_VAR_R); @@ -6025,6 +6024,7 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) } zend_throw_error(NULL, "__clone method called on non-object"); FREE_OP1(); + FREE_OP2(); HANDLE_EXCEPTION(); } } while (0); @@ -6032,10 +6032,10 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); FREE_OP1(); + FREE_OP2(); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -6047,15 +6047,55 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); FREE_OP1(); + FREE_OP2(); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (OP2_TYPE != IS_UNUSED) { + zval *properties = GET_OP2_ZVAL_PTR(BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("Only arrays can be unpacked for clone, %s given", zend_zval_value_name(properties)); + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (OP2_TYPE != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); FREE_OP1(); + FREE_OP2(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index f7bec6f7198c8..b58858a9973ba 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5172,7 +5172,6 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = RT_CONSTANT(opline, opline->op1); @@ -5195,6 +5194,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ } zend_throw_error(NULL, "__clone method called on non-object"); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -5202,10 +5202,10 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -5217,14 +5217,55 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("Only arrays can be unpacked for clone, %s given", zend_zval_value_name(properties)); + + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj(zobj); } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -15330,7 +15371,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -15353,6 +15393,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND } zend_throw_error(NULL, "__clone method called on non-object"); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -15360,10 +15401,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -15375,15 +15416,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("Only arrays can be unpacked for clone, %s given", zend_zval_value_name(properties)); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -33495,7 +33576,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = &EX(This); @@ -33518,6 +33598,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND } zend_throw_error(NULL, "__clone method called on non-object"); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -33525,10 +33606,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -33540,14 +33621,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("Only arrays can be unpacked for clone, %s given", zend_zval_value_name(properties)); + + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj(zobj); } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -41000,7 +41122,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = EX_VAR(opline->op1.var); @@ -41023,6 +41144,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC } zend_throw_error(NULL, "__clone method called on non-object"); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -41030,10 +41152,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -41045,14 +41167,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("Only arrays can be unpacked for clone, %s given", zend_zval_value_name(properties)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } diff --git a/build/gen_stub.php b/build/gen_stub.php index e69846733a9d9..dcf7fc7630aeb 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -983,6 +983,9 @@ class FunctionName implements FunctionOrMethodName { private /* readonly */ Name $name; public function __construct(Name $name) { + if ($name->name === '_clone') { + $name = new Name('clone', $name->getAttributes()); + } $this->name = $name; } @@ -2942,6 +2945,7 @@ class PropertyInfo extends VariableLike private const PHP_85_KNOWN = [ "self" => "ZEND_STR_SELF", "parent" => "ZEND_STR_PARENT", + "clone" => "ZEND_STR_CLONE", ]; /** diff --git a/ext/com_dotnet/com_handlers.c b/ext/com_dotnet/com_handlers.c index af980b7b86f2a..638fc5d8a3ae6 100644 --- a/ext/com_dotnet/com_handlers.c +++ b/ext/com_dotnet/com_handlers.c @@ -514,6 +514,7 @@ zend_object_handlers php_com_object_handlers = { php_com_object_free_storage, zend_objects_destroy_object, php_com_object_clone, + NULL, /* clone_with */ com_property_read, com_property_write, com_read_dimension, diff --git a/ext/com_dotnet/com_saproxy.c b/ext/com_dotnet/com_saproxy.c index ea0e9e47a13d9..ec79faa30a32b 100644 --- a/ext/com_dotnet/com_saproxy.c +++ b/ext/com_dotnet/com_saproxy.c @@ -402,6 +402,7 @@ zend_object_handlers php_com_saproxy_handlers = { saproxy_free_storage, zend_objects_destroy_object, saproxy_clone, + NULL, /* clone_with */ saproxy_property_read, saproxy_property_write, saproxy_read_dimension, diff --git a/ext/dom/tests/modern/token_list/clone.phpt b/ext/dom/tests/modern/token_list/clone.phpt index 039551f2d43d8..e0c71e9fd7910 100644 --- a/ext/dom/tests/modern/token_list/clone.phpt +++ b/ext/dom/tests/modern/token_list/clone.phpt @@ -7,11 +7,12 @@ dom $dom = DOM\XMLDocument::createFromString(''); $element = $dom->documentElement; -clone $element->classList; +try { + clone $element->classList; +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught Error: Trying to clone an uncloneable object of class Dom\TokenList in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +Error: Trying to clone an uncloneable object of class Dom\TokenList diff --git a/ext/reflection/tests/ReflectionClass_CannotClone_basic.phpt b/ext/reflection/tests/ReflectionClass_CannotClone_basic.phpt index 58ce9c65dce0f..64d884d6ae02c 100644 --- a/ext/reflection/tests/ReflectionClass_CannotClone_basic.phpt +++ b/ext/reflection/tests/ReflectionClass_CannotClone_basic.phpt @@ -6,10 +6,11 @@ TestFest PHP|Tek --FILE-- getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught Error: Trying to clone an uncloneable object of class ReflectionClass in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +Error: Trying to clone an uncloneable object of class ReflectionClass diff --git a/ext/reflection/tests/ReflectionClass_isCloneable_001.phpt b/ext/reflection/tests/ReflectionClass_isCloneable_001.phpt index 0b3701f1bb610..bde3a60a1e7f9 100644 --- a/ext/reflection/tests/ReflectionClass_isCloneable_001.phpt +++ b/ext/reflection/tests/ReflectionClass_isCloneable_001.phpt @@ -49,10 +49,14 @@ $obj = new ReflectionClass('xmlwriter'); var_dump($obj->isCloneable()); $obj = new ReflectionObject(new XMLWriter); var_dump($obj->isCloneable()); -$h = clone new xmlwriter; +try { + $h = clone new xmlwriter; +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} ?> ---EXPECTF-- +--EXPECT-- User class bool(true) bool(true) @@ -68,8 +72,4 @@ bool(true) Internal class - XMLWriter bool(false) bool(false) - -Fatal error: Uncaught Error: Trying to clone an uncloneable object of class XMLWriter in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +Error: Trying to clone an uncloneable object of class XMLWriter diff --git a/tests/classes/factory_and_singleton_007.phpt b/tests/classes/factory_and_singleton_007.phpt index 2c35090eed5e0..fc232bdb8655a 100644 --- a/tests/classes/factory_and_singleton_007.phpt +++ b/tests/classes/factory_and_singleton_007.phpt @@ -8,14 +8,13 @@ class test { } } -$obj = new test; -$clone = clone $obj; -$obj = NULL; +try { + $obj = new test; + $clone = clone $obj; +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} -echo "Done\n"; ?> ---EXPECTF-- -Fatal error: Uncaught Error: Call to protected test::__clone() from global scope in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +Error: Call to protected test::__clone() from global scope diff --git a/tests/classes/factory_and_singleton_008.phpt b/tests/classes/factory_and_singleton_008.phpt index 2b2c0721c75e5..672c083270730 100644 --- a/tests/classes/factory_and_singleton_008.phpt +++ b/tests/classes/factory_and_singleton_008.phpt @@ -8,14 +8,13 @@ class test { } } -$obj = new test; -$clone = clone $obj; -$obj = NULL; +try { + $obj = new test; + $clone = clone $obj; +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} -echo "Done\n"; ?> ---EXPECTF-- -Fatal error: Uncaught Error: Call to private test::__clone() from global scope in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +Error: Call to private test::__clone() from global scope