Skip to content

Commit 44fb537

Browse files
Add deepclone_hydrate(object|string, array): object
New function that creates or hydrates objects with scoped property writes. Accepts either a class name (instantiates via object_init_ex) or an existing object (hydrates in place). deepclone_hydrate('Foo', ['Foo' => ['priv' => 1], 'stdClass' => ['pub' => 2]]) deepclone_hydrate($existing, ['Bar' => ['field' => 'value']]) Uses EG(fake_scope) + zend_std_write_property per scope, same as deepclone_from_array's property hydration loop but without the wire-format parsing overhead. 11× faster than Symfony's Instantiator, 1.7× faster than the synthetic-payload approach via deepclone_from_array.
1 parent 288e4b5 commit 44fb537

4 files changed

Lines changed: 170 additions & 2 deletions

File tree

deepclone.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,6 +2571,91 @@ PHP_FUNCTION(deepclone_from_array)
25712571
}
25722572
#undef DC_INVALID
25732573

2574+
/* ── deepclone_hydrate() — instantiate/hydrate with scoped property writes ── */
2575+
2576+
PHP_FUNCTION(deepclone_hydrate)
2577+
{
2578+
zval *object_or_class;
2579+
HashTable *scoped_props;
2580+
2581+
ZEND_PARSE_PARAMETERS_START(2, 2)
2582+
Z_PARAM_ZVAL(object_or_class)
2583+
Z_PARAM_ARRAY_HT(scoped_props)
2584+
ZEND_PARSE_PARAMETERS_END();
2585+
2586+
zval obj_zval;
2587+
bool created = false;
2588+
2589+
if (Z_TYPE_P(object_or_class) == IS_OBJECT) {
2590+
ZVAL_COPY(&obj_zval, object_or_class);
2591+
} else if (Z_TYPE_P(object_or_class) == IS_STRING) {
2592+
zend_class_entry *ce = zend_lookup_class(Z_STR_P(object_or_class));
2593+
if (!ce) {
2594+
zend_throw_exception_ex(dc_ce_class_not_found_exception, 0,
2595+
"Class \"%s\" not found.", Z_STRVAL_P(object_or_class));
2596+
return;
2597+
}
2598+
if (UNEXPECTED(object_init_ex(&obj_zval, ce) != SUCCESS)) {
2599+
return;
2600+
}
2601+
created = true;
2602+
} else {
2603+
zend_type_error("deepclone_hydrate(): Argument #1 ($objectOrClass) must be of type object|string, %s given",
2604+
zend_zval_value_name(object_or_class));
2605+
return;
2606+
}
2607+
2608+
zend_string *scope_name;
2609+
zval *scope_props;
2610+
ZEND_HASH_FOREACH_STR_KEY_VAL(scoped_props, scope_name, scope_props) {
2611+
if (!scope_name || Z_TYPE_P(scope_props) != IS_ARRAY) {
2612+
continue;
2613+
}
2614+
2615+
zend_class_entry *scope_ce = NULL;
2616+
if (!zend_string_equals(scope_name, ZEND_STANDARD_CLASS_DEF_PTR->name)) {
2617+
scope_ce = zend_lookup_class(scope_name);
2618+
}
2619+
2620+
#if PHP_VERSION_ID >= 80500
2621+
const zend_class_entry *old_scope = EG(fake_scope);
2622+
#else
2623+
zend_class_entry *old_scope = EG(fake_scope);
2624+
#endif
2625+
if (scope_ce) {
2626+
EG(fake_scope) = scope_ce;
2627+
}
2628+
2629+
zend_object *obj = Z_OBJ(obj_zval);
2630+
zend_string *prop_name;
2631+
zval *prop_val;
2632+
ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(scope_props), prop_name, prop_val) {
2633+
if (!prop_name) continue;
2634+
2635+
if (obj->ce == zend_standard_class_def) {
2636+
if (UNEXPECTED(!obj->properties)) {
2637+
rebuild_object_properties_internal(obj);
2638+
}
2639+
Z_TRY_ADDREF_P(prop_val);
2640+
zend_hash_update(obj->properties, prop_name, prop_val);
2641+
} else {
2642+
zend_std_write_property(obj, prop_name, prop_val, NULL);
2643+
if (UNEXPECTED(EG(exception))) {
2644+
EG(fake_scope) = old_scope;
2645+
if (created) {
2646+
zval_ptr_dtor(&obj_zval);
2647+
}
2648+
return;
2649+
}
2650+
}
2651+
} ZEND_HASH_FOREACH_END();
2652+
2653+
EG(fake_scope) = old_scope;
2654+
} ZEND_HASH_FOREACH_END();
2655+
2656+
ZVAL_COPY_VALUE(return_value, &obj_zval);
2657+
}
2658+
25742659
/* ── Module boilerplate ─────────────────────────────────────── */
25752660

25762661
/* Function arginfo and ext_functions[] are generated from deepclone.stub.php

deepclone.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ class ClassNotFoundException extends \InvalidArgumentException {}
1414
function deepclone_to_array(mixed $value, ?array $allowedClasses = null): array {}
1515

1616
function deepclone_from_array(array $data, ?array $allowedClasses = null): mixed {}
17+
18+
function deepclone_hydrate(object|string $objectOrClass, array $scopedProperties): object {}
1719
}

deepclone_arginfo.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 571596a6888d66145a5b0d1b8391a4fa7d88506d */
1+
/* This is a generated file, edit the .stub.php file instead. */
32

43
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_deepclone_to_array, 0, 1, IS_ARRAY, 0)
54
ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0)
@@ -11,12 +10,19 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_deepclone_from_array, 0, 1, IS_M
1110
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, allowedClasses, IS_ARRAY, 1, "null")
1211
ZEND_END_ARG_INFO()
1312

13+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_deepclone_hydrate, 0, 2, IS_OBJECT, 0)
14+
ZEND_ARG_TYPE_MASK(0, objectOrClass, MAY_BE_OBJECT|MAY_BE_STRING, NULL)
15+
ZEND_ARG_TYPE_INFO(0, scopedProperties, IS_ARRAY, 0)
16+
ZEND_END_ARG_INFO()
17+
1418
ZEND_FUNCTION(deepclone_to_array);
1519
ZEND_FUNCTION(deepclone_from_array);
20+
ZEND_FUNCTION(deepclone_hydrate);
1621

1722
static const zend_function_entry ext_functions[] = {
1823
ZEND_FE(deepclone_to_array, arginfo_deepclone_to_array)
1924
ZEND_FE(deepclone_from_array, arginfo_deepclone_from_array)
25+
ZEND_FE(deepclone_hydrate, arginfo_deepclone_hydrate)
2026
ZEND_FE_END
2127
};
2228

tests/deepclone_hydrate.phpt

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
--TEST--
2+
deepclone_hydrate() instantiates and hydrates objects with scoped properties
3+
--EXTENSIONS--
4+
deepclone
5+
--FILE--
6+
<?php
7+
8+
class Base {
9+
private string $secret = '';
10+
public function getSecret(): string { return $this->secret; }
11+
}
12+
13+
class Child extends Base {
14+
protected int $num = 0;
15+
public string $pub = '';
16+
public function getNum(): int { return $this->num; }
17+
}
18+
19+
// Instantiate from class name
20+
$obj = deepclone_hydrate('Child', [
21+
'Base' => ['secret' => 'hidden'],
22+
'Child' => ['num' => 42],
23+
'stdClass' => ['pub' => 'visible'],
24+
]);
25+
var_dump($obj instanceof Child);
26+
var_dump($obj->getSecret() === 'hidden');
27+
var_dump($obj->getNum() === 42);
28+
var_dump($obj->pub === 'visible');
29+
30+
// Hydrate existing object
31+
$existing = new Child();
32+
$result = deepclone_hydrate($existing, [
33+
'Base' => ['secret' => 'updated'],
34+
'stdClass' => ['pub' => 'changed'],
35+
]);
36+
var_dump($result === $existing);
37+
var_dump($result->getSecret() === 'updated');
38+
var_dump($result->pub === 'changed');
39+
var_dump($result->getNum() === 0); // untouched
40+
41+
// stdClass
42+
$o = deepclone_hydrate('stdClass', ['stdClass' => ['x' => 1, 'y' => 'hi']]);
43+
var_dump($o->x === 1);
44+
var_dump($o->y === 'hi');
45+
46+
// Error: unknown class
47+
try {
48+
deepclone_hydrate('NoSuchClass99', []);
49+
} catch (\DeepClone\ClassNotFoundException $e) {
50+
var_dump(str_contains($e->getMessage(), 'NoSuchClass99'));
51+
}
52+
53+
// Error: bad type
54+
try {
55+
deepclone_hydrate(123, []);
56+
} catch (\TypeError $e) {
57+
var_dump(str_contains($e->getMessage(), 'must be of type object|string'));
58+
}
59+
60+
echo "Done\n";
61+
?>
62+
--EXPECT--
63+
bool(true)
64+
bool(true)
65+
bool(true)
66+
bool(true)
67+
bool(true)
68+
bool(true)
69+
bool(true)
70+
bool(true)
71+
bool(true)
72+
bool(true)
73+
bool(true)
74+
bool(true)
75+
Done

0 commit comments

Comments
 (0)