test(clone-with): add comprehensive tests for PHP 8.5 clone-with functionality

- Add test for property hooks preservation and invocation after __clone
- Add test for private/protected/readonly property scope handling in clone-with
- Add test for trait method scope usage inside clone-with operations
- Update documentation to reflect completed clone-with implementation coverage
- Remove pending classification for clone-with scoped/readonly/hooked properties
- Remove TODO comment about property handlers preservation in clone-with operation
master
韩天峰 1 day ago
parent 795a8e0a1d
commit b2246cd456
  1. 2
      docs/INCOMPATIBLE_PHP_FEATURES.md
  2. 1
      docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md
  3. 4
      src/Translator.php
  4. 71
      tests/compiler/basic/clone-with-property-hooks.phpt
  5. 39
      tests/compiler/basic/clone-with-scope-readonly.phpt
  6. 45
      tests/compiler/basic/clone-with-trait-scope.phpt

@ -16,7 +16,7 @@
- 不支持可变变量 `$$var` - 不支持可变变量 `$$var`
- 暂不支持 PHP 8.5 `#[NoDiscard]` - 暂不支持 PHP 8.5 `#[NoDiscard]`
- 支持 PHP 8.5 `(void)` 显式丢弃语句;操作数仍会求值并保留副作用,不能在赋值、返回、参数或条件等值上下文中使用。 - 支持 PHP 8.5 `(void)` 显式丢弃语句;操作数仍会求值并保留副作用,不能在赋值、返回、参数或条件等值上下文中使用。
- PHP 8.5 `clone()` / clone-with 依赖实际链接的 `libphp` 版本不低于 8.5。普通公开属性、动态属性、调用顺序、错误传播和 callable 路径已有 PHPT;TypePHP 内部类的 private/protected/readonly 更新仍是 XFAIL,property hook 类的自定义 object handlers 也尚未兼容 `clone_obj_with` - PHP 8.5 `clone()` / clone-with 依赖实际链接的 `libphp` 版本不低于 8.5。公开、动态、private/protected/readonly 和 property hook 属性,以及调用顺序、错误传播和 callable 路径均有 PHPT 覆盖
- PHP 8.4 property hooks 会编译为 AOT getter/setter,并注册对应的 Zend hook 元数据;直接属性读写、Reflection 和对象遍历均受支持。当前不支持对 hook 属性取引用。 - PHP 8.4 property hooks 会编译为 AOT getter/setter,并注册对应的 Zend hook 元数据;直接属性读写、Reflection 和对象遍历均受支持。当前不支持对 hook 属性取引用。
- PHP 8.4 Reflection Lazy Object 不能用于 TypePHP AOT 类。AOT 类以 persistent internal class 注册,而 Zend 的 `zend_object_make_lazy()` 明确拒绝 internal class;运行时动态加载的 ZendPHP user class 不受此限制。 - PHP 8.4 Reflection Lazy Object 不能用于 TypePHP AOT 类。AOT 类以 persistent internal class 注册,而 Zend 的 `zend_object_make_lazy()` 明确拒绝 internal class;运行时动态加载的 ZendPHP user class 不受此限制。
- 支持 `private(set)``protected(set)` 非对称属性可见性,并通过 PHP 8.4+ 的类级对象 handler 执行同等作用域检查。 - 支持 `private(set)``protected(set)` 非对称属性可见性,并通过 PHP 8.4+ 的类级对象 handler 执行同等作用域检查。

@ -82,7 +82,6 @@ These items should be documented with the exact boundary.
| Feature | Classification | Implementation Direction | | Feature | Classification | Implementation Direction |
|---|---|---| |---|---|---|
| PHP 8.5 `#[NoDiscard]` | Pending | Preserve PHP's warning behavior and timing consistently for native direct calls and Zend runtime fallbacks. TypePHP's compile-time `#[MustUse]` remains a separate feature. | | PHP 8.5 `#[NoDiscard]` | Pending | Preserve PHP's warning behavior and timing consistently for native direct calls and Zend runtime fallbacks. TypePHP's compile-time `#[MustUse]` remains a separate feature. |
| PHP 8.5 clone-with for scoped/readonly/hooked TypePHP properties | Pending | Preserve the lexical class scope in the Zend call frame and teach TypePHP's custom object handlers to clone through `clone_obj_with` without losing hook handlers. Public/dynamic properties and callable/error paths already delegate correctly to a PHP 8.5 runtime. |
| Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. | | Variable variables (`$$var`) | Pending | Add a function-local symbol table mirror for dynamic locals, and disable or synchronize native locals that escape into dynamic lookup. |
| Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. | | Closure or arrow function returning by reference | Pending | Closure metadata and wrappers must preserve return-by-reference and emit `ReturnRef`. |
| PHP 8.5 closures in constants, parameter defaults or property defaults | Pending | Use context-aware runtime initializers: cache constants and property defaults per request, create parameter defaults per omitted call, and never place request-local zvals in persistent MINIT storage. | | PHP 8.5 closures in constants, parameter defaults or property defaults | Pending | Use context-aware runtime initializers: cache constants and property defaults per request, create parameter defaults per omitted call, and never place request-local zvals in persistent MINIT storage. |

@ -2214,10 +2214,6 @@ CODE;
return $body . "return obj;\n"; return $body . "return obj;\n";
}; };
// TODO(PHP 8.5 clone-with): typephp_install_property_handlers()
// must preserve these custom handlers on the cloned object and
// provide compatible clone_obj_with behavior. Zend's default
// clone path currently drops hook dispatch for TypePHP classes.
$code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n"; $code .= "typephp_install_property_handlers({$ce}, &{$handlers});\n";
if ($classDef->requireCtor) { if ($classDef->requireCtor) {
$code .= "create_object_{$className} = php::getCreateObjectFn({$ce});\n"; $code .= "create_object_{$className} = php::getCreateObjectFn({$ce});\n";

@ -0,0 +1,71 @@
--TEST--
PHP 8.5 clone-with preserves TypePHP property handlers and invokes hooks after __clone
--SKIPIF--
<?php
if (PHP_VERSION_ID < 80500) {
die('skip requires PHP 8.5');
}
?>
--FILE--
<?php
class CloneWithHookValue
{
public int $value {
get => $this->value * 2;
set {
echo 'set:', $value, "\n";
if ($value < 0) {
throw new InvalidArgumentException('negative value');
}
$this->value = $value + 1;
}
}
public function __construct()
{
$this->value = 1;
}
public function __clone(): void
{
echo '__clone:', $this->value, "\n";
}
public function withValue(int $value): self
{
return clone($this, ['value' => $value]);
}
}
class CloneWithHookChild extends CloneWithHookValue {}
function read_clone_hook_dynamically(mixed $object): int
{
return $object->value;
}
function main(): void
{
$source = new CloneWithHookChild();
$copy = $source->withValue(5);
var_dump(read_clone_hook_dynamically($source));
var_dump(read_clone_hook_dynamically($copy));
try {
$source->withValue(-1);
} catch (InvalidArgumentException $error) {
echo $error->getMessage(), "\n";
}
}
?>
--EXPECT--
set:1
__clone:4
set:5
int(4)
int(12)
__clone:4
set:-1
negative value

@ -6,8 +6,6 @@ if (PHP_VERSION_ID < 80500) {
die('skip requires PHP 8.5'); die('skip requires PHP 8.5');
} }
?> ?>
--XFAIL--
TypePHP internal classes do not yet preserve private/protected/readonly property scope during clone-with
--FILE-- --FILE--
<?php <?php
@ -34,6 +32,16 @@ class CloneWithScopeBase
{ {
return [$this->privateValue, $this->protectedValue, $this->readonlyValue]; return [$this->privateValue, $this->protectedValue, $this->readonlyValue];
} }
public function withInvalidPrivate(): self
{
return clone($this, ['privateValue' => 'invalid']);
}
public function reinitializeReadonly(int $value): void
{
$this->readonlyValue = $value;
}
} }
class CloneWithScopeChild extends CloneWithScopeBase class CloneWithScopeChild extends CloneWithScopeBase
@ -54,6 +62,23 @@ function main(): void
var_dump($privateCopy->values()); var_dump($privateCopy->values());
var_dump($protectedCopy->values()); var_dump($protectedCopy->values());
// PHP leaves readonly slots omitted from the update array reinitializable
// once on the clone. A slot explicitly updated by clone-with is locked.
$protectedCopy->reinitializeReadonly(31);
var_dump($protectedCopy->values());
try {
$privateCopy->reinitializeReadonly(40);
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
try {
$source->withInvalidPrivate();
} catch (TypeError $error) {
echo $error::class, ":private\n";
}
try { try {
clone($source, ['protectedValue' => 99]); clone($source, ['protectedValue' => 99]);
} catch (Error $error) { } catch (Error $error) {
@ -92,5 +117,15 @@ array(3) {
[2]=> [2]=>
int(3) int(3)
} }
array(3) {
[0]=>
int(1)
[1]=>
int(20)
[2]=>
int(31)
}
Cannot modify readonly property CloneWithScopeBase::$readonlyValue
TypeError:private
Cannot access protected property CloneWithScopeChild::$protectedValue Cannot access protected property CloneWithScopeChild::$protectedValue
Cannot modify protected(set) readonly property CloneWithScopeBase::$readonlyValue from global scope Cannot modify protected(set) readonly property CloneWithScopeBase::$readonlyValue from global scope

@ -0,0 +1,45 @@
--TEST--
PHP 8.5 clone-with uses the consuming class scope inside trait methods
--SKIPIF--
<?php
if (PHP_VERSION_ID < 80500) {
die('skip requires PHP 8.5');
}
?>
--FILE--
<?php
trait CloneWithTraitScope
{
private int $traitPrivate = 1;
public function withTraitPrivate(int $value): self
{
return clone($this, ['traitPrivate' => $value]);
}
public function traitPrivate(): int
{
return $this->traitPrivate;
}
}
class CloneWithTraitBase
{
use CloneWithTraitScope;
}
class CloneWithTraitChild extends CloneWithTraitBase {}
function main(): void
{
$source = new CloneWithTraitChild();
$copy = $source->withTraitPrivate(9);
var_dump($source->traitPrivate());
var_dump($copy->traitPrivate());
}
?>
--EXPECT--
int(1)
int(9)
Loading…
Cancel
Save