diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb2b6f8..56eac2c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,14 +17,16 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4', '8.5'] zts: [false] include: - php: '8.4' zts: true - # 8.5 tracks the development branch (nightly); failures here are - # informational and must not block the matrix. - - php: '8.5' + # 8.6 tracks the development branch (nightly); failures here are + # informational and must not block the matrix. It is also the only + # job that exercises the zend_reflection_property_set_raw_value_* + # PHPAPI path (PHP_VERSION_ID >= 80600). + - php: '8.6' zts: false experimental: true continue-on-error: ${{ matrix.experimental == true }} diff --git a/deepclone.c b/deepclone.c index c5731d4..9d0783b 100644 --- a/deepclone.c +++ b/deepclone.c @@ -68,6 +68,22 @@ extern PHPAPI zend_class_entry *reflector_ptr; extern PHPAPI zend_class_entry *reflection_type_ptr; extern PHPAPI zend_class_entry *reflection_property_ptr; +/* PHPAPI helpers exposed by ext/reflection in PHP 8.6+. They encapsulate the + * setRawValue / setRawValueWithoutLazyInitialization logic — including the + * trampoline-based hook bypass and lazy-prop/realize handling — that we + * previously had to either re-implement or delegate to via a userland + * ReflectionProperty round-trip. */ +#if PHP_VERSION_ID >= 80600 +extern PHPAPI void zend_reflection_property_set_raw_value( + zend_property_info *prop, zend_string *unmangled_name, + void *cache_slot[3], const zend_class_entry *scope, + zend_object *object, zval *value); +extern PHPAPI void zend_reflection_property_set_raw_value_without_lazy_initialization( + zend_property_info *prop, zend_string *unmangled_name, + void *cache_slot[3], const zend_class_entry *scope, + zend_object *object, zval *value); +#endif + /* ── Compatibility shims for older PHP versions ────────────── */ /* zend_zval_value_name() landed in PHP 8.3 (returns "true"/"false"/"null" @@ -705,7 +721,7 @@ static zend_always_inline zval *dc_name_subarray_find(HashTable *scope_ht, zend_ return zend_hash_find_known_hash(scope_ht, name); } -#if PHP_VERSION_ID >= 80400 +#if PHP_VERSION_ID >= 80400 && PHP_VERSION_ID < 80600 /* fn_proxy slot cached across calls — first invocation fills it via method * lookup; subsequent invocations reuse the resolved zend_function*. */ static zend_function *dc_set_raw_no_lazy_fn = NULL; @@ -714,9 +730,11 @@ static zend_function *dc_set_raw_no_lazy_fn = NULL; * ReflectionProperty instances). Defined later; forward-declared here. */ static void dc_lazy_refl_cache_dtor(zval *zv); -/* Delegates to ReflectionProperty::setRawValueWithoutLazyInitialization because the - * required engine helpers (zend_lazy_object_decr_lazy_props, _realize) aren't ZEND_API. - * Per-request cache keyed on pi — the ReflectionProperty is per-class, not per-instance. */ +/* Pre-PHP-8.6 fallback: PHP 8.6 exposes zend_reflection_property_set_raw_value_ + * without_lazy_initialization() as PHPAPI, so the lazy-prop dance lives in one + * place in ext/reflection. On 8.4/8.5 we delegate through a userland + * ReflectionProperty round-trip — construct (cached per pi) + invoke + * setRawValueWithoutLazyInitialization($obj, $value). */ static bool dc_set_raw_value_without_lazy_init(zend_object *obj, zend_property_info *pi, zend_string *name, zval *value) { @@ -781,8 +799,8 @@ static bool dc_set_raw_value_without_lazy_init(zend_object *obj, static bool dc_write_backed_property(zend_object *obj, zend_property_info *pi, zend_string *name, zval *value, zend_long flags) { + bool call_hooks = (flags & DEEPCLONE_HYDRATE_CALL_HOOKS) != 0; #if PHP_VERSION_ID >= 80400 - bool call_hooks = (flags & DEEPCLONE_HYDRATE_CALL_HOOKS) != 0; bool no_lazy_init = (flags & DEEPCLONE_HYDRATE_NO_LAZY_INIT) != 0; /* Lazy objects: a direct slot write would bypass the engine's realization @@ -863,17 +881,23 @@ static bool dc_write_backed_property(zend_object *obj, zend_property_info *pi, #if PHP_VERSION_ID >= 80400 /* Skip the Reflection round-trip when there's no lazy-init to skip. */ if (no_lazy_init && !zend_lazy_object_initialized(obj)) { +# if PHP_VERSION_ID >= 80600 + zend_reflection_property_set_raw_value_without_lazy_initialization( + pi, name, NULL, pi->ce, obj, value); + bool ok = !EG(exception); +# else bool ok = dc_set_raw_value_without_lazy_init(obj, pi, name, value); -#if PHP_VERSION_ID >= 80100 +# endif +# if PHP_VERSION_ID >= 80100 if (enum_holder_used) { zval_ptr_dtor(&enum_holder); } -#endif +# endif return ok; } #endif - if (!ZEND_TYPE_IS_SET(pi->type) && !DC_PROP_HAS_HOOKS(pi)) { + if (!ZEND_TYPE_IS_SET(pi->type) && !DC_PROP_HAS_HOOKS(pi) && !call_hooks) { /* Move the old value out before running its destructor: a __destruct * on the old value can legitimately read (or reassign) this same slot. * Install the new value first so reentrant reads see a valid slot. */ @@ -905,7 +929,14 @@ static bool dc_write_backed_property(zend_object *obj, zend_property_info *pi, } zval_ptr_dtor(&old); } -#if PHP_VERSION_ID >= 80400 +#if PHP_VERSION_ID >= 80600 + else if (!call_hooks) { + /* Default mode: setRawValue semantics (bypass set hook on hooked + * non-virtual, type-check on typed). One PHPAPI call replaces our + * old trampoline + zend_update_property_ex split. */ + zend_reflection_property_set_raw_value(pi, name, NULL, pi->ce, obj, value); + } +#elif PHP_VERSION_ID >= 80400 else if (!call_hooks && DC_PROP_HAS_HOOKS(pi) && pi->hooks[ZEND_PROPERTY_HOOK_SET]) { zend_function *trampoline = zend_get_property_hook_trampoline( pi, ZEND_PROPERTY_HOOK_SET, name); @@ -5551,7 +5582,7 @@ static PHP_GSHUTDOWN_FUNCTION(deepclone) } } -#if PHP_VERSION_ID >= 80400 +#if PHP_VERSION_ID >= 80400 && PHP_VERSION_ID < 80600 static void dc_lazy_refl_cache_dtor(zval *zv) { zend_object_release((zend_object *) Z_PTR_P(zv));